Streamlining Websocket Pentesting with wsrepl
18 Jul 2023 - Posted by Andrew KonstantinovIn an era defined by instant gratification, where life zips by quicker than a teenager’s TikTok scroll, WebSockets have evolved into the heartbeat of web applications. They’re the unsung heroes in data streaming and bilateral communication, serving up everything in real-time, because apparently, waiting is so last century.
However, when tasked with pentesting these WebSockets, it feels like you’re juggling flaming torches on a unicycle, atop a tightrope! Existing tools, while proficient in their specific realms, are much like mismatched puzzle pieces – they don’t quite fit together, leaving you to bridge the gaps. Consequently, you find yourself shifting from one tool to another, trying to manage them simultaneously and wishing for a more streamlined approach.
That’s where https://github.com/doyensec/wsrepl comes to the rescue. This tool, the latest addition to Doyensec’s security tools, is designed to simplify auditing of websocket-based apps. wsrepl
strikes a much needed balance by offering an interactive REPL interface that’s user-friendly, while also being conveniently easy to automate. With wsrepl
, we aim to turn the tide in websocket pentesting, providing a tool that is as efficient as it is intuitive.
The Doyensec Challenge
Once upon a time, we took up an engagement with a client whose web application relied heavily on WebSockets for soft real-time communication. This wasn’t an easy feat. The client had a robust bug bounty policy and had undergone multiple pentests before. Hence, we were fully aware that the lowest hanging fruits were probably plucked. Nevertheless, as true Doyensec warriors (‘doyen’ - a term Merriam-Webster describes as ‘a person considered to be knowledgeable or uniquely skilled as a result of long experience in some field of endeavor’), we were prepared to dig deeper for potential vulnerabilities.
Our primary obstacle was the application’s custom protocol for data streaming. Conventional wisdom among pentesters suggests that the most challenging targets are often the ones most overlooked. Intriguing, isn’t it?
The Quest for the Perfect Tool
The immediate go-to tool for pentesting WebSockets would typically be Burp Suite. While it’s a heavyweight in web pentesting, we found its implementation of WebSockets mirrored HTTP requests a little bit too closely, which didn’t sit well with near-realtime communications.
Sure, it does provide a neat way to get an interactive WS session, but it’s a bit tedious - navigating through ‘Upgrade: websocket’, hopping between ‘Repeater’, ‘Websocket’, ‘New WebSocket’, filling in details, and altering HTTP/2
to HTTP/1.1
. The result is a decent REPL but the process? Not so much.
Don’t get me wrong, Burp does have its advantages. Pentesters already have it open most times, it highlights JSON or XML, and integrates well with existing extensions. Despite that, it falls short when you have to automate custom authentication schemes, protocols, connection tracking, or data serialization schemes.
Other tools, like websocketking.com and hoppscotch.io/realtime/websocket, offer easy-to-use and aesthetically pleasing graphical clients within the browser. However, they lack comprehensive options for automation. Tools like websocket-harness and WSSiP bridge the gap between HTTP and WebSockets, which can be useful, but again, they don’t offer an interactive REPL for manual inspection of traffic.
Finally, we landed on websocat, a netcat inspired command line WebSockets client that comes closest to what we had in mind. While it does offer a host of features, it’s predominantly geared towards debugging WebSocket servers, not pentesting.
wsrepl
: The WebSockets Pentesting Hero
Enter wsrepl, born out of necessity as our answer to the challenges we faced. It is not just another pentesting tool, but an agile solution that sits comfortably in the middle - offering an interactive REPL experience while also providing simple and convenient path to automation.
Built with Python’s fantastic TUI framework Textual, it enables an accessible experience that is easy to navigate both by mouse and keyboard. That’s just scratching the surface though. Its interoperability with curl’s arguments enables a fluid transition from the Upgrade request in Burp to wsrepl
. All it takes is to copy a request through ‘Copy as curl command’ menu option and replace curl
with wsrepl
.
On the surface, wsrepl
is just like any other tool, showing incoming and outgoing traffic with the added option of sending new messages. The real magic however, is in the details. It leaves nothing to guesswork. Every hexadecimal opcode is shown as per RFC 6455, a feature that could potentially save you from many unnecessary debugging hours.
A lesson learned
Here’s an anecdote to illustrate this point. At the beginning of our engagement with WebSockets, I wasn’t thoroughly familiar with the WebSocket RFC and built my understanding based on what Burp showed me. However, Burp was only displaying text messages, obscuring message opcodes and autonomously handling pings without revealing them in the UI. This partial visibility led to some misconceptions about how WebSockets operate. The developers of the service we were testing seemingly had the same misunderstanding, as they implemented ping traffic using 0x1
- text type messages. This caused confusion and wasted time when my scripts kept losing the connection, even though the traffic appeared to align with my Burp observations.
To avoid similar pitfalls, wsrepl
is designed to give you the whole picture, without any hidden corners. Here’s a quick rundown of WebSocket opcodes defined in RFC6544 that you can expect to see in wsrepl
:
Opcode | Description |
---|---|
0x0 | Continuation Frame |
0x1 | Text Frame |
0x2 | Binary Frame |
0x8 | Connection Close |
0x9 | Ping |
0xA | Pong (must carry the same payload as the corresponding Ping frame) |
Contrary to most WebSocket protocols that mainly use 0x1
type messages, wsrepl
accompanies all messages with their opcodes, ensuring full transparency. We’ve intentionally made the decision not to conceal ping traffic by default, although you have the choice to hide them using the --hide-ping-pong
option.
Additionally, wsrepl
introduces the unique capability of sending ‘fake’ ping messages, that use the 0x1
message frame. Payloads can be defined with options --ping-0x1-payload
and --pong-0x1-payload
, and the interval controlled by --ping-0x1-interval
. It also supports client-induced ping messages (protocol level, 0x9
), even though typically this is done by the server: --ping-interval
.
It’s also noteworth that wsrepl
incorporates an automatic reconnection feature in case of disconnects. Coupled with granular ping control, these features empower you to initiate long-lasting and stable WebSocket connections, which have proven useful for executing certain attacks.
Automation Made Simple with wsrepl
Moreover, wsrepl
is crafted with a primary goal in mind: enabling you to quickly transition into WebSocket automation. To do this, you just need to write a Python plugin, which is pretty straightforward and, unlike Burp, feels quintessentially pythonic.
from wsrepl import Plugin
MESSAGES = [
"hello",
"world"
]
class Demo(Plugin):
"""Demo plugin that sends a static list of messages to the server."""
def init(self):
self.messages = MESSAGES
It’s Python, so really, the sky’s the limit. For instance, here is how to send a HTTP request to acquire the auth token, and then use it to authenticate with the WebSocket server:
from wsrepl import Plugin
from wsrepl.WSMessage import WSMessage
import json
import requests
class Demo(Plugin):
"""Demo plugin that dynamically acquires authentication token."""
def init(self):
# Here we simulate an API request to get a session token by supplying a username and password.
# For the demo, we're using a dummy endpoint "https://hb.cran.dev/uuid" that returns a UUID.
# In a real-life scenario, replace this with your own authentication endpoint and provide necessary credentials.
token = requests.get("https://hb.cran.dev/uuid").json()["uuid"]
# The acquired session token is then used to populate self.messages with an authentication message.
# The exact format of this message will depend on your WebSocket server requirements.
self.messages = [
json.dumps({
"auth": "session",
"sessionId": token
})
]
The plugin system is designed to be as flexible as possible. You can define hooks that are executed at various stages of the WebSocket lifecycle. For instance, you can use on_message_sent
to modify messages before they are sent to the server, or on_message_received
to parse and extract meaningful data from the server’s responses. The full list of hooks is as follows:
Customizing the REPL UI
The true triumph of wsrepl
lies in its capacity to automate even the most complicated protocols. It is easy to add custom serialization routines, allowing you to focus on the stuff that matters.
Say you’re dealing with a protocol that uses JSON for data serialization, and you’re only interested in a single field within that data structure. wsrepl
allows you to hide all the boilerplate, yet preserve the option to retrieve the raw data when necessary.
from wsrepl import Plugin
import json
from wsrepl.WSMessage import WSMessage
class Demo(Plugin):
async def on_message_sent(self, message: WSMessage) -> None:
# Grab the original message entered by the user
original = message.msg
# Prepare a more complex message structure that our server requires.
message.msg = json.dumps({
"type": "message",
"data": {
"text": original
}
})
# Short and long versions of the message are used for display purposes in REPL UI.
# By default they are the same as 'message.msg', but here we modify them for better UX.
message.short = original
message.long = message.msg
async def on_message_received(self, message: WSMessage) -> None:
# Get the original message received from the server
original = message.msg
try:
# Try to parse the received message and extract meaningful data.
# The exact structure here will depend on your websocket server's responses.
message.short = json.loads(original)["data"]["text"]
except:
# In case of a parsing error, let's inform the user about it in the history view.
message.short = "Error: could not parse message"
# Show the original message when the user focuses on it in the UI.
message.long = original
In conclusion, wsrepl
is designed to make your WebSocket pentesting life easier. It’s the perfect blend of an interactive REPL experience with the ease of automation. It may not be the magical solution to every challenge you’ll face in pentesting WebSockets, but it is a powerful tool in your arsenal. Give it a try and let us know your experiences!