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 (RFC 1918) 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 RFC 1918 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:
http://wat4.urdhr.fr
is not reachable because the private IP address is rejected by Firefox (network.trr.allow-rfc1918=false
);http://wat6.urdhr.fr
is reachable (assuming a web server is running on this IP address and port).
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:
- the
wat6.urdhr.fr
domain name is resolved to IPv6 ffff:192.168.1.254; - this IPv6 address is not considered a private IP address by
NetAddr::IsIPAddrLocal()
; - this IPv6 address generates a
AF_INET6
address inDOHresp::Add()
; - this
AF_INET6
address is passed to the kernel; - 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 RFC 1918 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 RFC 1918 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
- 2020-10-22, Reported
- 2020-10-23, Confirmed
- 2020-11-17, Release of fix and security advisories
- 2020-12-01, Bounty awarded ($2500)
References
- CVE-2020-26961
- Firefox bug 1672528
- Firefox 83 Security Advisory
- Firefox ESR 78.5 Security Advisory
- Thunderbird 78.5 Security Advisory .
- RFC 4291, IP Version 6 Addressing Architecture and its section 2.5.5.2, IPv4-Mapped IPv6 Address
- RFC 3596, DNS Extensions to Support IP Version 6
- FRITZ!Box DNS Rebinding Protection Bypass aka CVE-2020-26887
- When TLS Hacks You, combines DNS-rebinding attacks with TLS session based injections
In the Firefox codebase and documentation, this support is called Trusted Recursive Resolver (TRR). ↩︎