WebSocket了解
定义
之前都是轮询与长轮询,都是由浏览器每隔一段时间去请求服务端,服务端再响应新数据
现在到它了,用于在 Web 浏览器和服务器之间创建快速双向通道的协议,说是不局限于每次由客户端主动请求了
过程
-
1. 以http握手启动,请求体升级成websocket,仅GET -
2. 服务端回一个确认包,回一个101 -
3. 两边就可以发消息了,最小单位是帧(frame) -
4. 维护一个持久的连接
http对比
http传输由客户端先发起(http、https)
无连接(即一次性连接,想要提高效率,要么keep-alive)
无状态(一般每次的请求响应之间是独立的,不记忆,需要cookie、session、token)
一请求一响应
websocket连接建立后可双向发起(ws://,wss://,也是80和443端口)
可长连接
有状态,貌似是因为可以实时更新?
连接建立后,双向通信
为什么WS的第一次请求是GET,见WebSocket 协议的规范 RFC 6455
https://www.rfc-editor.org/rfc/rfc6455.html
它还要求必须包含:
host头
Upgrade头(而且其值必须包含websocket)
Sec-WebSocket-Key头(其值是随机的16 个字节组成的数据值,且base64编码),比如0x0102,这就是2字节了;0x01,即8位二进制,是一字节
Origin
Sec-WebSocket-Version: 13
......等
https://www.rfc-editor.org/rfc/rfc6455.html
一般应用在聊天室、多人在线这种实时需求的场景
https://cloud.tencent.com/developer/article/1887095
python-websockets库
这里自己找G老师生成了,随便看看
server.py
import asyncioimport websockets# WebSocket server handlerasyncdefecho(websocket, path):print("Client connected")try:asyncfor message in websocket:print(f"Received: {message}")await websocket.send(f"Echo: {message}") # Echo back the messageexcept websockets.ConnectionClosedOK:print("Client disconnected")# Start the WebSocket serverasyncdefmain(): server = await websockets.serve(echo, "localhost", 8765)print("WebSocket server started on ws://localhost:8765")await server.wait_closed()asyncio.run(main())
这里是 调用的websockets.serve
,建立好了,只负责打印
client.py
import asyncioimport websocketsasyncdefclient(): uri = "ws://localhost:8765"# Server URIasyncwith websockets.connect(uri) as websocket:print("Connected to server")# Send a message to the server message = "Hello, WebSocket Server!"print(f"Sending: {message}")await websocket.send(message)# Wait for a response response = await websocket.recv()print(f"Received: {response}")asyncio.run(client())
因为是直接调的库,毕竟封装好了,请求发的http已经被抽象掉了,所以看不到具体实现
实际上更应该偏向这样的(客户端视角)
go代码的话,应该是这部分
https://github.com/HavocFramework/Havoc/blob/main/teamserver/pkg/service/agent.go
https://github.com/HavocFramework/Havoc/blob/main/teamserver/pkg/service/service.go
帧处理
http的传输格式是报文,而ws的传输数据格式是帧
这个就看看,具体的实现交给G老师
主要是有一个场景,目前有一个服务器是ws协议的,那客户端想要和它建立连接并发送数据
客户端要求:
1.发送http请求
2.解析响应包
3.发送/接收帧
服务器的要求:
1.解析http请求
2.发送响应包
3.发送/接收帧
python代码
client.py
import socketimport base64import hashlibdefgenerate_handshake_request():""" 生成 WebSocket 的握手请求 """ key = base64.b64encode(b"random_key_123").decode() request = ("GET /chat HTTP/1.1rn""Host: localhost:8000rn""Upgrade: websocketrn""Connection: Upgradern"f"Sec-WebSocket-Key: {key}rn""Sec-WebSocket-Version: 13rnrn" )return requestdefparse_handshake_response(response):""" 解析服务器的握手响应 """ lines = response.split("rn") status_line = lines[0]ifnot status_line.startswith("HTTP/1.1 101 Switching Protocols"):raise Exception("Failed to upgrade to WebSocket")for line in lines[1:]:if line.startswith("Sec-WebSocket-Accept"):# 这里可以添加对 Sec-WebSocket-Accept 的验证逻辑passdefsend_frame(sock, payload, opcode=0x1, final=True, mask=True):""" 发送 WebSocket 帧 """ first_byte = (final << 7) | opcode second_byte = 0x00if mask: second_byte |= 0x80 payload_length = len(payload)if payload_length <= 125: second_byte |= payload_length header = bytes([first_byte, second_byte])elif payload_length <= 65535: second_byte |= 126 header = bytes([first_byte, second_byte, (payload_length >> 8) & 0xFF, payload_length & 0xFF])else: second_byte |= 127 payload_length_bytes = payload_length.to_bytes(8, byteorder='big') header = bytes([first_byte, second_byte]) + payload_length_bytesif mask:import os mask_key = os.urandom(4) masked_payload = bytearray(payload.encode('utf-8'))for i inrange(len(payload)): masked_payload[i] ^= mask_key[i % 4] frame = header + mask_key + masked_payloadelse: frame = header + payload.encode('utf-8') sock.sendall(frame)defreceive_frame(sock):""" 接收 WebSocket 帧 """ first_byte = sock.recv(1)[0] final = bool(first_byte >> 7) opcode = first_byte & 0x0F second_byte = sock.recv(1)[0] masked = bool(second_byte >> 7) payload_length = second_byte & 0x7F offset = 2if payload_length == 126: payload_length = (int.from_bytes(sock.recv(2), byteorder='big')) offset = 4elif payload_length == 127: payload_length = int.from_bytes(sock.recv(8), byteorder='big') offset = 10if masked: mask_key = sock.recv(4) offset += 4 payload = bytearray(sock.recv(payload_length))for i inrange(payload_length): payload[i] ^= mask_key[i % 4]else: payload = sock.recv(payload_length)return final, opcode, payloaddefmain():# 发送 HTTP 请求 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(("localhost", 8000)) request = generate_handshake_request() sock.sendall(request.encode())# 解析响应包 response = sock.recv(1024).decode() parse_handshake_response(response)print("Let's talk now!")# 发送/接收帧whileTrue: message=input("Client: ") send_frame(sock, message)if message == 'exit':break final, opcode, payload = receive_frame(sock)print(f"Received: {payload.decode()}")if __name__ == "__main__": main()
server.py
import socketimport base64import hashlibdefparse_handshake_request(request):""" 解析客户端的握手请求 """ lines = request.split("rn") key = Nonefor line in lines:if line.startswith("Sec-WebSocket-Key"): key = line.split(": ")[1]return keydefgenerate_handshake_response(key):""" 生成握手响应 """ accept = base64.b64encode(hashlib.sha1((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode()).digest()).decode() response = ("HTTP/1.1 101 Switching Protocolsrn""Upgrade: websocketrn""Connection: Upgradern"f"Sec-WebSocket-Accept: {accept}rnrn" )return responsedefsend_frame(sock, payload, opcode=0x1, final=True, mask=True):""" 发送 WebSocket 帧 """ first_byte = (final << 7) | opcode second_byte = 0x00if mask: second_byte |= 0x80 payload_length = len(payload)if payload_length <= 125: second_byte |= payload_length header = bytes([first_byte, second_byte])elif payload_length <= 65535: second_byte |= 126 header = bytes([first_byte, second_byte, (payload_length >> 8) & 0xFF, payload_length & 0xFF])else: second_byte |= 127 payload_length_bytes = payload_length.to_bytes(8, byteorder='big') header = bytes([first_byte, second_byte]) + payload_length_bytesif mask:import os mask_key = os.urandom(4) masked_payload = bytearray(payload.encode('utf-8'))for i inrange(len(payload)): masked_payload[i] ^= mask_key[i % 4] frame = header + mask_key + masked_payloadelse: frame = header + payload.encode('utf-8') sock.sendall(frame)defreceive_frame(sock):""" 接收 WebSocket 帧 """ first_byte = sock.recv(1)[0] final = bool(first_byte >> 7) opcode = first_byte & 0x0F second_byte = sock.recv(1)[0] masked = bool(second_byte >> 7) payload_length = second_byte & 0x7F offset = 2if payload_length == 126: payload_length = (int.from_bytes(sock.recv(2), byteorder='big')) offset = 4elif payload_length == 127: payload_length = int.from_bytes(sock.recv(8), byteorder='big') offset = 10if masked: mask_key = sock.recv(4) offset += 4 payload = bytearray(sock.recv(payload_length))for i inrange(payload_length): payload[i] ^= mask_key[i % 4]else: payload = sock.recv(payload_length)return final, opcode, payloaddefmain(): server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.bind(("localhost", 8000)) server_socket.listen(1)print("Listening on port 8000...")whileTrue: client_socket, client_address = server_socket.accept()print(f"Accepted connection from {client_address}") request = b""whileTrue: data = client_socket.recv(1024) request += dataifb"rnrn"in request:break# 解析 HTTP 请求 key = parse_handshake_request(request.decode())if key:# 发送响应包 response = generate_handshake_response(key) client_socket.sendall(response.encode())print("Let's talk!")# 发送/接收帧whileTrue: final, opcode, payload = receive_frame(client_socket)print(f"Received: {payload.decode()}") message=input("Server: ")if message =='exit':break send_frame(client_socket, message)if __name__ == "__main__": main()
这里的发送和接收写的简单,所以看起来是按顺序的,必须等待对方,实际上实时部分需要靠多线程来处理一下,主要是第一次连接建立完后,哪边先发数据都可以,随时发,知道就行
这里示例代码两边都用的socket,因为ws和http都基于tcp协议
???
主要是有个poc,它在客户端仅用request.post就能完成对目标websocket的连接和传输(代码部分略过),看不进去。。。
该poc是个SSRF,它这里是相当于对外网的一个文件读取,内容是12313
但如果是对它内部的40056(比如外网是https://,有个内网的wss://..40056),会有tls的阻碍导致无法读取文件,所以复现的话
client/src/Havoc/Connector.cc
里:url改成ws://
含Socket->setSslConfiguration()的都注释掉
teamserver/cmd/server/teamserver.go
里:替换如下:
if err = t.Server.Engine.Run(Host+":"+Port);
ws://
https://4xura.com/ctf/htb/
make ts-build重新编译即可
./havoc server --profile ~/se*/b*/havoc.yaotl -v --debug
这样就可以正常访问了
总结
websocket主要用于双向通信,高效、实时
http与websocket的关系和区别
数据传输为帧(frame),比如把http的json格式转换成frame,看看就行
参考
https://cloud.tencent.com/developer/article/1887095
https://www.cnblogs.com/mq0036/p/18643983
https://www.rfc-editor.org/rfc/rfc6455.html
https://www.openmymind.net/WebSocket-Framing-Masking-Fragmentation-and-More/
原文始发于微信公众号(羽泪云小栈):WebSocket了解
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论