blog.mkiesel

häcks und so

25. Oct 2023 - Exfiltration via WebSerial

COMfiltrat0r - Stealing Data Like It's 1995

Table of Contents
Table of Contents

During a DLP↗ (Data Loss Prevention) audit, I had to find a way to physically copy files off a Windows computer that had all forms of USB storage blocked. While enumerating USB policies, I noticed that, from my black-box perspective, only policies against storage devices were in place. Using a microcontroller and a neat browser feature called WebSerial, I found a way to exfiltrate documents without installing any additional software. This post tells the journey of that discovery.

COMfiltrat0r Logo

This is the directors cut of cyllective’s ‘COMfiltrat0r - Exfiltration via WebSerial’↗.

 USB Enumeration

I only knew initially that USB storage should be blocked by something. First, I tried a USB drive, an Android phone in MTP mode, and a microSD card via a USB reader to check which storage devices were affected. All showed up in the device manager but were not accessible in the file explorer or via PowerShell.

Since they showed up in the device manager, they are blocked on a software level, not a BIOS/hardware level. To enumerate which additional policies were in place, I plugged in various other USB devices like a NIC, a USB-C docking station, an audio card, and a keyboard and mouse. All of those devices were recognized and useable. I then concluded that all storage devices were blocked, and all other devices were probably allowed.

In this company’s context, this makes sense. Employees take devices home and plug in various docking stations, keyboards, and network devices. To allow-list only specific devices would be an administrative nightmare.

 IDs and Classes

But how can some USB devices be blocked while others are allowed? Each USB device has a 16-bit vendor ID and a 16-bit device ID. Those IDs are used as an identifier so the OS knows which drivers to use for each device or if an ID-specific device rule should be triggered.

A device rule could, therefore, block all USB keyboards from a vendor/manufacturer that is different from the one used inside a company. However, this is not considered bulletproof (or, as mentioned, maintainable) as microcontrollers are often able to spoof/fake vendor and product IDs within certain limitations. Generally speaking, blocking based on IDs is not a good practice unless you’re blocking for example, IDs for known script-kiddy-ready devices like a Rubber Ducky or a Flipper Zero.

The better approach is then to block whole classes↗ of devices. USB device classes are groups to which a USB device belongs based on its function. Some of those classes are

The policy I encountered probably blocks all “Mass Storage” devices.

 COMmunication

With mass storage devices blocked, I looked for another way to transfer files, and my mind went straight to serial ports↗. On Windows, serial ports get registered as COM ports. (ex. COM3)

Using a serial terminal, you can talk to a COM port and the connected device over the RS-232 protocol↗. Since computers today no longer have built-in serial ports, USB to RS-232 adapters are pretty common. The simplified flow looks like this:

Communication Diagram

Serial communication is still used today for enterprise network equipment or in the professional AV world to control, for example, projectors. You may have seen the old DB9 connector or the Cisco light-blue serial to RJ-45 cable before.

Classic Serial (DB9) CableCisco Serial Cable
Classic Serial (DB9) CableCisco Serial Cable

This would be a viable option to exfiltrate data but would require two parts:

 Just use Software X

An easy solution for the first problem would be to install a serial terminal like PuTTY↗ and I would have a nice way of communicating with a serial device. There are a few problems here:

On the other hand, PowerShell has methods to talk to serial ports. Files could be encoded to base64 or hex for easy transfer. But then I’m executing a PowerShell script, which, in the best cases, should be blocked anyway or, at least, should trigger an alarm.

 The Browser can do What?

After searching for a bit, I came across WebUSB↗.

The WebUSB API provides a way to expose non-standard Universal Serial Bus (USB) compatible devices services to the web, to make USB safer and easier to use.

It came to my mind that I used this feature in the past. Some manufacturers provide web-based flashing tools for their sensors or microcontrollers. Those flashers can use this API to access hardware devices that are plugged in via USB directly. It’s really user-friendly since the user just needs to have the proper drivers installed. As a test setup, I programmed an Arduino-based microcontroller to reply via serial to any newline-terminated string with “hello world”. However, I quickly ran into a problem regarding USB endpoints.

Endpoints are, in a way, virtual cables/channels from the computer to the USB device. A device can have various endpoints for different functions. Each endpoint has a unique address. Endpoint 0, for example, is the control endpoint and is used during the setup of the device for exchanging IDs and negotiation speed. Please check out Ben Eater’s video↗ for way more information on the USB protocol itself.

The driver that Windows loaded kept registering the “serial input” endpoint of the microcontroller for itself. This meant that the browser would always get “Access denied” when trying to open the endpoint. Modifying the driver was not an option since the final PoC should work on an out-of-the-box Windows machine. Reprogramming the USB implementation to accept serial via multiple endpoints seemed a bit daunting after a quick online search. Luckily, while searching for example code on WebUSB, I stumbled upon WebSerial↗.

WebSerial is a way to directly read and write to serial ports registered/handled by the OS. Using a bit of JavaScript, those ports can be accessed. This was exactly what I needed.

// from https://developer.chrome.com/en/articles/serial/#open-port

// Prompt user to select any serial port.
const port = await navigator.serial.requestPort();

// Wait for the serial port to open.
await port.open({ baudRate: 9600 });

const reader = port.readable.getReader();

// Listen to data coming from the serial device.
while (true) {
  const { value, done } = await reader.read();
  if (done) {
    // Allow the serial port to be closed later.
    reader.releaseLock();
    break;
  }
  // value is a Uint8Array.
  console.log(value);
}

 Hardware

With the software side figured out, I still needed hardware that fulfilled the criteria mentioned above. Since I had a Raspberry Pi Pico↗ lying around, I went with that as well as an SPI microSD card adapter.

The Pico still had MicroPython↗ installed from a past experiment. Despite its young age, the community around MicroPython is fairly big, so I went with that rather than the official C/C++ SDK. Also, the Pico’s USB to Serial adapter can be accessed with a default Windows installation and does not require the installation of drivers.

 No REPL

When you plug in a microcontroller flashed with MicroPython, you get a Python shell via the serial port registered on the computer. This “REPL” prompt can be used to execute commands line by line, like the regular Python prompt on a computer. However, you can write custom functions and save those as Python files onto the microcontroller, and then access those when importing Python libraries. If you save a Python script as main.py onto the device, this script will be executed/imported once the microcontroller is plugged in/receives power.

My first attempt was a custom function that look like this:

import ubinascii

def sd_write(name: str, content: str):
    with open(name, "wb") as f:
        f.write(ubinascii.unhexlify(content))

I would then call this function by writing sd_write(FILENAME, HEXCONTENT) to the serial port via WebSerial. This was very unstable. Sometimes, the REPL prompt was there, and sometimes it did not start upon plugging in the Pico. To get around this, I simply used input() in main.py. This made the Pico wait for input upon power-up. (which is whatever I sent to the COM port) Also, this stops the REPL prompt from starting/failing.

Pico PoC

 Chunks

There was also the problem of transferring bigger files. input() in MicroPython only accepts 10'000 characters at once. To get around this, a way to send a file in chunks was needed. After lots of experimenting with data formats later, I settled on FILENAME;HEXCHUNK\r\n. With this, I never have to signal the beginning and end of a file, as the Python script will just open and close the specified file on each chunk. This reduces performance but works really reliably.

while True:
    data = input()
    if ";" in data:
        filename = data.split(";")[0]
        chunk_hex = data.split(";")[1]

        if file_exists(filename):
            file = open(filename, "ab")
        else:
            file = open(filename, "wb")

        chunk = ubinascii.unhexlify(chunk_hex)
        file.write(chunk)
        file.close()

On the JavaScript side, I needed a way to know when the device has finished processing the chunk so that the browser can send the next one. For this, I added a small async function to gohai/p5.webserial↗.

// Will only return once a message from the serial port is received
async function waitForSerialAck() {
    while (true) {
        if (!(serial)) {
            // Port was closed in the meantime
            return false;
        }

        if (serial.available() > 0) {
            // There are bytes to read, read all
            let message = String(serial.read());
            if (message != "") {
                // Got something
                //console.log(`COM: ${message}`);
                return true;
            }
        }

        // Poll every 100 ms
        await new Promise(resolve => setTimeout(resolve, 100));
    }
}

 Demo

 Defense

There are two main ways of blocking this exfiltration technique:

Technically, this technique could also work over other browser “hardware” APIs like Midi or VR. It’s probably for the best just to disable every feature that is not explicitly needed in a company context.

 Wrapping up

As a final step, I made it customer-friendly by switching from the Pico to a Teensy 4.1↗. With its included microSD card port, it only required a micro-USB cable and did not look as sketchy as the breadboard Pico. The Teensy was also detected and accessible on a default installation of Windows.

This was a cool project, during which I touched on so many new and interesting things. And the best part was that it was an actual finding inside the audit from which the idea was born. (even if, during the audit, I only demonstrated a small PoC)

The final code can be found on GitHub↗.