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配置那行代码。
内存马注入时,定义了WebSocket的处理路径,例如
/path
,那么ws的连接地址就是ws://ip:port/path
,找一个WebSocket客户端工具,连接这个地址。省去了自己写客户端代码
另外,作者在工具中还给出了jsp的写法。一个执行命令的jsp和一个代理的jsp。代理的问题就不在这篇中总结了,下次写!
另外,WebSocket还有关于DoS和CSRF的漏洞。包括渗透测试中的利用场景https://xz.aliyun.com/t/10376