WebSocket漏洞利用基础

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 sessions = new CopyOnWriteArrayList<>();

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) throws InterruptedException, IOException {
        Map 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(" " + message + "");
}

(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":""}

靶场测试: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() {
            @Override
            public void onMessage(String message) {
                ...
            }
        });
    }
}

配置类则大致如下

public class ServerConfig implements ServerApplicationConfig {
    @Override
    public Set getEndpointConfigs(Set> endpointClasses) {
        Set 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漏洞利用基础)