文章目录
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过长的方式:
- 使用urlclassloader加载远程字节码
- 将字节码放在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
。
示例代码如下:
publicclassSimplePrincipalCollectionTest{@Testpublicvoidtest01()throwsIOException,NotFoundException,CannotCompileException{SimplePrincipalCollection simplePrincipalCollection =newSimplePrincipalCollection();AesCipherService aes =newAesCipherService();byte[] key =java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");ByteArrayOutputStream barr =newByteArrayOutputStream();ObjectOutputStream oos =newObjectOutputStream(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一组。
加密:
逻辑:
- 明文经过填充后,分为不同的组,以组的方式对数据进行处理
- 初始化向量(IV)首先和第一组明文进行XOR(异或)操作,得到得值称为middle。
- 采用密钥key,对middle进行加密生成第一组的密文。
- 第一组加密的密文作为第二组的初始向量(IV),参与第二组明文的异或操作。
- 依次执行块加密,最后将每一块的密文拼接成最终的密文。
由于初始化向量(IV)每次加密都是随机的,所以IV经常会被放在密文的前面,解密时先获取前面的IV,再对后面的密文进行解密。
密文的第一组就是IV。
解密:
逻辑:
- 将密文进行分组(按照加密采用的分组大小),前面的
第一组是初始化向量IV
,从第二组开始才是真正的密文。 - 使用加密密钥对密文的第一组进行解密,得到Middle,
第一组中只要密文不变,那么Middle一定不会改变。
- 将Middle和初始化向量IV进行异或,得到该组的明文
- 前一块密文是后一块密文的IV,通过异或中间值,得到明文
- 块全部解密完成后,拼接得到明文,密码算法校验明文的格式(填充格式是否正确)
- 校验通过得到明文,校验失败得到密文
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一组。那么这次解密一定是失败的。原因如下:
- 如果进行了明文填充,那么最后一位的值一定是小于0x08且大于0x01的。
- 如果没有进行明文填充而直接额外加了一个组,那么最后一位的值一定是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 加密
具体加密过程如下:
- 将要加密的数据分成N个块并进行数据填充,假设要加密的数据为hello,aes加密分组为8字节每组,那么明文填充后应该为hello\x03\x03\x03
- 创建两个明文组,内容为\xAA\xAA\xAA\xAA\xAA\xAA\xAA\xAA,第二个为全0,将全0的分组跟全A分组链接,0分组在前A分组在后。
- 将第二步形成的联合分组加到shiro合法cookie的最后面,对
全0分组
的最后一位进行修改,直到页面不报错,或者说没有rememberMe=deleteme。此时明文的最后一位是0x01,因此能推断出middle最后一位
的数据。 - 接着修改倒数第二位直到页面不报错,推断出middle倒数第二位的数据,以此类推。此时可以得到全A组那一部分中middle的所有数据,只要全A组数据不变,那么全A组的middle永远不会变,因为middle是A组数据与key运算得到的。
- 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。 - 如果有多个分组,比如说在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. 防御方式
- 由于这种方法需要爆破得到key,因此可以对短时间内多次访问的ip进行禁止访问操作,达到防御目的。
- 升级至安全版本。
- 关闭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
版权归原作者 Shanfenglan7 所有, 如有侵权,请联系我们删除。