拖了很久的shiro分析
漏洞概述
Apache Shiro <= 1.2.4 版本中,加密的用戶信息序列化后存儲在Cookie的rememberMe字段中,攻擊者可以使用Shiro的AES加密算法的默認密鑰來構造惡意的Cookie rememberMe值,發送到Shiro服務端之后會先后進行Base64解碼、AES解密、readObject()反序列化,從而觸發Java原生反序列化漏洞,進而實現RCE。
該漏洞的根源在於硬編碼Key。
漏洞復現
使用shiroattack工具
Dnslog接收到請求
執行命令
漏洞分析
遠程調試
用idea連vulhub的docker環境來進行調試
首先進入容器
docker exec -it 34db756dfcfc /bin/bash
可以看到是用jar包起的環境,把文件拷貝出來(也可以docker-compose up -d
之后使用docker ps --no-trunc
來查看容器默認的啟動命令)
docker cp 34db756dfcfc:/shirodemo-1.0-SNAPSHOT.jar ~/
然后把jar包解壓了之后用idea打開
libraries里導入
在module中添加BOOT_INF
這個目錄
另外還需要改一下dockerfile
idea遠程調試docker
需要增加一組端口供調試用,這里我們用idea默認的5005
vulhub的shiro環境是java -jar xxx.jar的形式運行的,那么添加對jar程序啟動的調試命令即可,在啟動docker時用自定義的COMMAND替換默認的COMMAND
version: '2'
services:
web:
image: vulhub/shiro:1.2.4
ports:
- "8080:8080"
- "5005:5005"
command: java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar /shirodemo-1.0-SNAPSHOT.jar
然后配置好remote
下好斷點,輸入登錄賬戶密碼測試,看到以下界面說明成功了
原理分析
從官方的 issue 上來看,存在幾個重要的點:
- rememberMe cookie
- CookieRememberMeManager.java
- Base64
- AES
- 加密密鑰硬編碼
- Java serialization
首先正常登錄
返回的cookie值中的rememberme值如下:
o5NA+QAjgJpe6uKkIJ1li/WLqOAR2KfLIY3BzwfAUFbbkBSEfs3/259i4Qc2jq6lxUpLabYK2c0oR4faB3l8m0GDhzMvVYjFOR2TPKRtQmqlUAdaiJT06biAC56EMu6UbBJAujAgP1msHuJYkV1fDuhdLN5wGK+IlEpr1Xf+aa6oNkPqBkRG7+B/3bXQNTqmLFlarZUWxB6TwZyshplhx0ckyIfc0qJuf/f5Tt60gK/D28JUI93Gp3vGi/P7UUiwv2Qyzpz4hXZSUocGlC73qE+62ZvQ1ryzVRjpfTkG4Hat6sst04wbpFwdUSJxh6t4FLJ2i9bs5eIm/1UJpVHP9Eia5WaAShQa45qr5yIMA+q1rYxtz0WufvOi67fpqY3qi8LQ/ZnGwXUY+o6dLu2qmqHwTXbRTRKP4G5d3e5SA9FNvXUhYWRhcwo2zJ2lS2JK/D6S0u3HAak04+3wpbZm0UCJxafXlaFPUDhmiXBtIULcEELqBOaqLr1n7LV+F1Cl7kYNU6GozLVPvNlqW5UtLA==
跟一下登錄生成cookie的過程
生成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方法
之后進入serialize
,對登錄認證信息進行序列化
然后進行加密
org.apache.shiro.mgt.AbstractRememberMeManager
中的encrypt方法如下
protected byte[] encrypt(byte[] serialized) {
byte[] value = serialized;
CipherService cipherService = getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
value = byteSource.getBytes();
}
return value;
}
ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
調用的即為AES算法
可以看到使用CBC模式的AES加密算法,其中Padding規則是PKCS5。
具體實現在org/apache/shiro/crypto/JcaCipherService.java
中
private ByteSource encrypt(byte[] plaintext, byte[] key, byte[] iv, boolean prependIv) throws CryptoException {
final int MODE = javax.crypto.Cipher.ENCRYPT_MODE;
byte[] output;
if (prependIv && iv != null && iv.length > 0) {
byte[] encrypted = crypt(plaintext, key, iv, MODE);
output = new byte[iv.length + encrypted.length];
//now copy the iv bytes + encrypted bytes into one output array:
// iv bytes:
System.arraycopy(iv, 0, output, 0, iv.length);
// + encrypted bytes:
System.arraycopy(encrypted, 0, output, iv.length, encrypted.length);
} else {
output = crypt(plaintext, key, iv, MODE);
}
if (log.isTraceEnabled()) {
log.trace("Incoming plaintext of size " + (plaintext != null ? plaintext.length : 0) + ". Ciphertext " +
"byte array is size " + (output != null ? output.length : 0));
}
return ByteSource.Util.bytes(output);
}
IV(初始化向量)是隨機生成的,將IV放在crtpt()加密的數據之前然后返回
加密結束后,在org/apache/shiro/web/mgt/CookieRememberMeManager.java的rememberSerializedIdentity
方法中進行base64編碼,並通過response返回
這里的byte是前16位隨機的IV+AES密文,然后經過base64編碼
解析cookie
org/apache/shiro/web/mgt/CookieRememberMeManager.java
中會將傳遞的base64字符串進行解碼后放到字節數組中,因為java的序列化字符串即為字節數組
byte[] decoded = Base64.decode(base64);
然后進入解密流程
先解密后進行反序列化
AES是對稱加密,加解密密鑰都是相同的,並且shiro都是將密鑰硬編碼
public void setCipherKey(byte[] cipherKey) {
//Since this method should only be used in symmetric ciphers
//(where the enc and dec keys are the same), set it on both:
setEncryptionCipherKey(cipherKey);
setDecryptionCipherKey(cipherKey);
}
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}
在org/apache/shiro/crypto/JcaCipherService.java
的decrypt()方法中進行解密從cookie中取出iv與加密的序列化數據
調用crypt方法利用密文,key,iv進行解密
解密完成后進入反序列化,看上面的public AbstractRememberMeManager()
這里用的是默認反序列化類
public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
}
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);
try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
@SuppressWarnings({"unchecked"})
T deserialized = (T) ois.readObject();
ois.close();
return deserialized;
} catch (Exception e) {
String msg = "Unable to deserialze argument byte array.";
throw new SerializationException(msg, e);
}
}
readobject()
觸發反序列化
至此,Shiro對Cookie的rememberMe的處理流程已整體調試分析結束。
漏洞修復
Apache Shiro 1.2.5版本的源碼,修復方法就是將使用默認Key加密改為生成隨機的Key加密:https://github.com/apache/shiro/commit/4d5bb000a7f3c02d8960b32e694a565c95976848
參考
https://ares-x.com/2020/04/20/IDEA遠程調試Docker中程序的方法/
https://paper.seebug.org/shiro-rememberme-1-2-4/
https://xz.aliyun.com/t/6493?accounttraceid=052d6170a05a4736a42c47de607a2766sdut#toc-6
https://www.mi1k7ea.com/2020/10/03/淺析Shiro-rememberMe反序列化漏洞(Shiro550)/