概述
在Shiro550漏洞中,Cookie所使用的AES加密密钥为硬编码,所以我们可以构造恶意序列化数据并使用固定的AES密钥进行正确加密恶意序列发送给服务端,进而达到攻击的目的。但在该漏洞公布后,Shiro官方修复了这一漏洞,将AES密钥修改成了动态生成。也就是说,对于每一个Cookie,都是使用不同的密钥进行加解密的。
Shiro反序列化漏洞——Shiro721(CVE-2019-12422)
漏洞原理
在Shiro721漏洞中,由于Apache Shiro cookie中通过 AES-128-CBC 模式加密的rememberMe字段存在问题,用户可通过Padding Oracle Attack来构造恶意的rememberMe字段,并重新请求网站,进行反序列化攻击,最终导致任意代码执行。
虽然使用Padding Oracle Attack可以绕过密钥直接构造攻击密文,但是在进行攻击之前我们需要获取一个合法用户的Cookie。
漏洞流程
- 登录网站获取合法Cookie
- 使用rememberMe字段进行Padding Oracle Attack,获取intermediary
- 利用intermediary构造出恶意的反序列化密文作为Cookie
- 使用新的Cookie请求网站执行攻击
影响版本
Apache Shiro <= 1.4.1
特征判断
响应包中包含字段remember=deleteMe字段
漏洞环境搭建
搭建步骤与Shiro550类似,可以参考Java反序列化漏洞——Shiro550。
配置Maven的时候可以选择默认下载源码和文档,在Settings-Build Tools-Maven-Importing中
漏洞分析
密钥生成
在Shiro550中,密钥是硬编码,就像下面这样
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}
而在Shiro721中,密钥的生成方式变为了动态生成
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
AesCipherService cipherService = new AesCipherService();
this.cipherService = cipherService;
setCipherKey(cipherService.generateNewKey().getEncoded());
}
我们可以跟进调试一下,在 AbstractRememberMeManager()
方法中,通过generateNewKey()
获取密钥,跟进
初始化了一个KeyGenerator
对象并调用init()
方法初始化其参数,跟进看看参数是怎么被赋值的
这里调用了双参数init()
,并且获取了一个随机数发生器SecureRandom
。
下一步调用了kg.generateKey()
,跟到engineGenerateKey()
可见这里已经生成了一串16字节的随机序列,然后返回一个SecretKeySpec
对象,再使用getEncoded()
方法获取key
密钥序列。
至此就是Shiro721完整的密钥生成过程。
布尔条件
我们知道,Padding Oracle Attack攻击是一种类似于sql盲注的攻击,这就要求服务器端有能够被我们利用的布尔条件。在CBC字节翻转攻击&Padding Oracle Attack原理解析这篇文章中,我们模拟的服务器环境如下
- 当收到一个有效的密文(一个被正确填充并包含有效数据的密文)时,应用程序正常响应(200 OK)
- 当收到无效的密文时(解密时填充错误的密文),应用程序会抛出加密异常(500 内部服务器错误)
- 当收到一个有效密文(解密时正确填充的密文)但解密为无效值时,应用程序会显示自定义错误消息 (200 OK)
我们可以通过响应头来判断明文填充是否正确,进而爆破出中间值。那么对于解密不正确的Cookie,Shiro是怎么处理的呢?
Padding错误处理
解密函数在AbstractRememberMeManager.decrypt()
中
跟进cipherService.decrypt()
,最后到crypt()
中调用doFinal()
方法
这里的doFinal()
方法对密文进行异常处理
doFinal()
方法有IllegalBlockSizeException
和BadPaddingException
这两个异常,分别用于捕获块大小异常和填充错误异常。异常会被抛出到crypt()
方法中,最终被getRememberedPrincipals()
方法捕获,并执行onRememberedPrincipalFailure()
方法。
onRememberedPrincipalFailure()
方法调用了forgetIdentity()
。在Shiro550中我们分析过,该方法会调用removeFrom()
,在response头部添加字段Set-Cookie: rememberMe=deleteMe
。
倘若Padding结果不正确的话,响应包就会返回 Set-Cookie: rememberMe=deleteMe
。
Padding正确,反序列化错误处理
CBC模式下的分组密码,如果某一组的密文被破坏,那么在其之后的分组都会受到影响。这时候我们的密文就无法正确的被反序列化了。
Shiro中关于反序列化的处理在DefaultSerializer
类中
如果反序列化的结果错误,则会抛出异常。最后异常仍会被getRememberedPrincipals()
方法处理。
但是对于Java来说,反序列化是以Stream的方式按顺序进行的,向其后添加或更改一些字符串并不会影响正常反序列化。我们可以来测试一下。
我们获取正常用户的Cookie并使用密钥解密,可以看到最后填充的数据为0x0B
下面我们将其更改为其他合法填充方式,然后加密发送出去
服务器端正常响应,于是这里就构造出了布尔条件
- Padding正确,服务器正常响应
- Padding错误,服务器返回
Set-Cookie: rememberMe=deleteMe
漏洞利用
在Shiro550中,我们可以直接通过硬编码密钥直接生成攻击密文。但是Shiro721使用了动态密钥,无法直接获取密钥。但是仍然可以通过Padding Oracle Attack绕过密钥,直接生成攻击密文。
利用链和Shiro550类似,这里我们使用ShiroExploit.V2.51工具进行攻击测试。输入测试网址以及登录用户的Cookie
选择dnslog进行漏洞检测,此时已经开始爆破每一分组的intermediary
测试命令执行
使用生成的Cookie成功执行。