写在前面
这篇文章是我刚刚开始工作时写的学习笔记,当时对于一些概念还是一知半解,不过现在看来这篇写的还是没有什么问题的,所以呢发在这里做个备份好了。
说一说这个漏洞吧,Apache Shiro反序列化漏洞是近几年兴起的反序列化漏洞系列之一,反序列化漏洞首次出现是在2015年,将此类漏洞带入公众视线内是FoxGlove Security安全团队的一篇文章,但在15年年初Gabriel Lawrence和Chris Frohoff在AppSecCali就出具过相关的报告,而此份报告并没有被重视,因此反序列化漏洞也成了2015年年度最被低估的漏洞。
在近几年中,不论红队、公开武器库、还是国际黑客都积累大量的Payload和POC脚本,由于该漏洞的影响,OWASP对此类漏洞都进行了重新的定级和分化。
在OWASP Top 10:2021中此类漏洞被定义在A08:2021-Software and Data Integrity Failures,译为软件和数据完整性故障,包含A8:2017-Insecure Deserialization,2021年的A08是一个新的类别,重新对反序列化漏洞进行细致的分类和定级,也更能认识到此类漏洞的影响范围之广。
反序列化漏洞
序列化就是把对象转换成字节流,便于保存在内存、文件、数据库中;反序列化即逆过程,由字节流还原成对象。Java中的ObjectOutputStream
类的writeObject()
方法可以实现序列化,类ObjectInputStream
类的readObject()
方法用于反序列化。比如你可以将字符串对象先进行序列化,存储到本地文件,然后再通过反序列化进行恢复。
序列化和反序列化本身并不存在问题,真正问题在于,如果Java应用可以对任意数据做了反序化处理,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,非预期的对象在产生过程中就有可能带来任意代码执行。所以这个问题的根源在于类ObjectInputStream
在反序列化时,没有对生成的对象的类型做限制;假若反序列化可以设置Java类型的白名单,那么问题的影响就小了很多。
反序列化问题由来已久,且并非Java语言特有,在其他语言例如PHP和Python中也有相似的问题。报告中所指出的并不是反序列化这个问题,而是一些公用库,例如Apache Commons Collections
中实现的一些类可以被反序列化用来实现任意代码执行。WebLogic、WebSphere、JBoss、Jenkins、OpenNMS这些应用的反序列化漏洞能够得以利用,就是依靠了Apache Commons Collections
。这种库的存在极大地提升了反序列化问题的严重程度,可以比作在开启了ASLR地址随机化防御的系统中,出现了一个加载地址固定的共享库。
Shiro反序列化漏洞原理
Shiro提供了记住我(RememberMe)的功能,利用Cookie达成了即使关闭浏览器下次再打开时还是能记住登录状态的功能,二次访问时无需再次登录。
Shiro对rememberMe的cookie做了加密处理,shiro在CookieRememberMeManaer
类中将cookie中rememberMe字段内容分别进行 序列化、AES加密、Base64编码操作。
在识别身份的时候,需要对Cookie里的rememberMe字段解密。根据加密的顺序,不难知道解密的顺序为:
- 获取rememberMe cookie
- base64 decode
- 解密AES
- 反序列化
但是,AES加密的密钥Key被硬编码在代码里,意味着每个人通过源代码都能拿到AES加密的密钥。因此,攻击者构造一个恶意的对象,并且对其序列化,AES加密,base64编码后,作为cookie的rememberMe字段发送。Shiro将rememberMe进行解密并且反序列化,最终造成反序列化漏洞。
Shiro反序列漏洞指纹
登录包发送后对应的响应包中带有Set-Cookie: rememberMe=deleteMe的字段。
有的时候会出现长度为512的cookie(登录框界面勾选remember me选项并登录成功时会出现)。
当然,初次之外Shiro组件的前端也是有一定的特点,这也可以当做是漏洞指纹的一部分。
加密过程
登录http://localhost:8080/login.jsp,勾选rememberMe,登录成功后,看到一个key为rememberMe,value长度为512的cookie
官网中,我们知道处理Cookie的类是CookieRememberMeManaer
,该类继承AbstractRememberMeManager
类,跟进AbstractRememberMeManager
类,很容易看到AES的key
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
假设我们以root的用户名的登录了。如果登录成功,Shiro先将登录的用户名root字符串进行序列化,使用DefaultSerializer
类的serialize
方法
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) { byte[] bytes = serialize(principals); // 进行序列化
if (getCipherService() != null) {
bytes = encrypt(bytes); // AES加密
}
return bytes;
}
接着进行AES加密。动态跟踪到AbstractRememberMeManager
类的encrypt方法中,可以看到AES的模式为AES/CBC/PKCS5Padding,并且AES的key为Base64.decode("kPH+bIxk5D2deZiIxcaaaA=="),转换为16进制后是\x90\xf1\xfe\x6c\x8c\x64\xe4\x3d\x9d\x79\x98\x88\xc5\xc6\x9a\x68,key为16字节,128位
这边的AES的key在上面的代码中已经展示出来了
protected byte[] encrypt(byte[] serialized) {
byte[] value = serialized;
CipherService cipherService = getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.encrypt(serialized,getEncryptionCipherKey());
value = byteSource.getBytes();
}
return value;
}
进行AES加密,利用arraycopy()
方法将随机的16字节IV放到序列化后的数据前面,完成后再进行AES加密
至于这里是怎么看出AES的模式,因为没有源码,暂时没办法解释,如果有源码可以注意形如下面的代码,也算是比较明显的
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); //"算法/模式/补码方式"
最后在CookieRememberMeManage
r类的rememberSerializedIdentity()
方法中进行base64加密:
String base64 = Base64.encodeToString(serialized);
解密过程
有了AES的key、加密模式AES/CBC/PKCS5Padding,由于AES是对称加密,所以我们已经可以解密AES的密文了
第一步:获取rememberMe的Cookie
第二步:base64解码。CookieRememberMeManager
类的getRememberedSerializedIdentity()
方法
byte[] decoded = Base64.decode(base64);
第三步:AES解密。base64解码后的字节,减去前面16个字节
AbstractRememberMeManager
类的decrypt()
方法
protected byte[] decrypt(byte[] encrypted) {
byte[] serialized = encrypted;
CipherService cipherService = getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.decrypt(encrypted,getDecryptionCipherKey());
serialized = byteSource.getBytes();
}
return serialized;
}
第四步:反序列化。DefaultSerializer
类的deserialize()
方法
public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
}
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);
try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
@SuppressWarnings({"unchecked"})
T deserialized = (T) ois.readObject();
ois.close();
return deserialized;
} catch (Exception e) {
String msg = "Unable to deserialze argument byte array.";
throw new SerializationException(msg, e);
}
}
可以看到,解密和加密完全是对称的。第四步中的readObject()
方法,由于反序列化的对象完全由外部rememberMe Cookie控制。所以,一旦添加了有漏洞的common-collections
包,就会造成任意命令执行
利用方式
这部分之前学习的时候就没有写出来,当时是想着如果可以想看一看手工是怎么去利用这个漏洞,但由于技术比较菜就没能理解,现在这部分呢还是不要写出来了,如果想看的话搜一搜其他人的文章吧。
修复建议(此模块写于感想总结之后)
无论是否升级shiro到1.2.5及以上,如果shiro的rememberMe功能的AES密钥一旦泄露,就会导致反序列化漏洞(与个人的想法基本相同)
如果有条件可以跟shiro 1.3.2的代码,看到官方的操作如下:
- 删除代码里的默认密钥
- 默认配置里注释了默认密钥
- 如果不配置密钥,每次会重新随机一个密钥
可以看到并没有对反序列化做安全限制,只是在逻辑上对该漏洞进行了处理,如果在配置里自己单独配置AES的密钥,并且密钥一旦泄露,那么漏洞依然存在,所以漏洞修复的话,我建议下面的方案同时进行:
- 升级shiro到1.2.5及以上
- 如果在配置里配置了密钥,那么请一定不要使用网上的密钥,一定不要!!请自己base64一个AES的密钥,或者利用官方提供的方法生成密钥:org.apache.shiro.crypto.AbstractSymmetricCipherService#generateNewKey()
感想总结
Shiro漏洞的利用主要是怎么去构造这个反序列化的包去让服务端进行利用,其中有一个点就是AES加密,如果没有对方的AES加密的密钥、模式,这个数据包基本上是没有可能打进去的,这个想法是出于对加解密过程的个人理解,相应的个人认为,使用自主开发的AES加密密钥、算法、模式也算是对于Shiro漏洞的一种修复。
相关的资料链接
vulhub镜像:Apache Shiro 1.2.4反序列化漏洞(CVE-2016-4437)
官方资料:Shiro/SHIRO-550:Randomize default remember me cipher
漏洞解析:Apache Shiro Java反序列化漏洞分析
GitHub工具(集成化): ShiroExploit-Deprecated
GitHub工具(可用于手工复现): SHIRO-550
手工复现可参考:[Apache Shiro 1.2.4反序列化漏洞(CVE-2016-4437)复现
反序列化漏洞事件:【事件分析】NO.10 影响深远的反序列化漏洞
Comments | NOTHING