内存马原理总纲

内存马原理总纲

本系列的总纲篇。前面的文章按”类型”拆开讲了 Filter / Servlet / Listener / Valve / Agent / Spring / WebSocket 各自的内存马;这一篇回答为什么会有内存马,以及每一种内存马到底在模拟容器的哪一步。

本文涉及的 Tomcat 调用链均已用 javap 对 Tomcat 8.5.39 的 catalina.jar 逐一核实,不是凭记忆写的。


一、为什么会有内存马

一句话:内存马的本质,是把「恶意代码 + 触发条件」在运行时动态组装进容器的注册表/调度机制,让容器像调度正常组件一样调度它。

1.1 动态注册是容器/框架的合法能力

Web 容器和框架天生支持运行时动态注册组件——这是给插件化、热部署、监控用的设计:

容器/框架 动态注册能力 用途
Tomcat addFilterDef / addChild / addApplicationEventListener / Pipeline.addValve 运行时加 Filter / Servlet / Listener / Valve
Spring MVC registerMapping / 拦截器装配 动态加路由 / 拦截器
JVM Instrumentation.addTransformer + retransformClasses APM、热更新

1.2 容器眼里,组件只是「(代码, 触发条件)」

组件 代码 触发条件
Filter doFilter 逻辑 URL pattern
Servlet service 逻辑 URL path
Listener 回调方法 事件
Valve invoke 逻辑 请求进入容器
Controller 方法体 路由
Agent 插入代码 字节码 方法被调用

容器的调度机制只认”注册表里有没有”,不关心代码来源——jar、注解扫描、配置文件、运行时反射塞进去的,对容器没有区别。攻击者拿到代码执行后,就能模拟正常的注册过程,把恶意代码和触发条件填进注册表。

1.3 对比文件型 webshell

  • JSP webshell = 「磁盘文件(代码)」+「URL 映射」两个都在磁盘上,删文件即失效;
  • 内存马 = 把「代码 + 映射」直接组装进运行时的注册表,磁盘上什么都没有,随进程存活。

所以内存马存在的根本原因:动态注册是合法特性,容器无法区分运行时注册的是正常组件还是恶意组件。


二、Tomcat 请求处理总链路(8.5.39 实测调用链)

先把一条 HTTP 请求从进入到返回的真实调用链画出来(所有类.方法均经 javap 核实):

CoyoteAdapter.service()
  → Mapper.map()                        // URL 路由:定位 Host / Context / Wrapper
  → StandardEngineValve.invoke()        // Engine 管道的基础阀
  → StandardHostValve.invoke()          // Host 管道的基础阀
      └→ fireRequestInitEvent(request)  // ← Listener 内存马的触发点
  → StandardContextValve.invoke()       // Context 管道的基础阀
  → StandardWrapperValve.invoke()       // Wrapper 管道的基础阀
      ├→ StandardWrapper.allocate()     // 拿 Servlet 实例
      └→ ApplicationFilterFactory.createFilterChain()   // ← Filter 内存马的"组装点"
          → ApplicationFilterChain.doFilter()
            → internalDoFilter()        // ← Agent 内存马的 hook 点
              → Filter1.doFilter() ... FilterN.doFilter()
              → servlet.service()       // ← Servlet 内存马的执行点

关键观察:这条链的每一环,都对应一种内存马的”插入点”:

  • 请求还没进 Filter 链,StandardHostValve 就先派发 Listener 事件 → Listener 型在这里;
  • ApplicationFilterFactory 按注册表构建 Filter 链 → Filter 型在这里;
  • 链的终点是 servlet.service() → Servlet 型在这里;
  • 四个容器各自的 Pipeline → Valve 型可以挂在任意一段;
  • internalDoFilter 是每个请求必经的方法体 → Agent 型直接改它的字节码。

三、逐类:正常注册流程 vs 内存马模拟

下表”正常流程”列的是容器启动/请求时的真实调用(类.方法 + 作用);”内存马模拟”列的是攻击者用哪个 API/反射替代它。所有方法均对 8.5.39 javap 核实。

3.1 Filter 内存马

步骤 正常流程(类.方法 → 作用) 内存马模拟
1 ContextConfig 解析 web.xml / @WebFilter 反射拿 StandardContext
2 构造 FilterDef(filterName+filterClass) 构造 FilterDef,filterClass = 恶意类
3 构造 FilterMap(URL 模式) 构造 FilterMap,/*
4 StandardContext.filterStart():遍历 filterDefs → new ApplicationFilterConfig(Context, FilterDef) → 存 filterConfigs 反射调用同一个构造器 ApplicationFilterConfig(Context, FilterDef) 并放进 filterConfigs
5 请求时 ApplicationFilterFactory.createFilterChain():遍历 findFilterMaps() → filterConfigs.get(name) → chain.addFilter(config) 恶意 Filter 已在这三个集合里,被当作普通 Filter 一起调度

本质:模拟 addFilterDef + addFilterMap + ApplicationFilterConfig 构造,把「恶意 doFilter 代码 + /* 映射」组装进 filterDefs / filterMaps / filterConfigs。

3.2 Servlet 内存马

步骤 正常流程(类.方法 → 作用) 内存马模拟
1 StandardContext.createWrapper() 创建 Wrapper 同方法
2 StandardContext.addChild(wrapper) → 存 children(启动时自动 child.start()) 同方法,setServletClass 指向恶意类(或 setServlet(实例))
3 StandardContext.addServletMappingDecoded(pattern, name) → 写 servletMappings + Mapper 同方法,pattern=/shell
4 请求时 CoyoteAdapter.service() → Mapper.map() 路由到 Wrapper → StandardWrapperValve.invoke() → allocate() → service() 访问 /shell 即命中

本质:模拟 createWrapper + addChild + addServletMappingDecoded,把「恶意 service 代码 + 精确路径」组装进 children + Mapper。

3.3 Listener 内存马

步骤 正常流程(类.方法 → 作用) 内存马模拟
1 ContextConfig 注册监听器 直接 addApplicationEventListener(new EvilListener())
2 StandardContext.addApplicationEventListener() → applicationEventListenersList.add(listener) 同一个 public 方法
3 请求时 StandardHostValve.invoke() → Context.fireRequestInitEvent() → 遍历 getApplicationEventListeners() → instanceof ServletRequestListener → listener.requestInitialized() 恶意 Listener 已在列表里,每个请求到达即触发

本质:模拟 addApplicationEventListener,把「恶意回调 + 请求到达事件」组装进监听器列表。触发条件不需要 URL 映射——事件由容器自动派发。

3.4 Valve 内存马

步骤 正常流程(类.方法 → 作用) 内存马模拟
1 ContainerBase.addValve(valve) → Pipeline.addValve(valve) + valve.setContainer(this) + valve.start() 同方法 getPipeline().addValve(new EvilValve())
2 StandardPipeline.addValve():设 first,valve.setNext(...) 链到基础阀前 同方法,无需反射
3 请求进入容器时沿 Pipeline 逐个 invoke() 恶意 Valve 在管道上,任何请求都经过

本质:模拟 Pipeline.addValve,把「恶意 invoke 代码 + 请求进入容器」组装进管道。比 Filter 更底层——不碰 URL 映射,404、静态资源都经过。

3.5 Spring Interceptor / Controller 内存马

类型 正常流程(类.方法 → 作用) 内存马模拟
Interceptor 配置声明 → AbstractHandlerMapping 启动时适配进 adaptedInterceptors 列表;请求时 getHandlerExecutionChain() 遍历、preHandle() 反射取 adaptedInterceptors(private final List,可 add),把恶意 HandlerInterceptor add 进去
Controller 启动扫描 @RequestMapping → registerHandlerMethod() 填 mappingRegistry 直接调 public RequestMappingHandlerMapping.registerMapping(info, handler, method)

本质:Interceptor 模拟”装配拦截器链”,把恶意 preHandle + 每个请求组装进 adaptedInterceptors;Controller 模拟”登记路由”,把恶意方法 + /evil 组装进 mappingRegistry。

3.6 Agent 内存马

步骤 正常流程(类.方法 → 作用) 内存马模拟
1 JVM 加载类时若有 ClassFileTransformer 则回调 attach 后 inst.addTransformer(t, true)
2 APM/热更新对已加载类用 retransformClasses() 同方法,定位 ApplicationFilterChain
3 转换器返回修改后的字节码 javassist insertBefore 在 internalDoFilter 开头插入命令执行,return 短路

本质:这里的”组装”不再是往注册表加条目,而是直接改写代码本身的位置——恶意代码成了正常方法体的一部分。所以最难检测:无新组件、无新映射、类名原装。


四、统一视角

类型 模拟的注册过程 恶意代码 触发条件 组装进
Filter addFilterDef/addFilterMap/ApplicationFilterConfig doFilter URL /* StandardContext 三个集合
Servlet createWrapper/addChild/addServletMappingDecoded service 精确路径 children + Mapper
Listener addApplicationEventListener requestInitialized 请求事件 监听器列表
Valve Pipeline.addValve invoke 进入容器 管道
Spring Interceptor 装配 adaptedInterceptors preHandle 每个请求 拦截器链
Spring Controller registerMapping 控制器方法 路由 mappingRegistry
Agent Instrumentation retransform 插入方法体 方法调用 类字节码

五、为什么难检测,防御总思路

  1. 文件侧失效:内存马无磁盘文件,文件扫描、目录审计看不到。
  2. 注册表难区分:它用的是和正常组件完全相同的注册接口(addValve、addApplicationEventListener、registerMapping 等),容器无法在”注册行为”层面区分。
  3. 因此防御必须转向”行为检测”:
    • 事前:修复反序列化 / 表达式注入等 RCE 漏洞,切断”先拿到代码执行”这个前提;
    • 事中:RASP 监控 addValve / addFilterDef / registerMapping / retransformClasses 等注册调用,判断调用栈是否来自启动阶段、线程是否可疑;
    • 事后:周期性对比运行时注册表基线(Filter 列表 / Servlet 映射 / Listener / 管道 Valve / Spring HandlerMapping),以及 JVM 字节码 diff(Agent 型)。

六、系列导航


七、阅读建议与通用说明

  • 阅读顺序:建议先读本篇(总纲)建立整体框架,再按需读各分篇;各分篇之间用”延伸阅读”互相跳转。
  • 命令执行代码:各分篇里的 exec 方法(ProcessBuilder + redirectErrorStream(true))都是同一套写法——合并 stderr、字节循环读取,避免命令输出与错误流互相阻塞;因各篇需自包含而重复出现。
  • JSP 触发脚本的两种形态:
    • 讲解简化:Filter / Servlet / Listener 三篇正文里的 JSP 直接引用注入器类,假设恶意类已部署,便于阅读(已标注);
    • 自包含(真实可用):Valve 篇 3.4 与测试工程里的 JSP 把恶意类内联进 <%! %>,单独一个文件即可完成注入——真实利用(上传 JSP)应使用这种形态。
改变就是好事。