序言
浮云游子意,落日故人情。
填Shiro的坑。
Apache Shiro 简介
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。Shiro的优势在于轻量级,使用简单、上手更快、学习成本低。
Shiro-550
特征:返回包中包含rememberMe=deleteMe字段。
影响版本:shiro<1.2.24
环境搭建
1 | git clone https://github.com/apache/shiro.git |
打开samples/web文件,这个是带jsp界面的,待会儿审计的类存放在pom依赖中,maven会帮助我们去找的
修改一下pom.xml:
1 | <dependency> |
有时候IDEA会自动识别facets,没有的话自己去找web.xml手动配一下,
首先为项目生成artifacts,最后不要忘记配置artifact:
成功:
调试
官方提示在CookieRememberMeManager
类中
可以看出一对对称函数(shiro-core的jar包中):
加密
把断点下在org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin
,debug模式启动tomcat容器,root-secret
,勾选Remember Me
选项,来到:
来到这里,我们跟进entrypt
方法:
cipherService具体如下:
getEncryptionCipherKey
具体是干嘛的呢:
返回硬编码的key:kPH+bIxk5D2deZiIxcaaaA==
继续跟,直到结束前面所有的操作;
进入rememberSerializedIdentity
函数:
总结流程:
- 用户名序列化
- AES-CBC加密,key已知为
kPH+bIxk5D2deZiIxcaaaA==
- Base64编码
- 将上述设置到cookie中的rememberme字段
解密
断点打到org.apache.shiro.mgt.DefaultSecurityManager#getRememberedIdentity
:
两个重点:
1- 跟进getRememberedPrincipals
函数:
base64为rememberMe字段的value,需要先进行base64解码,返回解密内容
2- 跟进convertBytesToPrincipals
函数,参数bytes存放的是base64解密内容
可以看到先AES解密,再反序列化
解密过程总结:
- 读取cookie中的rememberMe字段的值
- Base64解码
- AES解密 Key已知
- 反序列化得到用户名
调试小记
火狐+burpsuite
- 启动tomcat
第一次登陆注册,钩上rememberMe,之后正常登陆,抓包留存:
- 在解密这里下断点,在登录状态下,删除JSESSIONID字段,发送第一步保存的数据包,
- 触发断点,可以继续跟解密步骤了
漏洞利用
这里结合URLDNS打一下:
1 | target = "http://127.0.0.1:8081/samples_web_war_exploded/" |
Shiro-721
用户可通过Padding Oracle 加密生成的攻击代码来构造恶意的rememberMe字段
影响版本:Apache Shiro < 1.4.2版本
背景
AES加密模式
AES是一种对称加密的分组加密算法。
分组长度固定为128bit
密钥长度可变,128bit、192bit、256bit
AES应密钥长度可以称为AES-128,AES-192,AES-256
AES加密标准 | 密钥长度 | 分组长度 | 加密轮数 |
---|---|---|---|
AES-128 | 128 bits ( 4 Bytes × 32 bits/Bytes ) | 128 bits | 10 |
AES-192 | 192 bits ( 6 Bytes × 32 bits/Bytes ) | 128 bits | 12 |
AES-256 | 256 bits ( 8 Bytes × 32 bits/Bytes ) | 128 bits | 14 |
分组模式
分组加密有 5 种可选方式:
ECS
( Electronic Codebook Book , 电话本模式 )CBC
( Cipher Block Chaining , 密码分组链接模式 )CTR
( Counter , 计算器模式 )CFB
( Cipher FeedBack , 密码反馈模式 )OFB
( Output FeedBack , 输出反馈模式 )
CBC 模式
CBC模式作为分组加密的一种,加解密方法如图:
加密过程中,
IV(Initialization Vector)表示起始向量,通常是随机生成的,长度同分组大小相同,同明文一起传输。
padding:表示填充位,凑成长度为分组的整数倍
第一个明文块首先会和IV进行异或操作,再执行AES加密,得到第一密文块。
之后,每个明文块会与前一个密文块进行异或,再进行加密。
最终的密文长度就是图中所有ciphertext块的拼接长度
解密过程中,
首先密文按照分组长度进行分组,
第一个密文块直接进行AES解密,得到middletext,该结果再与IV进行异或,得到第一明文块
之后每一个AES的解密出来的middletext都会和前一个密文块进行异或,得到对应的明文块。
明文块再拼接到一起就是解密出来的明文
Padding 填充模式
Padding是在加密过程中,在最后一个分组的结尾进行填充,用于补齐。
AES-CBC模式下的三种Padding方式:
- NoPadding : 明文长度必须是 16 Bytes 的倍数。
- PKCS5Padding : 以完整字节填充 , 每个填充字节的值是用于填充的字节数 。即要填充 N 个字节 , 每个字节都为 N。
- ISO10126Padding : 以随机字节填充 , 最后一个字节为填充字节的个数 。
Shiro用的是PKCS5Padding:
正确的padding byte值只可能为:
- 1个字节的padding为0x01
- 2个字节的padding为0x02,0x02
- 3个字节的padding为0x03,0x03,0x03
- 4个字节的padding为0x04,0x04,0x04,0x04
- …
值得一提的是,当待加密的数据长度刚好满足分组长度的倍数时,仍然需要填充一个分组长度。
Padding Oracle Attack
Padding Oracle Attack 是一种针对CBC模式分组加密算法的攻击,知道padding model,解密获得明文。
要成功进行Padding Oracle Attack是需要服务端返回两个不同的响应特征来进行Bool判断的。
在Apache Shiro的场景中,这个服务端的两个不同的响应特征为:
- Padding Oracle错误时,服务端响应报文的Set-Cookie头字段返回
rememberMe=deleteMe
; - Padding Oracle正确时,服务端返回正常的响应报文内容;
解密:破解明文
大前提:
1. 服务器会对解密结果进行padding校验,给出响应,再判断解密结果的正确性。
2. 攻击者知道全部密文以及初始向量IV
当解密时,最后一组的middle是固定的,倒数第二组的密文和最后一组的middle异或的结果一定是满足padding模式的。
合格的明文padding只有8种case,这是可以穷举的。
这时我们需要一个二元组,第一组tmp_IV全是0,第二个为当前组的密文C[n]。
开始爆破:
尝试让这组plaintext的最后一个字节为0x01,tmp_IV的最后一个字节在0x00~0xff 范围内开始穷举,去与middle整体异或,直到异或结果的最后一位为0x01,假设此时tmp_IV最后一个字节的结果为0x42。
我们可以通过它得到实际middle的最后一个字节的内容为0x42 XOR 0x01 = 0x43。
之后让plaintext的最后两个字节为0x0202,最后三个字节为0x030303,最后四个字节为0x040404…,最后8个字节为0x0808080808080808。
此时就获得了最后一组密文的middle。
把最后一组密文的middle与倒数第二组密文进行XOR,便可得到最后一组的明文。
舍弃掉最后一组密文,向服务器提交第一组~倒数第二组密文,迭代1、2操作,获得倒数第二组明文。依次规律,直到获得所有分组的明文
将明文拼接到一起就是解密结果
因此,通过Padding Oracle Attack可以在不知道key的情况下,获取全部明文的值。
加密:篡改明文
在我们获得每一组的middle之后,攻击者可以通过控制IV,使密文解密为任意明文。
参考f1sh
1 | 原明文^原IV = middle |
简单来说,如果我们可以控制IV,将IV = 原明文^原IV^新明文
,将新的IV提交,服务器解密就可以得到我们想要的明文。
做法:
原明文的AES加密结果为C[0]-C[N],我们想要的明文为P[0]-P[N]
我们只需要C[N]即可,只要C[N]不变,M[N]就不变
1 | 全0+C[N]爆破,得到M[N] |
CBC翻转攻击
一句话,通过修改密文进而篡改明文。
第N组明文=第N组密文的解密结果 XOR 第N-1组密文
公式表达
1 | Plaintext[0] = Decrypt(Cipher[0]) XOR IV (N=0) |
N>=1下我们假设:
M = Decrypt(Cipher[N])
C = Cipher[N-1]
P = Plaintext[N]
根据异或特性有,两个数相同为0,不同为1:
1 | M XOR C = P |
举个例子:
cipher = “0123456789,helloworld,java”
将通过CBC攻击密文篡改明文为”0123456789,helloworld,javA”
1 | from Crypto.Cipher import AES |
调试
和550一样,debug启动起来,把断点下到org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals的convertBytesToPrincipals处,持续跟进:
接下来进入
做了一系列处理
进入doFinal函数
调用链:
1 | javax.crypto.Cipher#doFinal(byte[]) |
可以看到了来到了Shiro选择的PKCS5Padding类unpad函数:
这里如果unpad报错,会爆异常new CryptoException(msg, e);
回到上层可以看到,由onRememberedPrincipalFailure
函数来处理
只要padding错误,服务端就会返回一个cookie: rememberMe=deleteMe;
漏洞利用
前提1: 攻击者拥有正常用户的rememberMe字段
shiro的rememberMe中存放的是:前16字节为IV,后面为AES加密之后的密文
1 | import base64 |
熟悉的aced0005~
注意,最后一行都是0x10,说明已经是填充过后的了
前提2:Java反序列化格式
Java反序列化是按照字节流的格式进行读取,反序列化数据末尾的脏数据不会报错。
这是因为:readobject读取的serialdata长度是在serialdata头部就约定好的。
payload构造原理
原理是攻击者将正常用户的rememberMe拿到,使用不断的Padding Oracle Attack对我们的恶意反序列化payload编写密文,进行加密。
经过调试,我们可以知道:
当用户的cookie认证失败,服务端就会返回一个cookie: rememberMe=deleteMe;
只要padding错误,服务端就会返回一个cookie: rememberMe=deleteMe;
所以我们只要在AES密文(rememberMe经过Base64解密之后的值)后面添加脏数据,解密之后符合padding model就可以绕过了。
复习一遍Shiro加解密流程:
加密:token -(序列化)-> token_serilized(aced0005…) -(AES_Enc)-> cipher -(Base64)-> rememberMe
解密:rememberMe -(Base64)->cipher-(AES_Dec)->token_serilized(aced0005…)-(反序列化)->token
审计了一下longofo师傅的项目,终于搞懂了最终的payload是怎么生成的。
假设现在yso.bin是我们的恶意payload,它作为明文首先要被分组,最后一列填充,倒序排列。
为什么倒序排列?因为遍历明文时候,第一组其实就是明文的最后一组,构造我们的恶意密文的方向其实是从后向前的。
首先生成一组脏数据X,这组脏数据会放在最终payload的最后一组,规范一点,X被0-127任意值填充。
我们需要的是:
- 正常的rememberMe
- 二元组:0x00*16+X
接下来使用padding oracle attack去加密这些分组,怎么做?爆破!
rememberMe+0x00*16+X发送给server,padding oracle attack 获得X对应的tempIV。
我们希望X的解密结果是我们Payload的最后一组,所以我们有:
P[n] XOR tempIV = C[n-1]
也就是说,我们知道X这组的middle是tempIV,知道明文P[n],我们就可以通异或,定制化倒数第二组密文。
之后继续这样做,构建二元组,rememberMe+0x00*16+C[n-1],发送给服务器,获得倒数第二组的tempIV,再与我们的倒数第二组明文进行异或,得到倒数第三段密文。
这样不断向前推导,可以获得IV+C,对它进行Base64加密,这串就是新的rememberMe。
当Shiro解密时,AES解密结果decoded就是我们的恶意payload(肯定符合padding model 是因为我们的payload前期已经处理过了,已经做好了填充),直接参与反序列化,触发漏洞。
Shiro对AES解密的结果只校验了是否符合padding model,符合就无脑去反序列化,这也是漏洞成因的关键所在。
我们payload最后其实是有一串脏数据的(那一串被随机数填满的),但是这并不影响反序列化,因为反序列化读取是有自己的规范的,只读serialdata指定长度,反完走人,后面多出来的不管。
结尾
老洞新看,发现很多师傅的文章中,没有讲最终的payload是如何生成的,自己尝试玩了几天,还是很有收获的。
感谢Zebork师兄与我一起头脑风暴,Padding Oracle Attack那块确实很有意思^ _ ^
参考:
Going the other way with padding oracles: Encrypting arbitrary data!
工具:PaddingOracleAttack-Shiro-721
Shiro721公告
利用:Shiro反序列化漏洞
Padding Oracle Attack 浅析
Mi1k7ea:浅析CBC字节翻转攻击与Padding Oracle Attack
Mi1k7ea:浅析Shiro Padding Oracle Attack(Shiro721)
从更深层面看Shiro Padding Oracle漏洞
ShiroExploit检测工具
Shiro组件漏洞与攻击链分析