Nacos 反序列化漏洞初步探针

  1. 前置知识基础:

参考《附录》

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接口如图(找一个实现反序列化功能的类)

Nacos 反序列化漏洞初步探针_第1张图片

再搜索Hessian(前置知识基础有讲为什么搜索的是Hessian)

Nacos 反序列化漏洞初步探针_第2张图片

根据以上信息分析漏洞出现范围

锁定到以下位置

进行代码分析:

此处只放了核心代码的解析,完整解析参考《附录》

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 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 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 序列化技术来传递敏感信息或执行敏感操作,选择更为安全的数据传输方式。

你可能感兴趣的:(java,intellij-idea,web安全)