shiro 721 反序列化漏洞复现与原理以及Padding Oracle Attack攻击加解密原理

1. 前置知识

1.1 shiro550利用条件

知道aes加密的key且目标服务器含有可利用的攻击链。

原理

在Shiro <= 1.2.4中,反序列化过程中所用到的AES加密的key是硬编码在源码中,当用户勾选RememberMe并登录成功,Shiro会将用户的cookie值序列化,AES加密,接着base64编码后存储在cookie的rememberMe字段中.

服务端收到登录请求后,会对cookie的rememberMe字段的值进行base64解码,接着进行AES解密,然后反序列化。由于AES加密是对称式加密(key既能加密数据也能解密数据),所以当攻击者知道了AES key后,就能够构造恶意的rememberMe cookie值从而触发反序列化漏洞。

影响版本:1.2.4以下

解决payload过长的方式:

  1. 使用urlclassloader加载远程字节码
  2. 将字节码放在post的body中,恶意类实现加载body中的字节码即可.

1.2 shiro721利用条件

知道已经登陆用户的合法cookie且目标服务器含有可利用的攻击链就可以进行漏洞利用。

原理

shiro721用到的加密方式是AES-CBC,而且其中的ase加密的key基本猜不到了,是系统随机生成的。而cookie解析过程跟cookie的解析过程一样,也就意味着如果能伪造恶意的rememberMe字段的值且目标含有可利用的攻击链的话,还是能够进行RCE的。

通过Padding Oracle Attack攻击可以实现破解AES-CBC加密过程进而实现rememberMe的内容伪造。下面会有单独的篇幅讲Padding Oracle Attack。

影响版本:

1.2.5,
1.2.6,
1.3.0,
1.3.1,
1.3.2,
1.4.0-RC2,
1.4.0,
1.4.1

shiro-721对cookie中rememberMe的值的解析过程

在这里插入图片描述

1.3 基于返回包的shiro特征检测

1. 根据返回包中是否有rememberMe=DeleteMe

在这里插入图片描述

构造恶意的rememberMe数据,看房回包中是不是rememberMe=DeleteMe。

2. 使用序列化的SimplePrincipalCollection类的对象

发送一个经过序列化的SimplePrincipalCollection类的对象来判断。如果key正确,则返回包中不会有rememberMe=DeleteMe

示例代码如下:

public class SimplePrincipalCollectionTest {
    @Test
    public void test01() throws IOException, NotFoundException, CannotCompileException {
        SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
        AesCipherService aes = new AesCipherService();
        byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(simplePrincipalCollection);
        oos.close();
        ByteSource ciphertext = aes.encrypt(barr.toByteArray(), key);
        System.out.println(ciphertext.toString());
    }
}

Java Shiro SimplePrincipalCollection 探测密钥正确性

2. 环境搭建

git clone https://github.com/inspiringz/Shiro-721.git
cd Shiro-721/Docker
docker build -t shiro-721 .
docker run -p 8080:8080 -d shiro-721

3. 漏洞复现

先使用合法账户登陆,记得勾选remember Me,然后使用burp抓包获取cookie:
在这里插入图片描述

获取到cookie:
在这里插入图片描述

将其中的remember Me字段复制下来输入到工具中进行利用:

java -jar ysoserial.jar CommonsBeanutils1 "touch /tmp/123" > payload.class
python shiro_exp.py http://192.168.171.137:8080/login.jsp LIO2vKStP5R4NN+TLY0Bgfrz+3sacQHB1BfrOheCVAHeFAGtRsX9JW24tCvcedluOxZwFPoOSs7/tA0fK+UJ9ylRjLIT87NIN1smV22TVqdQ4vSJXB42IQCTV1mDA2CwlDpoeem6M4qY2SeB4JwIpV+iUwNJoOj+NfWeX3/lLZHkoCnsR5TCm6GrHyhdaDZYK0BAJNXFQ9658sJGAF1fztcfR0pYD9RtX26iLW73+D0pd3x6DhPQB7euA4uhUZ3Ue8RoOK3jTqxHC3U5n0DIMpc1RWlHVzUyHjejFAPXCReV+7ds/dWr+b5XlgP9/7ajmi2+6dqr2apVaIhEMC5SP4X4Y+QZw3wS6w76pD1vT8JSlG6l+h4+tIRuS4/gbUzX8GhmPCtw2MBMS/xZ2FsjvTPexdPLEf+114qo4152aNNcXul4zN3czLlve+otlqd5E/WyhhbBA2+EFk+Pewnsq2g2sS53s57H9BcWhXHkcwf0cIrkOXAn9a9xfkkm1HH9 payload.class

在这里插入图片描述
在这里插入图片描述

此 exp 爆破时间较长,建议使用 ysoserial 生成较短的 payload 验证(eg: ping 、 touch /tmp/success, etc),约 1 个多小时可生成正确的 rememberme cookie,生成成功后将自动停止运行。

在这里插入图片描述

在这里插入图片描述

最终会生成恶意的rememberMe cookie,我们使用这个cookie替换原数据包中的cookie。然后登陆进服务器看,会发现/tmp目录下被创建了一个123文件。

4. Padding Oracle Attack原理

这种攻击方式可以达到破解AES-CBC加密的效果。我们先了解几个基础知识。

4.1 分组密码填充

首先我们知道密码都是字符,一个字符8bit也就是1个byte。

aes加密的时候会将字符进行分组并填充,有可能是每8字节一组,也可能是16字节一组。

但是密码不可能恰好填满每个组,可能第一组填满了,第二组只有一个字符,这时候就需要进行填充。

如果明文密码分组完后刚好不需要填充,那么会额外添加一个组,这个组的数据全部都是0xXY,这个数据是多少取决于aes加密是多少个字节分一组的。具体举例如下:

假设密码为一个1,且加密算法中分组的逻辑是8个字节也就是64bit为一组。因为1只占1个字节,所以第一组中还有7个字节是空的。这时候第一组中的数据为:

1 0x07 0x07 0x07 0x07 0x07 0x07 0x07

如果明文恰好不需要填充比如是8个1,且分组逻辑还是8个字节也就是64bit为一组,那么分完组的明文如下:

第一组:

1 1 1 1 1 1 1 1 

第二组:

0x08 0x08 0x08 0x08 0x08 0x08 0x08 0x08

综上,填充的逻辑是,一定会填充数据,区别是填充一个组还是填充几位,如果分组后的明文最后一组中有N字节没有数据就填充0x0N。

如果分组后刚好合适,那么额外添加一个组,且这个组的明文数据一定全是0xXY,0xXY是多少取决于aes加密的分组逻辑填充了多少个数据

如果解密出的明文的结尾不是符合规律的0xXY,那么系统会判断解密失败。

4.2 AES-CBC模式算法

CBC主要是引入一个初始化向量(IV)来加强密文的随机性,保证相同明文通过相同的密钥加密的结果不一样。Shiro的IV通过源码我们可知是16byte的。因此分组也是16byte一组。

加密:

在这里插入图片描述

逻辑:

  1. 明文经过填充后,分为不同的组,以组的方式对数据进行处理
  2. 初始化向量(IV)首先和第一组明文进行XOR(异或)操作,得到得值称为middle。
  3. 采用密钥key,对middle进行加密生成第一组的密文。
  4. 第一组加密的密文作为第二组的初始向量(IV),参与第二组明文的异或操作。
  5. 依次执行块加密,最后将每一块的密文拼接成最终的密文。

由于初始化向量(IV)每次加密都是随机的,所以IV经常会被放在密文的前面,解密时先获取前面的IV,再对后面的密文进行解密。密文的第一组就是IV。

解密:
在这里插入图片描述

逻辑:

  1. 将密文进行分组(按照加密采用的分组大小),前面的第一组是初始化向量IV,从第二组开始才是真正的密文。
  2. 使用加密密钥对密文的第一组进行解密,得到Middle,第一组中只要密文不变,那么Middle一定不会改变。
  3. 将Middle和初始化向量IV进行异或,得到该组的明文
  4. 前一块密文是后一块密文的IV,通过异或中间值,得到明文
  5. 块全部解密完成后,拼接得到明文,密码算法校验明文的格式(填充格式是否正确)
  6. 校验通过得到明文,校验失败得到密文

4.3 解密

假设密文跟IV的值如下:

初始化向量: 7B 21 6A 63 49 51 17 0F
第一组密文: F8 51 D6 CC 68 FC 95 37
第二组密文: 85 87 95 A2 8E D4 AA C6


我们首先要知道,系统判断解密是否成功的点是解密后明文点最后一位或者几位的值跟aes算法中的分组逻辑是否匹配,匹配才能解密成功。

例如,解密后最后一组明文的最后一位值为0xFF,而aes算法的分组逻辑是8byte一组。那么这次解密一定是失败的。原因如下:

  1. 如果进行了明文填充,那么最后一位的值一定是小于0x08且大于0x01的。
  2. 如果没有进行明文填充而直接额外加了一个组,那么最后一位的值一定是0x08。

只有这两种可能性,而0xFF没在这两种可能性中,所以解密失败。


我们这时候选择只将IV跟第一段密文传入服务及进行解密,且将IV改成全0,就会有下面这张图:

在这里插入图片描述

我们只用第一组密文是因为第一组密文一定没有填充,也就是说解密出的明文不可能有0x01-0x08中的值(假设这个例子中是8字节一组)。

图中最后一行解密我们是看不到的,只会看到页面反馈是解密失败。这时候我们只需更改传进去的IV的最后一位,从0x00改到0xFF,在这个例子中直到当IV最后一位等于0x3C的时候,程序显示解密成功。

因为明文的最后一组一定会有填充物,而我们只传进去了IV跟一组密文,因此推断出这组密文被系统认为是最后一组密文,那么只有当这组密文的明文有合法填充位的时候解密才会成功,程序才不会报错。

因为我们知道这一组其实是密文中的第一组,也就一定不会有填充位,也就意味着所有的数据都不可能有0x01-0x08中的值(假设这个例子中是8字节一组)。

综上,这时候有8种可能性。因为明文填充位有可能是0x01到0x08八种,于是middle中的最后一位也就有八种可能性。这时候我们可以对这8种可能的middle进行分别实验,举例如下:


假设当明文填充位的数据为0x01-0x08时分别对应的middle最后一位的值为ABCDEFGH。

我们假设middle的最后一位是A,且填充位是0x01
这时候将IV最后一位的值进行更改,使得A异或IV的最后一位的值得到0x02。
然后对IV的倒数第二位进行爆破,从0x00到0xFF。

如果爆破成功,网页返回200,就能确定最后一位填充位是0x01,且前面的数据都不是0x01-0x08中的。进而能确定middle的最后一位。

如果爆破失败,那将middle最后一位换成另一个,以此类推。

只要第二个数据能爆破成功,就能确定第一个数据一定正确。


最终我们测试出填充位是0x01,反推出middle中的最后一位是0x3D,IV最后一位 xor 0x01 = middle最后一位。

接下来就更改IV的最后两位,使得解密可以成功,此时如果解密成功,那么解密出的明文最后两位一定是0x02,由此可推算出middle的倒数第二位。快速的做法是根据刚得到的middle中最后一位的值来更改IV中最后一位的值,先使得得到明文的最后一位是0x02,然后其实只需要更改IV倒数第二位的值就可以了,极大的减少了运算次数,节省破解时间。

以此类推可以推断出middle的值,进而可以解密真正的明文。

第一组的密文会变成第二组密文的IV,根据上面的逻辑可以继续进行解密,然后推断出所有的铭文数据。

4.4 加密

在这里插入图片描述

具体加密过程如下:

  1. 将要加密的数据分成N个块并进行数据填充,假设要加密的数据为hello,aes加密分组为8字节每组,那么明文填充后应该为hello\x03\x03\x03
  2. 创建两个明文组,内容为\xAA\xAA\xAA\xAA\xAA\xAA\xAA\xAA,第二个为全0,将全0的分组跟全A分组链接,0分组在前A分组在后。
  3. 将第二步形成的联合分组加到shiro合法cookie的最后面,对全0分组的最后一位进行修改,直到页面不报错,或者说没有rememberMe=deleteme。此时明文的最后一位是0x01,因此能推断出middle最后一位的数据。
  4. 接着修改倒数第二位直到页面不报错,推断出middle倒数第二位的数据,以此类推。此时可以得到全A组那一部分中middle的所有数据,只要全A组数据不变,那么全A组的middle永远不会变,因为middle是A组数据与key运算得到的。
  5. A组前一组的密文是由A组的middle与明文得到的,middle是不会变的,明文是我们要加密的铭文组的最后一个组,这两个属性我们都知道,因此我们可以用hello\x03\x03\x03这个明文去跟middle去异或,得到A组上一组的密文,假设运算得到的密文全部为B。那么如果我们想将hello\x03\x03\x03加密传送给服务端,则需要传输的数据为\xBB\xBB\xBB\xBB\xBB\xBB\xBB\xBB\xAA\xAA\xAA\xAA\xAA\xAA\xAA\xAA。
  6. 如果有多个分组,比如说在hello前面还有八个A字符,而我们已经得到这个明文字符串的结尾密文为\xBB\xBB\xBB\xBB\xBB\xBB\xBB\xBB\xAA\xAA\xAA\xAA\xAA\xAA\xAA\xAA,此时需要给\xBB\xBB\xBB\xBB\xBB\xBB\xBB\xB前面再添加八位的全0字符,接着就从后往前依次修改全0组的数据,像第3、4步一样,得到全B组的middle,可以用这个middle加密倒数第二组的明文形成密文,以此类推可以得到所有明文的密文。

5. 防御方式

  1. 由于这种方法需要爆破得到key,因此可以对短时间内多次访问的ip进行禁止访问操作,达到防御目的。
  2. 升级至安全版本。
  3. 关闭rememberMe持久化登录功能。

6. 参考文章

Shiro RCE again(Padding Oracle Attack)
Padding Oracle Attack(填充提示攻击)详解及验证
Apache Shiro 反序列化漏洞(Shiro-721 CVE-2016-4437)
利用Oracle Padding加密任意数据
Apache Shiro反序列化漏洞-Shiro-550复现总结
Shiro-721 RCE Via Padding Oracle Attack

  • 19
    点赞
  • 57
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Shanfenglan7

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值