Tomcat 特有,比 Filter 更底层、更隐蔽。
目录
- 一、什么是 Valve?
- 二、Pipeline 是什么?
- 三、Valve 在 Pipeline 中是”单向链表”
- 四、Valve 与 Filter 的关键区别
- 五、静态注册 Valve
- 六、动态注入(内存马核心)
- 七、Valve 的层级扩展(Engine / Host 级别注入)
- 八、四种内存马对比总结
- 总结
一、什么是 Valve?
Valve 是 Tomcat Pipeline-Valve 管道机制的一部分,属于 Tomcat 特有的概念,不属于 Servlet 规范。
类比:请求处理的”高速公路收费口”
为了方便理解这几种内存马的层级关系,用高速公路来类比一下:
- Listener:像高速公路入口的感应线圈(车一过就记录,不干预)。
- Filter:像安检闸机(可以拦车、检查、放行)。
- Valve:像高速公路上的隧道阀门,位于整个系统的最底层,所有车都必须经过它,而且它可以修改车的路线甚至把车”吞掉”。

每个容器(Engine / Host / Context / Wrapper) 都有自己的 Pipeline,里面可以添加多个 Valve。
二、Pipeline 是什么?
一句话解释:Pipeline 是 Tomcat 容器内部的一个处理链,里面可以按顺序放多个 Valve(阀门),请求会像水流一样依次流过这些阀门。
每个容器实例(Engine、Host、Context、Wrapper)都有一个 Pipeline 对象,这是 Tomcat 架构的固定设计。
即使不添加任何自定义 Valve,Pipeline 也至少包含一个 基础阀门(basic),用于完成容器的核心任务(比如调用子容器、处理 Servlet)。
可以通过
pipeline.addValve(valve)添加任意多个自定义 Valve,它们会按照添加顺序依次执行(先添加的先执行)。
请求的大致流转路径是:Engine → Host → Context → Wrapper 的 Valve 管道,最后才到 Servlet。也正因为如此,Valve 比 Filter 更早触发(在 Context 级别甚至更早),而且可以拦截所有请求(包括静态资源、404 等),不需要任何 URL 映射。
Filter、Servlet、Listener 都属于 Context 级别(即一个 Web 应用内部),而 Valve 可以加在 Engine、Host、Context、Wrapper 任意一层。
三、Valve 在 Pipeline 中是”单向链表”
1 | 请求 → [Valve A] → [Valve B] → [Valve C] → [基础阀门] → Servlet |
需要注意的点:
每个 Valve 的
invoke方法中必须调用getNext().invoke(request, response),否则请求链会中断(后面的 Valve 和 Servlet 都不会执行)。这个和 Filter 里的chain.doFilter(request, response)作用一模一样。添加过多 Valve 会影响性能,但内存马场景一般只加 1 个。
Valve 是全局生效的:添加到 Engine 的 Valve 会影响所有 Host 下的所有应用;添加到 Context 的 Valve 只影响当前 Web 应用。
拿到容器的 Pipeline 对象(通过反射获取 StandardContext、StandardHost 等),就可以直接调用
addValve()方法,而且该方法通常是 public 的,不需要反射破解。
四、Valve 与 Filter 的关键区别
| 特性 | Valve | Filter |
|---|---|---|
| 是否 Servlet 规范 | ❌ Tomcat 特有 | ✅ 规范定义 |
| 触发层级 | 容器级(Engine/Host/Context) | 应用级(Context 内) |
| 能否跨 Web 应用 | 能(Engine/Host 级别 Valve 可影响所有应用) | 不能(只拦截注册的应用) |
| 需要映射 URL 模式 | 否(自动全局拦截) | 是(需配置 /* 等) |
| 隐蔽性 | 更高(不常见于检测规则) | 较高(但已是重点查杀对象) |
五、静态注册 Valve
静态注册是 Tomcat 管理员配置全局功能的标准方式,不需要写任何 Java 代码(除了 Valve 实现类本身)。先搞清楚静态注册的流程,对后面理解动态注入也有帮助。
步骤 1:编写一个简单的 Valve 实现类
创建一个 Java 项目,写一个类实现 org.apache.catalina.Valve 接口。
Valve 是一个接口,实现它就必须实现接口中定义的所有抽象方法,即使不需要某个方法的功能,也要提供一个最简单的实现(比如空方法或返回默认值),否则编译就会报错。
1 | import org.apache.catalina.Valve; |
补充:异步处理是什么?
同步:请求进入
invoke方法后,必须等getNext().invoke()返回,整个请求才算处理完。这是默认方式。异步:在
invoke中,可以启动一个新线程去处理业务,然后立即返回invoke(不调用getNext()),让 Tomcat 线程不被阻塞。这需要阀门设置isAsyncSupported() { return true; },并且后续还要处理异步完成时的回调。
对于内存马来说,几乎不需要异步,保持 return false 即可。
步骤 2:编译并打包成 JAR
切换到正确的编译目录(对于包 com.demo,源文件的根目录是 src/main/java):
1 | cd E:\WWW\Valve\src\main\java |
几个注意点:
-cp是classpath(类路径)的缩写,作用是告诉 Java 编译器去哪里查找用户自定义的类(以及第三方库)。MyStaticValve.java中引用了 Tomcat 的Valve、Request、Response等类(位于catalina.jar),以及ServletException(位于servlet-api.jar),所以编译时必须通过-cp指定这些依赖的位置,否则编译器会报”找不到符号”的错误。classpath 分隔符在 Windows 上是分号
;,在 Linux 上是冒号:。如果有编码问题,加上
-encoding UTF-8参数。catalina.jar是 Tomcat 核心库的固定文件名,不能改名,确保路径指向正在使用的 Tomcat 9 的lib目录。
编译完成后打包成 JAR:
1 | jar cvf myvalve.jar com\demo\MyStaticValve.class |
步骤 3:将 JAR 放入 Tomcat 的 lib 目录
1 | copy myvalve.jar E:\WWW\apache-tomcat-9.0.117\lib\ |

步骤 4:修改 conf/server.xml
配置文件路径:E:\WWW\apache-tomcat-9.0.117\conf\server.xml
先了解一下 server.xml 的层级结构(Valve 只能在 Engine、Host、Context 里添加):
1 | <Server>(最顶层,代表整个 Tomcat 实例) |
找到 <Engine name="Catalina" defaultHost="localhost"> 标签,在里面添加:
1 | <Valve className="com.demo.MyStaticValve" /> |
写法
<Valve className="..." />等价于<Valve className="..."></Valve>,但更简洁。

步骤 5:重启 Tomcat
步骤 6:验证
访问任意 URL,控制台会输出 [StaticValve] Request URI: /xxx。

六、动态注入(内存马核心)
原理简述
通过熟悉的反射获取 StandardContext,然后:
- 调用
standardContext.getPipeline().addValve(Valve)添加自定义 Valve。 - 在 Valve 的
invoke方法中解析cmd参数,执行命令。 - 将命令结果通过
response直接回显到浏览器。 - 调用
getNext().invoke(request, response)放行请求,保证正常业务不受影响。
与 Filter 内存马相比,注入 Valve 不需要操作 FilterDefs、FilterMaps 那些结构,只需拿到 Pipeline 对象直接 addValve() 就行,注入步骤更少,也更简单。
Payload(JSP)
1 | <%@ page import="java.lang.reflect.Field" %> |
验证效果
访问 http://localhost:8080/inject.jsp,可以看到注入成功,列出了当前 Pipeline 中所有的 Valve:

中间那个就是我们自己注入的,其他的是 Tomcat 自带的。
再次访问时会提示已经存在,防止多次注入:

然后访问任意路径带上 cmd 参数即可执行命令:
1 | http://localhost:8080/任意?cmd=whoami |

七、Valve 的层级扩展(Engine / Host 级别注入)
上面的动态注入代码只添加到了 Context 级别(当前 Web 应用)。如果想要影响范围更广,可以向上取父容器,注入到 Host 或 Engine 级别:
1 | // 获取 Host(向上取父容器) |
作用范围不同,Engine 最广,Host 次之,Context 最窄。实战中根据需要选择,注入 Engine 级别隐蔽性更高,但也更容易影响正常业务,需要谨慎。
八、四种内存马对比总结
把 Filter、Servlet、Listener、Valve 四种内存马的核心特性放在一张表格里,方便对比选择:
| 类型 | 是否 Servlet 规范 | 触发层级 | 是否需要 URL 映射 | 回显是否方便 | 注入复杂度 | 隐蔽性 |
|---|---|---|---|---|---|---|
| Filter | ✅ 是 | Context | 需要 /* |
直接 | 中 | 中 |
| Servlet | ✅ 是 | Context | 需要具体路径 | 直接 | 低 | 低 |
| Listener | ✅ 是 | Context | 不需要 | 需要反射 | 低(注入)高(回显) | 中 |
| Valve | ❌ 否(Tomcat 特有) | Engine/Host/Context | 不需要 | 直接 | 低 | 高 |
总结
Valve 内存马是 Tomcat 内存马里隐蔽性相对最高的一种,核心优势在于:
- 不属于 Servlet 规范,很多安全产品的检测规则覆盖不到。
- 无需 URL 映射,任何请求都会经过,不像 Filter 还得配
/*。 - 注入步骤少,反射拿到
StandardContext后直接addValve()就行,不需要操作FilterDefs那些复杂结构。 - 可以加在多个层级,灵活控制影响范围。
整个内存马系列到这里,Filter → Servlet → Listener → Valve 四种方式都过了一遍,后面还有 Agent 内存马,原理上又是不同的思路,到时候再写。