漏洞原理:Apache Shiro框架提供了记住密码的功能(RememberMe),用户登录成功后会将用户的登录信息加密编码,然后存储在Cookie中。对于服务端,如果检测到用户的Cookie,首先会读取rememberMe的Cookie值,然后进行base64解码,然后进行AES解密再反序列化。
1. 加密流程分析
当我们勾选记住密码的选项之后,登录时断点打到DefaultSecurityManager.rememberMeSuccessfulLogin
方法下

获取一个RememberMeManager
对象之后进入onSuccessfulLogin
方法

调用forgetIdentity()
方法对subject
进行处理,subject
对象表示单个用户的状态和安全操作,包含认证、授权等,跟进

在forgetIdentity()
方法中,对subject进行了处理

我们继续跟进forgetIdentity
,getCookie()
方法获取请求的cookie,接着会进入到removeFrom()
方法

跟进removeFrom()
方法,removeForm主要在response头部添加字段Set-Cookie: rememberMe=deleteMe


然后我们再次回到onSuccessfulLogin
方法中,如果设置了rememberMe
则进入rememberIdentity
方法

我们跟进看看后续怎么处理的,rememberIdentity
方法中主要是两部分,一部分是convertPrincipalsToBytes
方法用来清除之前的认证信息,并根据用户名生成新的Principal

我们跟进convertPrincipalsToBytes
方法查看,发现主要干了两件事情
- 将我们的
principals(用户名)
序列化 - 对序列化之后的结果进行加密

我们继续跟进我们的加密算法encrypt
,看看如何加密的

这里可以通过getCipherService
获取的加密对象,看到是一个很明显的AES加密

那么我们跟进getEncryptionCipherKey()
函数,发现加密密钥返回的是encryptionCipherKey
值

我们看看它是在哪里设置的,发现setEncryptionCipherKey
函数是赋值的

在setCipherKey()
函数中对其进行赋值,而且赋值的是一个常量DEFAULT_CIPHER_KEY_BYTES


是一个写死的base64字符串
1 | private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA=="); |

让我们返回到刚刚的流程,convertPrincipalsToBytes
之后进入我们的rememberSerializedIdentity
部分

这里就是很简单的将结果base64加密,然后设置到cookie里面

至此,加密部分我们基本了解完毕:
1 | 序列化principals(用户名) -> 对结果进行AES加密 -> 对结果进行base64加密 -> 设置到cookie中 |
大致流程

2. 解密流程分析
我们登录之后利用burp重新发包,看看cookie是如何解密的,发包之后发现并没有断住,是因为除了cookie之外,它还通过JSESSIONID=C434C16E1B719C7DFC51E116A05638E3
来鉴权,当存在JSESSIONID
的时候不会触发反序列化

我们删除JSESSIONID=C434C16E1B719C7DFC51E116A05638E3
再试一次
成功断在AbstractRememberMeManager.getRememberedPrincipals
处

我们进入getRememberedSerializedIdentity
方法,看名字应该是处理反序列化的,它从我们的请求中读取base64加密之后的cookie

判断cookie是否被删除、确保base64字符串被正确的填充,最后将base64解码为字节数组

将字节数组返回到我们的getRememberedPrincipals
方法中,然后调用convertBytesToPrincipals
方法来从字节数组中获取我们的权限(Principals)

调用decrypt
方法来AES解码,和上面加密步骤基本相同,密钥也是默认密钥

唯一值得注意的是我们的解密向量iv是根据传入的数据一起的

返回deserialize(bytes)
的值,我们看看deserialize()
方法,继续跟进发现调用的是默认的反序列化方法

一路走到readObject
方法,触发反序列化
整体过程如下:

3. 利用
我们可以利用URLDNS链进行探测
1 | //URLDNS.java |
编译运行
1 | javac URLDNS.java |
在目录下生成了ser.bin的二进制文件,我们利用python脚本对其进行加密
1 | from Crypto.Cipher import AES |

将序列化数据填入到cookie中,然后删除JSESSIONID
,发送数据包

burp成功收到请求

参考文档: