- 前置知识基础:
参考《附录》
2. 代码审计分析:
第一步,下载源码:
Releases · alibaba/nacos · GitHub
第二步,思路分析:
将源码放入idea中打开(因为是Java并且idea使用比较熟练,根据个人喜好选择审计工具),确定审计思路,由于该漏洞是该漏洞存在于Nacos中,是一个反序列化漏洞,攻击者可以无限制使用hessian进行反序列化利用,最终实现RCE。所以审计方式采用敏感函数回溯法。
首先,该漏洞是要实现执行命令获取信息,所以我们的目标是找一个实现该功能的类。
其次,是反序列化漏洞所以该类就要实现java.io. Serializable接口,也可以实现Externalizable接口。(但是Externalizable接口的功能可以通过使用ObjectOutputStream和ObjectInputStream的writeObject()和readObject()方法来实现。并且实现也比较麻烦所以先分析Serializable接口)。
再而,还需要可被控制的变量用来传递构造的payload。
最后,需要Java调用外部可执行程序或系统命令。
Java中用于执行外部进程的方法主要有以下几种:
使用Runtime类:通过调用Runtime.getRuntime().exec()方法启动新进程。可以创建子进程并与其进行交互,例如获取子进程的输入/输出流等信息。
使用ProcessBuilder类:功能类似于Runtime类,但提供了更多定制化参数,例如环境变量、工作目录等等。可以通过调用ProcessBuilder.start()方法来启动新进程,并返回进程对象。
使用Process类:通过Process类,我们可以控制子进程的状态、输入/输出流等信息。例如通过Process.waitFor()方法可以等待子进程完成执行。
使用Desktop类:可以用来执行本地操作系统相关的默认应用程序。例如,可以使用Desktop类启动默认的浏览器或电子邮件客户端。
第三步:审计代码:
根据上述步骤先全局搜索Serializable接口如图(找一个实现反序列化功能的类)
再搜索Hessian(前置知识基础有讲为什么搜索的是Hessian)
根据以上信息分析漏洞出现范围
锁定到以下位置
进行代码分析:
private <T> T deseiralize0(byte[] data) {
if (ByteUtils.isEmpty(data)) {
return null;
}
Hessian2Input input = new Hessian2Input(new ByteArrayInputStream(data));
input.setSerializerFactory(serializerFactory);
Object resultObject;
try {
resultObject = input.readObject();
input.close();
} catch (IOException e) {
throw new RuntimeException("IOException occurred when Hessian serializer decode!", e);
}
return (T) resultObject;
}
private <T> T deseiralize0(byte[] data) {
定义了一个私有泛型方法deseiralize0,其返回类型为T,接收一个byte数组data作为参数。
if (ByteUtils.isEmpty(data)) {
判断传入的字节数组是否为空或长度为0,如果是,则返回null。
Hessian2Input input = new Hessian2Input(new ByteArrayInputStream(data));
创建一个Hessian2Input对象input,通过字节数组data创建一个ByteArrayInputStream对象,并将其传递给Hessian2Input的构造函数初始化。
input.setSerializerFactory(serializerFactory);
设置Hessian2Input对象的序列化工厂为serializerFactory。
Object resultObject;
声明一个Object类型的变量resultObject,用于存储反序列化后的对象。
try {
开始try代码块,尝试执行下面的代码块。
resultObject = input.readObject();
通过调用Hessian2Input对象的readObject()方法,将字节数组data反序列化成对象并赋值给resultObject变量。
input.close();
关闭Hessian2Input对象以释放资源。
} catch (IOException e) {
捕获IO异常IOException,并执行相应的处理代码。
throw new RuntimeException("IOException occurred when Hessian serializer decode!", e);
抛出运行时异常RuntimeException,并设置相应的提示信息和异常堆栈信息。
}
结束try代码块。
return (T) resultObject;
将resultObject强制转换为泛型类型T,并返回该对象。
public <T> byte[] serialize(T obj) {
ByteArrayOutputStream byteArray = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(byteArray);
output.setSerializerFactory(serializerFactory);
try {
output.writeObject(obj);
output.close();
} catch (IOException e) {
throw new RuntimeException("IOException occurred when Hessian serializer encode!", e);
}
return byteArray.toByteArray();
}
public <T> byte[] serialize(T obj) {
定义了一个公共泛型方法serialize,其返回类型为byte数组,接收一个泛型对象obj作为参数。
ByteArrayOutputStream byteArray = new ByteArrayOutputStream();
创建一个ByteArrayOutputStream对象byteArray,用于存储序列化后的字节数组。
Hessian2Output output = new Hessian2Output(byteArray);
创建一个Hessian2Output对象output,并将byteArray传递给其构造函数以进行初始化。
output.setSerializerFactory(serializerFactory);
设置Hessian2Output对象的序列化工厂为serializerFactory。
try {
开始try代码块,尝试执行下面的代码块。
output.writeObject(obj);
通过调用Hessian2Output对象的writeObject()方法,将泛型对象进行序列化。
output.close();
关闭Hessian2Output对象以释放资源。
} catch (IOException e) {
捕获IO异常IOException,并执行相应的处理代码。
throw new RuntimeException("IOException occurred when Hessian serializer encode!", e);
抛出运行时异常RuntimeException,并设置相应的提示信息和异常堆栈信息。
}
结束try代码块。
return byteArray.toByteArray();
将ByteArrayOutputStream对象byteArray中的字节数组提取出来并返回。
将核心代码看完后分析漏洞出现原因:
public class HessianSerializer implements Serializer
HessianSerializer是实现serializer功能的类
Hessian2Input input = new Hessian2Input(new ByteArrayInputStream(data))
通过字节数组data创建0一个ByteArrayInputStream对象,用来传递构造的payload
serialize方法用于将对象序列化成byte数组。方法首先创建一个ByteArrayOutputStream对象和一个Hessian2Output对象,将ByteArrayOutputStream对象传递给Hessian2Output的构造函数进行初始化,然后通过调用setSerializerFactory方法设置序列化工厂,之后调用writeObject方法将对象写入输出流中,并通过close方法关闭Hessian2Output对象,最后返回ByteArrayOutputStream对象中的字节数组。
deserialize方法用于将byte数组反序列化成对象。方法首先根据传入的字节数组创建一个ByteArrayInputStream对象,并将其作 为参数创建一个Hessian2Input对象,然后通过调用setSerializerFactory方法设置序列化工厂,接着通过调用readObject方法从输入流中读取反序列化后的对象,最后关闭Hessian2Input对象,并将对象强制转换成泛型类型并返回。
提到处理Raft请求时,使用hessian进行反序列化。具体地,在Raft协议中,节点之间会互相同步日志,这些日志需要进行序列化和反序列化。当一个节点接收到另一个节点发送过来的数据时,需要先对数据进行反序列化,然后再进行相应的操作,比如根据数据更新本地状态或重新向其他节点发送数据等。因此,可以使用Hessian序列化器将数据进行反序列化。
总结:该漏洞出现原因攻击者利用raft的leader功能将结点接收到的日志记录同步,将构造的payload传入日志中,再利用同步功能使用Hessian2Input包装后放入序列化工厂serializerFactory反序列化,实现代码执行
POC构造思路根据上面的解释与定义,我们可以得到一个大致的思路
构造一个对象 -> 将其序列化 —> 提交数据到能反序列化的方法
payload构造工具:Ysoserial
修复建议:
反序列化漏洞的出现原因主要是因为在进行 Java 对象的反序列化时,没有对序列化数据进行足够的校验和过滤,导致恶意攻击者可以构造恶意序列化数据,在反序列化过程中执行任意代码,从而导致安全漏洞。
具体来说,在 Java 序列化和反序列化过程中,可以通过 java.io.ObjectInputStream 和 java.io.ObjectOutputStream 进行操作。当一个对象被序列化后,其包含的所有属性以及类型信息都被写入到了序列化数据中。而在反序列化时,Java 虚拟机会根据序列化数据来重建对象,将各个属性值重新赋值给对象。
这个过程中,如果没有对序列化数据进行严格的校验和过滤,就可能会被恶意攻击者利用,构造恶意序列化数据,从而在反序列化时执行任意代码,导致安全问题。例如,攻击者可以将类路径或类名随意修改,借此利用 Java 反射机制执行一些危险操作;或者通过构造恶意的流程控制语句,实现远程命令执行或者拒绝服务攻击等。
为了避免反序列化漏洞,应在反序列化时对输入的序列化数据进行严格的校验和过滤,仅反序列化信任的数据,并对反序列化操作授权。同时,还应尽可能避免使用 Java 序列化技术来传递敏感信息或执行敏感操作,选择更为安全的数据传输方式。