Skip to content

Commit

Permalink
Add support for websockets (secdev#4578)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasdrufva committed Nov 6, 2024
1 parent ab975bf commit 68670b4
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 9 deletions.
16 changes: 11 additions & 5 deletions scapy/contrib/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
import base64
import zlib
from hashlib import sha1
from scapy.fields import (BitFieldLenField, Field, BitField, BitEnumField, ConditionalField, XIntField, FieldLenField, XNBytesField)
from scapy.fields import (BitFieldLenField, Field, BitField, BitEnumField, ConditionalField, XNBytesField)
from scapy.layers.http import HTTPRequest, HTTPResponse
from scapy.layers.inet import TCP
from scapy.packet import Packet
from scapy.error import Scapy_Exception
import logging


class PayloadLenField(BitFieldLenField):
Expand Down Expand Up @@ -94,16 +95,21 @@ def getfield(self, pkt, s):
payloadData = (data_int ^ mask_int).to_bytes(len(payloadData), 'big')

if("permessage-deflate" in pkt.extensions):
try:
try:
payloadData = pkt.decoder[0](payloadData + b"\x00\x00\xff\xff")
except Exception:
# Failed to decompress payload
pass
logging.debug("Failed to decompress payload", payloadData)

return s[length:], payloadData

def addfield(self, pkt, s, val):
# Ensure val is bytes and append the data to the packet
if pkt.mask:
key = struct.pack("I", pkt.maskingKey)[::-1]
data_int = int.from_bytes(val, 'big')
mask_repeated = key * (len(val) // 4) + key[: len(val) % 4]
mask_int = int.from_bytes(mask_repeated, 'big')
val = (data_int ^ mask_int).to_bytes(len(val), 'big')

return s + bytes(val)

def i2len(self, pkt, val):
Expand Down
8 changes: 4 additions & 4 deletions scapy/layers/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,10 +529,10 @@ def do_dissect(self, s):
"""From the HTTP packet string, populate the scapy object"""
first_line, body = _dissect_headers(self, s)
try:
version_status_reason = re.split(br"\s+", first_line, maxsplit=2) + [None]
self.setfieldval('Http_Version', version_status_reason[0])
self.setfieldval('Status_Code', version_status_reason[1])
self.setfieldval('Reason_Phrase', version_status_reason[2])
method_path_version = re.split(br"\s+", first_line, maxsplit=2) + [None]
self.setfieldval('Method', method_path_version[0])
self.setfieldval('Path', method_path_version[1])
self.setfieldval('Http_Version', method_path_version[2])
except ValueError:
pass
if body:
Expand Down
61 changes: 61 additions & 0 deletions test/contrib/websocket.uts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# WebSocket layer unit tests
# Copyright (C) 2024 Lucas Drufva <[email protected]>
#
# Type the following command to launch start the tests:
# $ test/run_tests -P "load_contrib('websocket')" -t test/contrib/websocket.uts

+ Syntax check
= Import the WebSocket layer
from scapy.contrib.websocket import *

+ WebSocket protocol test
= Packet instantiation
pkt = WebSocket(wsPayload=b"Hello, world!", opcode="text", mask=True, maskingKey=0x11223344)
pkt.show()
import binascii
print(binascii.hexlify(bytes(pkt)))
assert pkt.wsPayload == b"Hello, world!"
assert pkt.mask == True
assert pkt.maskingKey == 0x11223344


= Packet dissection
raw = b'\x01\x0dHello, world!'
pkt = WebSocket(raw)
pkt.show()

assert pkt.fin == 0
assert pkt.rsv == 0
assert pkt.opcode == 0x1
assert pkt.mask == False
assert pkt.payloadLen == 13
assert pkt.wsPayload == b'Hello, world!'

= Dissect masked packet
raw = b'\x01\x8d\x11\x22\x33\x44\x59\x47\x5f\x28\x7e\x0e\x13\x33\x7e\x50\x5f\x20\x30'
pkt = WebSocket(raw)
pkt.show()

assert pkt.fin == 0
assert pkt.rsv == 0
assert pkt.opcode == 0x1
assert pkt.mask == True
assert pkt.payloadLen == 13
assert pkt.wsPayload == b'Hello, world!'

= Session with compression

bind_layers(TCP, WebSocket, dport=5000)
bind_layers(TCP, WebSocket, sport=5000)

from scapy.sessions import TCPSession

filename = scapy_path("/test/pcaps/websocket_compressed_session.pcap")
pkts = sniff(offline=filename, session=TCPSession)

assert len(pkts) == 13

assert pkts[7][WebSocket].wsPayload == b'Hello'
assert pkts[8][WebSocket].wsPayload == b'"Hello"'
assert pkts[10][WebSocket].wsPayload == b'Hello2'
assert pkts[11][WebSocket].wsPayload == b'"Hello2"'
Binary file added test/pcaps/websocket_compressed_session.pcap
Binary file not shown.

0 comments on commit 68670b4

Please sign in to comment.