美文网首页
WebSocket漏洞利用基础

WebSocket漏洞利用基础

作者: AxisX | 来源:发表于2022-07-14 20:17 被阅读0次

    WebSocket是一种网络通信协议,位于应用层。与HTTP协议不同但兼容(同样基于TCP)。它与HTTP最不同的是,HTTP通信只能由客户端发起,WebSocket则是双向的,服务器也可以主动向客户端推送消息。

    1. 协议请求

    https://zh.m.wikipedia.org/zh-hans/WebSocket

    客户端请求

    GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    Origin: http://example.com
    Sec-WebSocket-Protocol: chat, superchat
    Sec-WebSocket-Version: 13
    

    服务器回应

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
    Sec-WebSocket-Protocol: chat
    

    这些字段是必须有的,也基本都要设置成这样。Sec-WebSocket-Key是随机字符串,服务器端会根据它来构造一个SHA-1信息摘要然后进行base64编码后作为Sec-WebSocket-Accept的值。

    协议标识符是ws。如果加密,则为wss(二者关系类似于http和https)。默认端口也是80和443

    ws://example.com:80/xxx/path
    

    2. Spring中应用WebSocket

    基于Spring的应用中使用WebSocket一般可以有三种方式:使用Java提供的@ServerEndpoint注解实现、使用Spring提供的低层级WebSocket API实现、使用STOMP消息实现。而CVE-2018-1270就出现在了使用STOMP的场景下。

    看到此处不免有个困惑,STOMP和WebSocket又有什么关系?
    https://stackoverflow.com/questions/40988030/what-is-the-difference-between-websocket-and-stomp-protocols/48373153
    简单来说,STOMP(Streaming Text Oriented Messaging Protocol) 是在 WebSockets 之上派生的。STOMP 只是提到了一些关于如何使用 WebSockets 在客户端和服务器之间交换消息帧的具体方法。可以理解为,STOMP是客户端和服务器之间包装消息的信封,而WebSocket则是快递。如果没有信封,那么在发消息时就缺少了一些信息,例如目的路由。

    如果要在没有STOMP的情况下创建WebSocket链接就需要自己处理路由到特定消息的处理程序。比如采用第二种方式—WebSocket API实现。

    (1)WebSocket API实现

    服务器端
    采用WebSocket API而没有STOMP就意味着消息要自己进行处理。主要是两步:
    (1)一个Handler继承自AbstractWebSocketHandler的子类,对“建立连接”、“接收/发送消息”、“异常情况”等场景进行处理。
    (2)一个Configurer,继承自WebSocketConfigurer。将Handler配置到WebSocket,它将处理来自 websocket 客户端的所有消息。这样就完成了服务器端。

    @Component
    public class SocketHandler extends TextWebSocketHandler {
        List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();
    
        @Override
        public void handleTextMessage(WebSocketSession session, TextMessage message) throws InterruptedException, IOException {
            Map<String, String> value = new Gson().fromJson(message.getPayload(), Map.class);
            session.sendMessage(new TextMessage("Hello " + value.get("name") + " !"));
        }
    
        @Override
        public void afterConnectionEstablished(WebSocketSession session) throws Exception {
            sessions.add(session); //the messages will be broadcasted to all users.
        }
    }
    
    @Configuration
    @EnableWebSocket
    public class WebSocketConfig implements WebSocketConfigurer {
        public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
            registry.addHandler(new SocketHandler(), "/name");
        }
    }
    

    客户端
    客户端则是在浏览器的js文件中定义一些方法,例如创建WebSocket连接,发送消息,监听响应等。

    function connect() {
        ws = new WebSocket('ws://10.128.5.250:8089/name');
        ws.onmessage = function(data){
            showGreeting(data.data);
        }
         setConnected(true);
    }
    
    function disconnect() {
        if (ws != null) {
            ws.close();
        }
        setConnected(false);
    }
    
    function sendName() {
        var data = JSON.stringify({'name': $("#name").val()})
        ws.send(data);
    }
    
    function showGreeting(message) {
        $("#greetings").append("<tr><td> " + message + "</td></tr>");
    }
    

    (2) STOMP实现

    服务器端
    STOMP自带信封功能,省去了注册Handler,而是直接注册一个StompEndpoints,即客户端连接的地址。还可以在方法中定义服务器处理消息和发送消息(广播)的前缀。
    (1)客户端发送消息 /message/hello,服务端处理消息@MessageMapping(“/hello”)
    (2)客户端监听/topic/greeting,服务端发送消息@SendTo("/topic/greeting")

    @Configuration
    @EnableWebSocketMessageBroker
    public class WebSocketConfig implements WebSocketMessageBrokerConfigurer{
        @Autowired
        private MyChannelInterceptor myChannelInterceptor;
     
        @Override
        public void registerStompEndpoints(StompEndpointRegistry registry) {
            registry.addEndpoint("/stomp-websocket").withSockJS();
        }
     
        @Override
        public void configureMessageBroker(MessageBrokerRegistry registry) {
            //客户端需要把消息发送到/message/xxx地址
            registry.setApplicationDestinationPrefixes("/message");
            //服务端广播消息的路径前缀,客户端需要相应订阅/topic/yyy这个地址的消息
            registry.enableSimpleBroker("/topic");
        }
     
        @Override
        public void configureClientInboundChannel(ChannelRegistration registration) {
            registration.interceptors(myChannelInterceptor);
        }
    }
    
    @Controller
    public class GreetingController {
        @MessageMapping("/hello")
        @SendTo("/topic/greeting")
        public HelloMessage greeting(Greeting greeting) {
            return new HelloMessage("Hello," + greeting.getName() + "!");
        }
    }
    

    STOMP的资料:
    http://jmesnil.net/stomp-websocket/doc/
    https://stomp.github.io/stomp-specification-1.0.html
    STOMP协议包含的一些命令如:CONNECT、SEND、SUBSCRIBE、UNSUBSCRIBE、DISCONNECT等。在SUBSCRIBE时候,可以添加selector,类似过滤器。在spring-messaging模块基于SockJS(Javascript的一个库),它配合浏览器来支持WebSocket协议。该模块中selector的内容是用SpEL来解析的,并且用的是StandardEvaluationContext,所以存在SpEL漏洞。

    在CVE-2018-1270的漏洞利用过程中,除了在app.js客户端头部加入selector,还在消息通信过程中利用burp修改了SUBSCRIBE的内容。除了插入SpringEL表达式,插入别的内容是不是也能造成相应的漏洞?

    例如发送聊天信息时,将消息后的内容改为xss的payload,那么收到信息的用户浏览器中可能就会弹窗。

    {"message":"<img src=1 onerror='alert(1)'>"}
    

    靶场测试:https://portswigger.net/web-security/websockets/lab-manipulating-messages-to-exploit-vulnerabilities

    (3)注解实现

    WebSocket API中需要一个Handler和一个Configurer。注解实现的服务器中则需要一个@ServerEndpoint和一个Configurer。

    @ServerEndpoint("/reverse")
    public class ReverseWebSocketEndpoint {
     
        @OnMessage
        public void handleMessage(Session session, String message) throws IOException {
            session.getBasicRemote().sendText("Reversed: " + new StringBuilder(message).reverse());
        }
    }
    
    @Configuration
    @EnableWebSocket
    public class WebSocketConfig{
     
        @Bean
        public ReverseWebSocketEndpoint reverseWebSocketEndpoint() {
            return new ReverseWebSocketEndpoint();
        }
     
        @Bean
        public ServerEndpointExporter serverEndpointExporter() {
            return new ServerEndpointExporter();
        }
    }
    

    基于这种方式,有人制作了WebSocket内存马。链接:https://github.com/veo/wsMemShell

    3. WebSocket内存马

    服务器端
    根据服务器端注解实现的方法可以看到需要一个Endpoint和一个Configurer。因为本身Tomcat中没有配置恶意的内存马,也就是无法采用注解扫描的形式。那么就需要把Endpoint写成如下形式

    public class XXXEndpoint extends Endpoint {
        @Override
        public void onOpen(Session session, EndpointConfig endpointConfig) {
            session.addMessageHandler(new MessageHandler.Whole<String>() {
                @Override
                public void onMessage(String message) {
                    ...
                }
            });
        }
    }
    

    配置类则大致如下

    public class ServerConfig implements ServerApplicationConfig {
        @Override
        public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> endpointClasses) {
            Set<ServerEndpointConfig> results = new HashSet<>();
            for (Class endpointClass : endpointClasses) {
                if (endpointClass.equals(ChatEndpoint.class)) {
                    ServerEndpointConfig serverEndpointConfig = ServerEndpointConfig.Builder.create(endpointClass, "/path").build();
                    results.add(serverEndpointConfig);
                }
            }
            return results;
        }
    }
    

    可以看到如果单考虑一个类的话,最核心的配置方法只有一行代码,ServerEndpointConfig根据Endpoint类和路径进行配置。

    ServerEndpointConfig serverEndpointConfig = ServerEndpointConfig.Builder.create(endpointClass, "/path").build();
    

    客户端
    客户端大致代码如下,在connectToServer中填入服务器端配置的ws地址ws://ip:port/path

    WebSocketContainer webSocketContainer = ContainerProvider.getWebSocketContainer();
    ClientEndpointConfig config = ClientEndpointConfig.Builder.create().decoders(StockTickDecoder.class).build();
    Session session = webSocketContainer.connectToServer(StockTickerClient().class, config, new URI("ws://xxx.com/path"));
    

    内存马实现
    上述内存马链接中获取StandardContext的方法和之前写内存马文章中提到的方式不同。它是从StandardRoot入手,进而获取StandardContext。

    在Tomcat启动Context的过程中,会实例化WebResourceRoot接口,该接口默认的实现类是StandardRoot,用于读取webapp的资源。从webapp中读取servlet、filter、listener等。另外还会实例化Loader对象,来支持运行期间加载各类class。但是用之前文中提到的那些获取StandardContext方法也可以。重点在于反射实现Endpoint配置那行代码。

    Tomcat WebSocket内存马
    内存马注入时,定义了WebSocket的处理路径,例如/path,那么ws的连接地址就是ws://ip:port/path,找一个WebSocket客户端工具,连接这个地址。省去了自己写客户端代码
    客户端连接ws
    另外,作者在工具中还给出了jsp的写法。一个执行命令的jsp和一个代理的jsp。代理的问题就不在这篇中总结了,下次写!

    另外,WebSocket还有关于DoS和CSRF的漏洞。包括渗透测试中的利用场景https://xz.aliyun.com/t/10376

    相关文章

      网友评论

          本文标题:WebSocket漏洞利用基础

          本文链接:https://www.haomeiwen.com/subject/aobzbrtx.html