shiro550
利用条件
- shiro<=1.2.4
shiro550这个反序列化漏洞整体逻辑比较简单。详细分析参考H0t-A1r-B4llo0n师傅的文章,写得非常通俗易懂,非常详细。
这里进行简单总结
shiro在用户成功登录并且要求rememberMe的情况下,会将principals(可看作是用户名)记录到cookie中。在记录时,并非明文存储,而是经过了序列化->aes加密->base64编码
然后再存到cookie中。
问题就出在aes加密,因为它的密钥是以静态变量的形式定义在了代码中,是写死的,如果用户不进行修改,那么就可以伪造rememberMe这个cookie。
然后,在服务器接收到请求时,会先检查rememberMe,如果不为空且不等于”deleteMe”,则会尝试base64解码->aes解密->反序列化
。前面我们已经能拿到aes密钥,能伪造rememberMe,那这里就相当于拥有了一个反序列化的入口了,下面就可以尝试各种反序列化poc了。
shiro721
利用条件
- shiro<=1.4.1
721生成密钥的方式
首先看看721和550有什么区别,前面我们知道550的密钥是以硬编码的形式写在了代码中。那么721的做法是怎样的呢?
根据如下的调用栈可以看出,721的密钥采用了随机函数生成,修复了550的漏洞。
engineGenerateKey:115, AESKeyGenerator (com.sun.crypto.provider)
generateKey:546, KeyGenerator (javax.crypto)
generateNewKey:62, AbstractSymmetricCipherService (org.apache.shiro.crypto)
generateNewKey:43, AbstractSymmetricCipherService (org.apache.shiro.crypto)
<init>:99, AbstractRememberMeManager (org.apache.shiro.mgt)
<init>:87, CookieRememberMeManager (org.apache.shiro.web.mgt)
<init>:76, DefaultWebSecurityManager (org.apache.shiro.web.mgt)
具体流程
721相对550来说涉及到的知识点更广,需要对密码学知识有一定的了解理解起来才会比较轻松。由于我们不再能像550那样直接得到密钥,所以要想伪造cookie就需要使用更复杂的攻击手段,其中涉及到了padding oracle和cbc翻转攻击。核心的公式就如下两个,不过理解起来还是比较费劲的。
∵ FuzzIV[8] ^ MediumValue[8] = PlainText[8] = 0x01
∴ MediumValue[8] = FuzzIV[8] ^ 0x01
∵ PlainText(n+1) = CipherText(n) ^ Block_Cipher_Decryption(C(n+1))
∴ CipherText(n) = PlainText(n+1) ^ Block_Cipher_Decryption(C(n+1))
具体的流程参考H0t-A1r-B4llo0n师傅,分为上、下两篇。
这里仅记录一些我认为有问题的点。
上篇
首先是在上中padding oracle的分析中,对于每一个分组的最后一个字节的爆破(即MediumValue[8],假设每组大小为8)。根据下面这个公式,IV是已知的,我们只需要得到正确的FuzzIV[8]即可得到MediumValue[8]
∵ FuzzIV[8] ^ MediumValue[8] = PlainText[8] = 0x01
∴ MediumValue[8] = FuzzIV[8] ^ 0x01
但是实际上并没有这么简单,因为我们无法确保padding正确时,PlainText[8]一定是0x01。就比如如下测试,我们按照从0到255的顺序遍历FuzzIV[8]
def xor(a,b):
a=[int(i,16) for i in a.split(" ")]
b=[int(i,16) for i in b.split(" ")]
s=""
for i in range(len(a)):
s+=hex(a[i]^b[i])+" "
return s
fuzziv="0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x01"
iv="0x39 0x73 0x23 0x32 0x5A 0x3B 0x00 0x04"
MediumValue="0x29 0x34 0x5A 0x6B 0x07 0x00 0x02 0x06"
print "plain: "+xor(iv,MediumValue)
for i in range(0,256):
fuzz = "0x00 0x00 0x00 0x00 0x00 0x00 0x00 %s"%hex(i)
print xor(fuzz,MediumValue)
当FuzzIV[8]=0x04时,计算结果如下。很明显,这是正确的padding。然而FuzzIV[8] ^ MediumValue[8] = 0x02
而不是0x01
FuzzIV = "0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x04"
MediumValue = "0x29 0x34 0x5A 0x6B 0x07 0x00 0x02 0x06"
FuzzIV ^ MediumValue = 0x29 0x34 0x5a 0x6b 0x7 0x0 0x2 0x2
如果此时,我们使用如下公式来计算MediumValue[8],那就出大错了。
MediumValue[8] = FuzzIV[8] ^ 0x01
那么正确的做法应该是怎样的呢?查看python paddingoracle模块作者的做法就明白了,在paddingoracle.py的bust函数中。作者使用了重试的做法来解决这个问题,只要某一个FuzzIV[8]可以使padding正确,就尝试继续往下爆破,如果走不通,再回来重新爆破FuzzIV[8],重新爆破时,是从上一次错误的位置开始的。这里有一个疑问,如果FuzzIV[8]错误了,并且继续爆破的时候一错再错,会不会导致算错误?这个问题暂且放这,不在深究。
下篇
作者此处的计算是有问题的,正确算法如下
pad = block_size - (len(plaintext) % block_size)
pad = 16-(279%16)=16=7=9
参考文章
CVE-2016-4437 Shiro550 ( Apache Shiro RememberMe 1.2.4 反序列化漏洞 ) 分析
CVE-2019-12422 Shiro721 ( Apache Shiro RememberMe Padding Oracle 1.4.1 反序列化漏洞) 分析-上
CVE-2019-12422 Shiro721 ( Apache Shiro RememberMe Padding Oracle 1.4.1 反序列化漏洞) 分析-下