/dev/posts/

Firefox DoH DNS rebinding protection bypass using IPv4-mapped addresses

Published:

Updated:

I found that the filtering of private IPv4 addresses in the DNS-over-HTTPS (DoH) implementation of Firefox could by bypassed. This is CVE-2020-26961 and Mozilla bug 1672528. It has been fixed in Firefox 83, Firefox ESR 78.5 and Thunderbird 78.5.

Summary

When using Firefox builtin support for DoH[1], private IPv4 addresses (RFC1918) are rejected (ignored) by default (network.trr.allow-rfc1918=false). This protection can prevent some form of browser-based attacks of machines located on the LAN (DNS rebinding attacks). I found this protection could by bypassed by using a IPv4-mapped IPv6 address (eg. ::ffff:192.168.1.254).

Wording from the CVE entry:

When DNS over HTTPS is in use, it intentionally filters RFC1918 and related IP ranges from the responses as these do not make sense coming from a DoH resolver. However when an IPv4 address was mapped through IPv6, these addresses were erroneously let through, leading to a potential DNS Rebinding attack. This vulnerability affects Firefox < 83, Firefox ESR < 78.5, and Thunderbird < 78.5.

Impact

DNS rebinding is a technique which exploits the user browser for attacking other services. It is especially powerful against many LAN-only (or localhost-only) services such as routers, smart TVs, etc. These services are often designed under the assumption that they are only reachable by local machines. They often (sometimes for convenience) expose services without authentication or lack proper security.

Some network operators block private IPv4 addresses in DNS responses coming of of their recursive resolvers as a protection against DNS rebinding attacks. When Firefox uses its own DoH implementation, any DNS rebinding protection implemented by the network operator (or by the system) would be bypassed. However, Firefox implements its own DNS-rebinding protection in this case (by default i.e. when network.trr.allow-rfc1918=false).

As the DNS-rebinding protection implemented in Firefox DoH could easily be bypassed, enabling DoH in Firefox could open the local services to DNS-rebinding attacks.

Example

In order to test this, I have created some domaine name records:

wat4   A    192.168.1.254
wat6   AAAA ::ffff:192.168.1.254

Let's check them on the command-line interface:

dig @1.1.1.1 +short A wat4.urdhr.fr
# => 192.168.1.254
dig @1.1.1.1 +short AAAA wat6.urdhr.fr
# => ::ffff:192.168.1.254

We are going to configure Firefox to use a DoH resolver (in about:config):

# Force DoH:
network.trr.mode=5
# Choose a specific DoH resolver:
network.trr.custom_uri=https://cloudflare-dns.com/dns-query
# Disabled private IPv4 addresses from DoH (this is the default):
network.trr.allow-rfc1918=false

Le'ts first check that the DoH resolver we are going to use actually resolves our domains. We can use the dnspython library for this:

import dns.query
import dns.message
resolver = "https://cloudflare-dns.com/dns-query"
dns.query.https(dns.message.make_query("wat4.urdhr.fr", "A"), resolver).answer
# => [<DNS wat4.urdhr.fr. IN A RRset: [<192.168.1.254>]>]
dns.query.https(dns.message.make_query("wat6.urdhr.fr", "AAAA"), resolver).answer
# => [<DNS wat6.urdhr.fr. IN AAAA RRset: [<::ffff:192.168.1.254>]>]

Now let's try using Firefox:

We have been able to bypass the filtering of private IPv4 addresses by using a private IPv4-mapped IPv6 address.

Relevant Code Snippets

The process is as follow:

  1. the wat6.urdhr.fr domain name is resolved to IPv6 ffff:192.168.1.254;
  2. this IPv6 address is not considered a private IP address by NetAddr::IsIPAddrLocal();
  3. this IPv6 address generates a AF_INET6 address in DOHresp::Add();
  4. this AF_INET6 address is passed to the kernel;
  5. the kernel connects to 192.168.1.256.

The DOHresp::Add() method is responsible for building a NetAddr object from DNS bytes:

nsresult DOHresp::Add(uint32_t TTL, unsigned char* dns, unsigned int index,
                      uint16_t len, bool aLocalAllowed) {
  NetAddr addr;
  if (4 == len) {
    // IPv4
    addr.inet.family = AF_INET;
    addr.inet.port = 0;  // unknown
    addr.inet.ip = ntohl(get32bit(dns, index));
  } else if (16 == len) {
    // IPv6
    addr.inet6.family = AF_INET6;
    addr.inet6.port = 0;      // unknown
    addr.inet6.flowinfo = 0;  // unknown
    addr.inet6.scope_id = 0;  // unknown
    for (int i = 0; i < 16; i++, index++) {
      addr.inet6.ip.u8[i] = dns[index];
    }
  } else {
    return NS_ERROR_UNEXPECTED;
  }

  if (addr.IsIPAddrLocal() && !aLocalAllowed) {

    return NS_ERROR_FAILURE;
  }

  // While the DNS packet might return individual TTLs for each address,
  // we can only return one value in the AddrInfo class so pick the
  // lowest number.
  if (mTtl < TTL) {
    mTtl = TTL;
  }

  if (LOG_ENABLED()) {
    char buf[128];
    addr.ToStringBuffer(buf, sizeof(buf));
    LOG(("DOHresp:Add %s\n", buf));
  }
  mAddresses.AppendElement(addr);
  return NS_OK;
}

The aLocalAllowed parameter of this method is controlled by the network.trr.allow-rfc1918 configuration.

The important snippet is:

if (addr.IsIPAddrLocal() && !aLocalAllowed) {
  return NS_ERROR_FAILURE;
}

The NetAddr::IsIPAddrLocal() method checks whether the IP address is a private one:

bool NetAddr::IsIPAddrLocal() const {
  const NetAddr* addr = this;

  // IPv4 RFC1918 and Link Local Addresses.
  if (addr->raw.family == AF_INET) {
    uint32_t addr32 = ntohl(addr->inet.ip);
    if (addr32 >> 24 == 0x0A ||    // 10/8 prefix (RFC 1918).
        addr32 >> 20 == 0xAC1 ||   // 172.16/12 prefix (RFC 1918).
        addr32 >> 16 == 0xC0A8 ||  // 192.168/16 prefix (RFC 1918).
        addr32 >> 16 == 0xA9FE) {  // 169.254/16 prefix (Link Local).
      return true;
    }
  }
  // IPv6 Unique and Link Local Addresses.
  if (addr->raw.family == AF_INET6) {
    uint16_t addr16 = ntohs(addr->inet6.ip.u16[0]);
    if (addr16 >> 9 == 0xfc >> 1 ||    // fc00::/7 Unique Local Address.
        addr16 >> 6 == 0xfe80 >> 6) {  // fe80::/10 Link Local Address.
      return true;
    }
  }
  // Not an IPv4/6 local address.
  return false;
}

We see that this method returns false for IPv4-mapped IPv6 addresses. The DOHresp::Add() method then appends this IP address to the list of available IP addresses.

Fix

This bug was fixed with:

static bool isLocalIPv4(uint32_t networkEndianIP) {
  uint32_t addr32 = ntohl(networkEndianIP);
  if (addr32 >> 24 == 0x0A ||    // 10/8 prefix (RFC 1918).
      addr32 >> 20 == 0xAC1 ||   // 172.16/12 prefix (RFC 1918).
      addr32 >> 16 == 0xC0A8 ||  // 192.168/16 prefix (RFC 1918).
      addr32 >> 16 == 0xA9FE) {  // 169.254/16 prefix (Link Local).
    return true;
  }
  return false;
}

bool NetAddr::IsIPAddrLocal() const {
  const NetAddr* addr = this;

  // IPv4 RFC1918 and Link Local Addresses.
  if (addr->raw.family == AF_INET) {
    return isLocalIPv4(addr->inet.ip);
  }
  // IPv6 Unique and Link Local Addresses.
  // or mapped IPv4 addresses
  if (addr->raw.family == AF_INET6) {
    uint16_t addr16 = ntohs(addr->inet6.ip.u16[0]);
    if (addr16 >> 9 == 0xfc >> 1 ||    // fc00::/7 Unique Local Address.
        addr16 >> 6 == 0xfe80 >> 6) {  // fe80::/10 Link Local Address.
      return true;
    }
    if (IPv6ADDR_IS_V4MAPPED(&addr->inet6.ip)) {
      return isLocalIPv4(IPv6ADDR_V4MAPPED_TO_IPADDR(&addr->inet6.ip));
    }
  }

  // Not an IPv4/6 local address.
  return false;
}

Story

While scanning the recent CVE entries, I found an interesting report about a DNS rebinding protection bypass for FRITZ!Box, CVE-2020-26887. The local DNS resolver of the FRITZ!Box has a protection against DNS rebinding attack. It works by rejecting DNS responses which include private IPv4 addresses (eg. 192.168.1.254). However, this protection could be bypassed by using a private IPv4-mapped IPv6 addresses (eg. ::ffff:192.168.1.254).

I first checked if we could use this technique to bypass the DNS-rebinding protection of the Freebox as well. This approach does not work for the Freebox lccal DNS resolver: private IPv4-mapped IPv6 addresses are filtered as well.

This led me to check how Firefox would behave when receiving IPv4-mapped IPv6 addresses from its own DoH implementation.

Timeline

References


  1. In the Firefox codebase and documentation, this support is called Trusted Recursive Resolver (TRR). ↩︎