Java 内存马不是某个固定类,也不是一种特定 Payload。它是一类把处理逻辑放进正在运行的 Java Web 进程,并挂入请求链、框架路由或 JVM 执行过程的技术。
理解内存马只需要先抓住三件事:
1 | 请求能不能到达它 |
后续所有 Servlet、Filter、Listener、Valve、Handler、Controller 和 Agent 型内存马,都建立在这三个问题上。
本系列只用于授权靶场、安全研究、代码审计和应急响应。
一、内存马与普通 WebShell 有什么区别
普通 WebShell 通常依赖磁盘文件:
1 | 请求 → JSP、Class 或脚本文件 → 执行 |
内存马的主要执行入口已经进入运行时:
1 | 请求 → JVM 中的处理组件 → 执行 |
因此,“磁盘上没有 JSP”不代表不存在后门,“重启后消失”也不代表问题已经解决。
| 对比项 | 普通 WebShell | Java 内存马 |
|---|---|---|
| 主要载体 | 磁盘文件 | JVM 中的对象或已修改类 |
| 请求入口 | 文件映射 | 请求链、框架路由或 JVM 执行点 |
| 常规文件扫描 | 较容易发现 | 可能完全没有对应文件 |
| 重启影响 | 文件仍在时通常继续存在 | 纯内存对象通常消失 |
| 排查重点 | 文件内容与访问日志 | 运行时注册、对象引用与字节码 |
“内存”描述的是主要驻留状态,不代表植入过程绝对无文件。攻击者仍可能使用临时 JAR、上传文件、启动参数或 Agent 完成注入,然后删除初始文件。
二、内存马为什么能驻留
2.1 请求可达
一个对象存在于堆中,不代表它能够处理请求。它必须进入真实执行路径:
1 | 请求 |
Filter 需要进入过滤链,Valve 或 Handler 需要进入容器请求链,Controller 需要进入框架路由。没有这条可达关系,就只是一个普通内存对象。
2.2 长期持有
局部变量会在调用结束后失去作用。内存马要持续存在,必须被生命周期更长的对象引用,例如:
- Web 应用上下文;
- 中间件请求链;
- Spring 容器;
- ClassLoader;
- 静态字段或后台线程;
- Instrumentation。
只要这些对象仍然持有它,垃圾回收器就不会释放它。
1 | JVM 根对象 |
2.3 条件触发
内存马通常不会处理所有请求,而是判断特定条件:
- URL Path;
- Header;
- Cookie;
- 参数名或参数值;
- 请求方法;
- 特定事件或方法调用。
条件不满足时继续正常链路,满足时才执行异常逻辑。这也是普通业务访问往往不报错的原因。
三、常见类型与版本边界
Java Web 请求可以粗略分为四层:
| 层级 | 常见类型 | 特点 |
|---|---|---|
| Servlet 规范层 | Servlet、Filter、Listener | 概念通用,不同容器内部保存方式不同 |
| 容器原生层 | Tomcat Valve、Jetty Handler、Undertow HttpHandler | 更靠近请求入口 |
| 框架层 | Spring Controller、Interceptor、Handler | 与应用框架绑定 |
| JVM 层 | Agent、Transformer、被修改的类 | 可能不增加任何 Web 组件 |
分析版本问题时不要把所有差异都归因于 JDK:
1 | 中间件产品和版本 → 决定对象挂在哪里 |
例如,javax.servlet.* 与 jakarta.servlet.* 是 Servlet API 代际变化,不是 JDK 8 与 JDK 17 的直接区别。判断代码能否运行时,必须同时确认这四项。
四、入口、驻留和恢复必须分开
攻击者先通过漏洞或已有权限获得一次代码执行,再把处理逻辑挂入运行时:
1 | 文件上传、反序列化、JNDI、表达式注入、管理面或 Agent |
这里必须区分三个概念:
| 概念 | 含义 |
|---|---|
| 植入入口 | 攻击者最初如何获得代码执行能力 |
| 驻留位置 | 异常对象或字节码最终挂在哪里 |
| 复活点 | 清除当前对象后,什么机制能再次植入 |
删除一个 Filter,只能说明一个驻留点被移除;如果漏洞、Agent、恶意制品或管理面权限仍然存在,内存马还会再次出现。
完整处置顺序应是:
1 | 隔离节点 |
到这里,Java 内存马的基础模型已经确定:请求可达决定能否触发,长期引用决定能否驻留,异常行为决定是否构成安全问题。后续文章只是把这套模型分别落到不同组件和中间件中。