前言
本文为是作者耗时两个通宵写出的Shiro反序列化漏洞原理详解,主要涉及Shiro认证的一些基础概念以及为什么能够这样进行攻击。文章削弱了具体代码以及反序列化部分,主要强调很多新手宝宝看不懂(面试常考)的漏洞版本划分即利用区别等内容。因为反序列化部分其实是一个攻击大类,在利用中也只是把对应攻击链Poc进行加密,Shiro反序列化的根本我认为还是在如何能让服务器开始反序列化这一步,即伪造明文这一步。反序列化相关的Sink、找链什么的其实专门去看Java反序列化漏洞比本文好。
Shiro认证:
用户登录时如果点击“记住密码”,会发送请求包类似下面:- POST /login HTTP/1.1
- Host: example.com
- Content-Type: application/x-www-form-urlencoded
- username=admin&password=123456&rememberMe=true //大部分网站rememberMe都在请求体
复制代码 登陆成功:响应包的响应头会有 Set-Cookie: rememberMe=值 ,里面保存加密的用户登录信息- HTTP/1.1 302 Found
- Location: /home
- Set-Cookie: rememberMe=YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=; Path=/; HttpOnly
- Set-Cookie: JSESSIONID=xxx; Path=/; HttpOnly
复制代码 之后的会话中请求头会携带该加密的登录信息的Cookie- GET /home HTTP/1.1
- Host: example.com
- Cookie: rememberMe=YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=; JSESSIONID=xxx
复制代码 登陆失败:- HTTP/1.1 302 Found
- Location: /home
- Set-Cookie: rememberMe=deleteMe; Path=/; HttpOnly
- Set-Cookie: JSESSIONID=xxx; Path=/; HttpOnly
复制代码 rememberMe值的可能情况:
情况典型值示例出现场景rememberMe=登录信息rememberMe=YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=用户登录成功并勾选“记住我”deleteMerememberMe=deleteMe登录失败、Cookie 无效(密钥错误、填充校验失败)、注销、主动清除空值rememberMe=某些框架或浏览器的特殊处理无效值rememberMe=invalid 或乱码攻击者伪造失败、Cookie 损坏无此 Cookie(响应头中不出现 rememberMe)用户未勾选“记住我”,或者解出的明文不是用户信息rememberMe登录信息格式
该值为Base64编码值,解开后的格式与版本有关
情况Shiro 版本IV 类型密钥类型Cookie 结构固定IV + Ciphertext=1.4.2随机随机IV + Ciphertext + Tag反序列化点
当服务器Base64解开Cookie的rememberMe值,将利用Cookie里的IV和服务器内存中Key(启动网站进程生成)解密Ciphertext后,会得到用户登录信息的序列化值,为了读取里面的登录信息具体值,服务器会进行反序列化。
如果攻击者可以自定义该序列化值,让服务器最终能反序列化它,就能触发反序列化攻击。
注意:因为客户端Cookie正常是由服务器的Set-Cookie发来的,所以正确的用户Cookie里的IV就是服务器内存的IV(这里是为后续爆破密钥储备知识点)
加密方式
Shiro截断</strong></p>误判:
这种判断方式可能存在误判
因为我们只知道服务器校验填充是否合法。假如在爆破中间值3[16]时,我只修改了密文2[16]为了找到符合以下公式的值:
0x01 = 遍历的密文2[16] 异或 中间值3[16]
但如果运气好,此时满足
0x02 0x02 = 未遍历密文2[15] 遍历的密文2[16] 异或 中间值3[15] 中间值3[16]
服务器也会返回填充正确,这样的误判直到0x0f 0x0f ... 0x0f 总共16种情况
最终误判概率为:
P(0x0n 误判)=(1/256)^n
等于
P(总误判)≈1/(256×255)=1/65280
阶段二:伪造明文
构造明文3:
因为解密时的公式:
明文3 = 密文2 异或 中间值3
现在已经通过爆破获得 中间值3,只需要修改密文2,就能构造出想让服务器解析的明文3
构造明文2:
因为解密公式:
明文2 = 密文1 异或 中间值2
阶段三:扩容明文分组
上面描述的原理只能控制256(密文分组-1)个字符,对攻击来说远远不够。所以我们需要对分组进行扩容
我们伪造一个密文4,无论他是否合理,服务器都会对这个分组密文进行解密,得到中间值4。由于密文4不合理,中间值4可能也不合理,但我们不关心是否合理,只需要修改密文3,通过填充校验爆破出中间值4就行
这样我们就从原本的:
IV+C1+C2+C3 (可控值:明文2+明文3 16*2=32字节)
变为(扩容1组)
IV+新C1+新C2+新C3+C4 (可控值:明文2+明文3+明文4 16*3=48字节)
同理(扩容2组)
IV+新C1+新C2+新C3+新C4+C5(可控值:明文2+明文3+明文4+明文5 16*4=64字节)
理论可以扩容到shiro上限,但每扩容一个分组,增加16256次爆破
举例:
假如我们原本登录成功得到一个正确Cookie:
Set-Cookie:rememberMe=Base64(IV+密文)
rememberMe=Base64(0x00 ... 0x00 0x01 ... 0x01 0x02 ... 0x02)
通过分组得到:
IV = 0x00 0x00 ... 0x00
C1 = 0x01 0x01 ... 0x01
C2 = 0x02 0x02 ... 0x02
现在按扩容到C4构造:
IV = 0x00 0x00 ... 0x00
C1 = 0x01 0x01 ... 0x01
C2 = 0x02 0x02 ... 0x02
C3 = 0x03 0x03 ... 0x03 (写任意值AES都能解密)
C4 = 0x04 0x04 ... 0x04 (写任意值AES都能解密)
将rememberMe=Base64(IV+C1+C2+C3+C4)作为请求头发送给服务器
服务器会先解密C4,假设得到C4的中间值4(攻击者未知):
中间值4 = F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF
1、获取中间值
爆破倒数第一位
保持C4不变,首先改变C3的最后一个字节C3[16],比如改为0xDD
令rememberMe=Base64(00 ... 00|01 ... 01|02 ... 02|03 ... DD|04 ... 04)
服务器视角:
判断错误:
让0xDD与中间值4[16]的0xFF异或(攻击者不可视),结果得到的不是0x01,所以返回 “密码不正确、校验不合格”的相关内容(不考虑误报),如状态码500 (攻击者只能看到响应信息)
判断正确:
而当C3[16]遍历到 0xFE 时:
0x01 = 中间值4[16] 异或 0xFE
服务器返回“密码不正确、校验合格”的相关内容,如状态码301,那么就得到:
中间值4[16] = 0x01 异或 0xFE
= 0xFF
接着爆破倒数第二位
由于上一步算出了中间值4[16]等于0xFF,可以得到
新C3[16] = 0x02 异或 0xFF
= 0xFD
那么改变C3[15],比如改为0xEE
令rememberMe=Base64(00 ... 00|01 ... 01|02 ... 02|03 ... EE FD|04 ... 04)
服务器视角:
判断错误:
让0xEE 0xFD与中间值4[15]和中间值4[16]的0xFE 0xFF异或(攻击者不可视),结果得到的不是0x02 0x02,所以返回 “密码不正确、校验不合格”的相关内容
判断正确:
当C3[15]遍历到 0xFC 时:
0x02 0x02 = 中间值4[15] 中间值4[16] 异或 0xFC 0xFD
服务器返回“密码不正确、校验合格”的相关内容,如状态码301,那么就得到:
中间值4[15] = 0x02 异或 0xFC
= 0xFE
最终得到完整的中间值4:
中间值4 = F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF
2、伪造明文
因为公式:
明文 = C3 异或 中间值4
而中间值4是已知的,所以公式:
新C3 = 伪造明文4 异或 中间值4
我们伪造一个明文:helloword
步骤 1:
把“helloword”变成 16 字节明文
伪造明文:helloword(9 字节)
PKCS5Padding:再补 7 个 0x07
明文4 = 68 65 6C 6C 6F 77 6F 72 64 07 07 07 07 07 07 07
步骤 2:
计算新 C3(16 字节)
已知:
中间值4 = F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF
逐字节异或:
新C3 = 明文4 异或 中间值4[i
得到(示例值,按位计算即可):
新C3 = 97 93 9F 9F 9A 81 9A 8E 9B F8 F8 F8 F8 F8 F8 F8
3、截断
得到新的C3,现在要进行C3位置的爆破操作,改变C2,固定新C3,去掉C4
该过程发送的Cookie值去掉C4
原Cookie:
IV + C1 + C2 + C3 + C4
00 ... 00 01 ... 01 02 ... 02 03 ... 03 04 04 ... 04
该阶段Cookie:
IV + C1 + C2 + 新C3
00 ... 00 01 ... 01 02 ... 02 97 93 9F 9F 9A 81 9A 8E 9B F8 F8 F8 F8 F8 F8 F8
4、重复获取中间值
该阶段固定伪造出C4位置明文的新C3,遍历C2的值,去获取新C3的中间值,从而伪造C3位置的明文
Shiro GCM漏洞(高版本)
版本:Shiro >=1.4.2
原理:GCM不存在填充机制,所以只能用全版本通用方式——爆破密钥,详情见下一章。
爆破密钥漏洞(全版本)
版本:全版本
条件:密钥在爆破字典中
原理:在讨论算法中其实很清楚知道,无论AES-CBC还是AES-GCM的加解密算法都是公开已知的。而解密时需要的两个变量IV和Key,解密时服务器是使用Cookie中提取的IV,所以也就只存在一个变量Key,我们可以利用:
1、密钥错误,服务器解密失败,返回deleteMe
2、密钥正确,服务器解密成功,但明文不是登陆信息,静默处理,返回null
解密处理流程:- KS₁ = AES_Encrypt(Key, CTR₀)
- KS₂ = AES_Encrypt(Key, CTR₀ + 1)
- KS₃ = AES_Encrypt(Key, CTR₀ + 2)
复制代码 所以关键就是我们根据对应版本加密算法,遍历密钥伪造密文,利用服务器是否返回Set-Cookie: rememberMe=deleteMe 字段,从而爆破出密钥。
这也是为什么工具中会出现对应的GCM选项,因为加密方式完全不同
比如以1.2.4版本漏洞来看,虽然是默认密钥,但本质也能走爆破流程。而1.2.4属于AES-CBC加密,就不能勾选AES-GCM。如果勾选了就爆破不出来。
总结
[table][tr]漏洞影响版本漏洞成因利用[/tr][tr][td]Shiro550[/td][td]Shiro |