shiro反序列化漏洞分析

spider   ·   发表于 2023-04-08 22:09:13   ·   代码审计

前言

一手shiro反序列化漏洞分析笔记,分析给掌控的各位同学学习啦

搭建环境
java版本:jdk1.8.0_65
Shiro版本:1.2.4 https://github.com/apache/shiro
服务器中间件:tomcat 8.5
下载shiro
https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4
下载好打开


配置一下maven
https://blog.csdn.net/qq_42057154/article/details/106114515
修改pom.xml配置
加一个version,默认没有的


配置tomcat
下载tomcat
https://archive.apache.org/dist/tomcat/tomcat-7/v7.0.10/bin/apache-tomcat-7.0.10.zip


一切尽在不言中:

部署好以后是class格式文件,这时候不利于代码分析(主要是class格式的代码用不了find usage)

完成,访问
http://localhost:8080/shiro/


当rememberMe=on时,便会返回一串cookie


未登陆的情况下,请求包的cookie中没有rememberMe字段,返回包set-Cookie里也没有deleteMe字段
登陆失败的话,不管勾选RememberMe字段没有,返回包都会有rememberMe=deleteMe字段
不勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段。但是之后的所有请求中Cookie都不会有rememberMe字段
勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段,还会有rememberMe字段,之后的所有请求中Cookie都会有rememberMe字段
我们知道rememberMe是shiro的特征,shiro是apache用来做认证的。而这部分核心的代码便是CookieRememberMeManager类。
文章主要从加密解密过程进行分析
加密过程
CookieRememberMeManager继承了AbstractRememberMeManager类,后者有一个onSuccessfulLogin方法判断是否成功登录,我们将断点设在此处:


进入forgetIdentity(subject),这一步主要是处理requests和response


跟进forgetIdentity(requests,reponse),走进removeFrom(request, response)获取了配置信息等:


继续跟进,进入if,这边是判断是否RememberMe,显然是true,这个值是前面获取到的,进入循环体


进入rememberIdentity(subject, token ,info),继续跟进,进入convertPrincipalsToBytes(accountPrincipals)


convertPrincipalsToBytes()方法根据字面意思可知,我们是要将传入的值accountPrincipals进行操作以后转为byte[]格式,跟进该函数:


首先这个类很简单,容易读懂,有两个函数需要我们关注一下,serialize(principals),还有个是encrypt(bytes)。principals这里的值是root,即用户名,意味对用户名进行序列化然后进行加密。
先跟一下serialize(principals):
简单易懂,就是对我们的用户名root使用ObjectOutputStream进行序列化操作,并返回toByteArray,byte数组格式:


一路step over,重新回到convertPrincipalsToBytes


调用encrypt(bytes),我们进去看一看:
首先看到的是CipherService cipherService = getCipherService()


继续进入if循环语句:
cipherService.encrypt(serialized, getEncryptionCipherKey())
getEncryptionCipherKey看名字像是在获取加密key
先跟进getEncryptionCipherKey:


这边跳转到了getEncryptionCipherKey,下面有个set方法,这让我们想到了java bean这一部分,所以看一下哪个调用了setCipherService:


查看setCipherKey被调用的地方,发现存在于AbstractRememberMeManager构造器中


查看一下这个值:PH+bIxk5D2deZiIxcaaaA==
那么我们 需要找的值CipherKey便是这个


回到encrypt(serialized, getEncryptionCipherKey()),上面分析完getEncryptionCipherKey()值,下面看一下encrypt()方法,进入该方法


进入encrypt方法:


这边是生成ivBytes值,这边就不用管如何生成的了,是一个随机16字节的数组


继续,调用到到如下:
encrypt(plaintext, key, ivBytes, generate)
可知这边返回了该值,这个加密过程使用到了四个参数,进去看看

继续step over,回到
ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());


这里的byteSource就是上面生成的加密数据(16位向量iv值与AES加密后的值拼接)至此,序列化加密到此结束
之后一直next,知道进入rememberIdentity:


进入rememberSerializedIdentity(subject, bytes),将经过加密后的信息进行Base64编码然后赋给cookie


难么,整个加密流程结束,身份信息经过了序列化——>加密——>base64编码

解密过程

来到CookieRememberMeManager.java,getRememberedSerializedIdentity是用来提取cookie并进行base64解码的,查看调用关系:


getRememberedSerializedIdentity其实是用来base64解码的,convertBytesToPrincipals是用来AES解密的


我们这边使用客户端向服务器发包,发一个shiro exp让后端处理:


在getRememberedSerializedIdentity处设置断点来进行分析:


进入getRememberedSerializedIdentity,跟到readValue


获取到name值,也就是我们发送的数据包中的rememberMe的值并返回


将base64编码后的rememberMe值进行base64解码并返回


继续往下跟,回到getRememberedPrincipals(),所以这里的bytes值为base64解码后的值

往下,到达convertBytesToPrincipals(bytes, subjectContext),进入函数,看函数名可知,这是对bytes进行解密。

进入decrypt(bytes),
第一步时获取解密方式

进入函数cipherService.decrypt(encrypted, getDecryptionCipherKey())


先选择getDecryptionCipherKey(),查看其值


是一个get方法,想到javabean,查看下面的函数setDecryptionCipherKey调用关系:


跳转:


继续查看setCipherKey的调用,代码到达了AbstractRememberMeManager构造器


查看这里的DEFAULT_CIPHER_KEY_BYTES值,发现是Base64.decode(“kPH+bIxk5D2deZiIxcaaaA==”)


所以上面的decryptionCipherKey值为Base64.decode(“kPH+bIxk5D2deZiIxcaaaA==”)
然后继续跟代码,
返回到函数cipherService.decrypt(encrypted, getDecryptionCipherKey()),并进入decrypt方法

具体解密原理:


看一号框,这里的ivByteSize值是前面算出来的,为16,
System.arraycopy(ciphertext, 0, iv, 0, ivByteSize)这个操作是将ciphertext(这里传入的值是base64解码后的字节数据)这个值的前16为赋给iv
看二号框,这里的encryptedSize值是ciphertext的长度减去前16位后获取的值赋给encrypted(这其实就是加密过程中的AES加密)
最后一步return decrypt(encrypted, key, iv);
三个参数,encrypted(上面生成的),key(我们传入的,前面获取到的key),iv(也是上面获取大的,ciphertext的前16位)


我们进入decrypt:

实际上上面的函数便是使用java对encrypted进行一系列的AES解密。
最后返回decrypt(encrypted, key, iv)解密后的值


此时,解密部分便分析完毕:


解密后的数据也就是我们在前面加密过程中序列化后的数据,然后进行return serialized,继续跟进
回到了convertBytesToPrincipals


看最后一步,进入deserialize(bytes)


跟进deserialize,这边就很熟悉了,对前面获取的数据进行反序列化并返回。


难么,整个rememberMe解密流程结束,身份信息经过了base64解码——>AES解密——>反序列化
所以,如果我们能构造出恶意类进行序列化,然后经过一些列的加密,然后base64编码之后放入remmenberMe中的话,那么便可以实现反序列化漏洞。
以上便是整个漏洞的原理

PS 判断key

这边需要补充一个点,就是shiro获取key的原理,一般情况下先判断是否时shiro,而后爆破key,那么这边有集中思路。参考(https://mp.weixin.qq.com/s/bzkUgnRhAPIYBE3j7rlWBQ)

结合Dnslog与URLDNS

通过在dnslog域名前加对应key的randomNum,结合对应的dnslog记录,即可获取到应用对应的Shiro key了。


利用时间延迟或报错
可以考虑结合触发Java异常进⾏判断,若系统返回对应的报错系统,或者返回通用的报错提示,说明当前的key和gadget组合是成功的,同理,也可以使用报错的方式进行key的枚举。这种方法的话存在一个比较棘手的点:枚举的次数多,耗时长。因为要结合可用gadget执行相关代码进行判断,那么假设字典的key个数为100个,那么枚举的次数就是gadget与key的笛卡尔积(10个gadget就耀枚举1000次)
结合CookieRememberMeManaer
在登陆成功时,如果启用了RememberMe功能,shiro会在CookieRememberMeManaer类中将cookie中rememberMe字段内容进行序列化、AES加密、Base64编码操作。然后保存在cookie中。在关闭浏览器后,重新访问对应的业务接口,此时就是反过来的操作,解码,解密,然后序列化。最后获取到当前用户的身份信息。
解密后就是对应的反序列化以及生成对应的用户凭证组的信息了。在调用上述方式时,如果抛出异常,则会调用onRememberedPrincipalFailure方法。
结合前面的简单分析,可以知道,当从cookie中获取到rememberMe字段时,通过一系列的解码解密反序列化,成功话的会则得到用户凭证组信息。否则在response中返回Set-Cookie:rememberMe=deleteMe。
也就说,可以尝试构造好一个包含用户凭证组信息的伪造rememberMe的值,然后经过AES加密后进行请求。经过一系列的解码解密操作后,若此时返回包不返回Set-Cookie: rememberMe=deleteMe,说明当前的key是正确的。可以以此作为判断标准,使用不同密钥对这串序列化数据进行加密并发包,即可快速爆破获取到Shiro加密密钥。当key正确时,response的header没有相关关键字,当key不正确时,返回包返回Set-Cookie: rememberMe=deleteMe。

用户名金币积分时间理由
Track-魔方 600.00 0 2023-09-25 18:06:28 深度 300 普适 200 可读 100

打赏我,让我更有动力~

0 条回复   |  直到 2023-4-8 | 323 次浏览
登录后才可发表内容
返回顶部 投诉反馈

© 2016 - 2024 掌控者 All Rights Reserved.