DNS over HTTPS (+ModSecurity WAF)
One of the problems with DNS is that a query is sent over an unencrypted connection, anyone listening to the packets knows the websites you visit.
One of the main security problems with DNS is that a query is sent over an unencrypted connection. That means that anyone listening to packets between you and your DNS server could know what websites you are visiting, even if the website that you are browsing is secured with HTTPS. In this article I'll show you how I was able to use the CloudFlare as your DNS over a HTTPS resolver, and also how to filter out phishing domains using libModSecurity at the same time.
Another problem you can solve with DNSSEC is the Man-In-The-Middle scenario, when unsecured it's easy to change DNS answers and forward visitors to a phishing site and unfortunately only a small percentage of website domains use DNSSEC.
Can DNS over HTTPS be a solution?
DNS over HTTPS (DoH) is a protocol for performing remote DNS resolution via the HTTPS protocol. A goal of the method is to increase user privacy and security by preventing eavesdropping and manipulation of DNS data by Man-In-The-Middle attacks. DNS over HTTPS is a standard as RFC 8484 under the IETF. It uses HTTP/2 and HTTPS and supports "wire format" DNS response data, as returned in existing UDP responses, in an HTTPS payload with the application/dns-message
MIME type.
DoH could let the user send an unencrypted DNS query to his localhost (for example to 127.0.0.1:53/udp) where there's something similar to a DNS cache server that takes his query and forwards it to CloudFlare 1.1.1.1 DNS via HTTPS. In this case, the unencrypted DNS query stay away from prying eyes (and noses) and we can even get advantages from the HTTP protocol in order to filter out, blacklist and protect the user's browsing. If we are able to transform a standard DNS query to an encrypted HTTPS request, why don't we add some filters on it? The open source WAF ModSecurity is useful to make rules on what users can and can not query!
Sure, we can add a "Pi-Hole" blacklist in order to block users to resolve certain "bad guy" hostnames, but we can do more than this! As you know, many phishing websites usually choose a hostname to trick users, like login.google.com.access.pure-evil-phishing.xyz
with a valid ssl certificate from letsencrypt to polish the illusion.
I want to test and see if I can send a DNS over HTTPS query to an Nginx with ModSecurity and write a rule that says: "if DNS query contains google.com but google
is not the 2nd level domain and com
is not the top level domain, then block it!".
What I'm going to show you:
CloudFlare HTTPS resolver
The easiest way to query for a domain name via HTTPS to CloudFlare 1.1.1.1 is to send a simply GET request to https://cloudflare-dns.com with two parameters: name
the domain name to resolve, and type
the record type you want. Don't forget to specify application/dns-json
on the Accept
header:
$ http 'https://cloudflare-dns.com/dns-query?name=google.com&type=A' 'accept:application/dns-json'
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
CF-RAY: 4a6750253a36be52-MXP
Connection: keep-alive
Content-Length: 203
Content-Type: application/dns-json
Date: Sat, 09 Feb 2019 15:19:13 GMT
Expect-CT: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
Server: cloudflare
Vary: Accept-Encoding
cache-control: max-age=36
{
"AD": false,
"Answer": [
{
"TTL": 36,
"data": "216.58.205.78",
"name": "google.com.",
"type": 1
}
],
"CD": false,
"Question": [
{
"name": "google.com.",
"type": 1
}
],
"RA": true,
"RD": true,
"Status": 0,
"TC": false
}
Easy, isn't it? As an alternative you can even use the DNS wireformat (that will be useful when we'll create the ModSecurity rule) but it requires you to know a little bit deep the DNS protocol.
echo -n 'q80BAAABAAAAAAAAA3d3dwZnb29nbGUDY29tAAABAAE=' | base64 -d | \
curl -s -H 'content-type: application/dns-message' \
--data-binary @- https://cloudflare-dns.com/dns-query | \
hexdump -C
00000000 ab cd 81 80 00 01 00 01 00 00 00 01 03 77 77 77 |.............www|
00000010 06 67 6f 6f 67 6c 65 03 63 6f 6d 00 00 01 00 01 |.google.com.....|
00000020 c0 0c 00 01 00 01 00 00 00 ab 00 04 d8 3a cd 64 |.............:.d|
00000030 00 00 29 05 ac 00 00 00 00 00 00 |..)........|
0000003b
Anyway, as you might know, the domain name inside a DNS query is represented as a sequence of labels that consists of a length octet followed by that number of octets. Let's take, for example, www.google.com: inside a query it becomes:\x03www\x06google\x03com
then others 2 bytes that represents the record type (for example A \x00\x01
or NS \x00\x02
etc...) and, at the end, others 2 bytes for the class (IN \x00\x01
). The domain name terminates always with the zero length octet for the null label of the root.
So, we can easily create the sequence in bash with something like:
echo -ne '\xab\xcd\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00'\
'\x03www\x06google\x03com\x00'\
'\x00\x01\x00\x01' | \
hexdump -C
00000000 ab cd 01 00 00 01 00 00 00 00 00 00 03 77 77 77 |.............www|
00000010 06 67 6f 6f 67 6c 65 03 63 6f 6d 00 00 01 00 01 |.google.com.....|
00000020
I don't want to go deep too much on DNS protocol, but if you're wondering what's the meaning of the first line \xab\xcd\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00
it is just the header that define some useful information like the ID, the flag "i'm a query", etc...
Now you need to send it as the body of a POST request. You can pipe a curl
syntax to the previous command like this:
echo -ne '\xab\xcd\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x06google\x03com\x00\x00\x01\x00\x01' | \
curl -s -H 'content-type: application/dns-message' \
--data-binary @- \
https://cloudflare-dns.com/dns-query | \
hexdump -C
00000000 ab cd 81 80 00 01 00 01 00 00 00 01 03 77 77 77 |.............www|
00000010 06 67 6f 6f 67 6c 65 03 63 6f 6d 00 00 01 00 01 |.google.com.....|
00000020 c0 0c 00 01 00 01 00 00 00 34 00 04 d8 3a cd 44 |.........4...:.D|
00000030 00 00 29 05 ac 00 00 00 00 00 00 |..)........|
0000003b
As you can see, CloudFlare answer that the record A of www.google.com is 4 bytes long 0x04
and the IP is 0xd83acd44
The CloudFlare DoH client
CloudFlare has open sourced his DoH client that you can find at https://developers.cloudflare.com/argo-tunnel/downloads/ once installed you can just run sudo cloudflared proxy-dns
to get started:
Assuming that you already have a running Nginx listening on HTTPS and with HTTP/2 active (or a CloudFlare free account) and a working libModSecurity inside it (if not, please take a look at https://www.nginx.com/blog/compiling-and-installing-modsecurity-for-open-source-nginx/) we can tell to cloudflared
to forward all requests to our website (I'll use my website doh.rev3rse.it) with:
# ./cloudflared proxy-dns --address 127.0.0.1 --upstream https://doh.rev3rse.it/dns-query
INFO[0000] Adding DNS upstream url="https://doh.rev3rse.it/dns-query"
INFO[0000] Starting DNS over HTTPS proxy server addr="dns://127.0.0.1:53"
INFO[0000] Starting metrics server addr="127.0.0.1:44992"
Now we just need to handle all requests coming from our cloudflared
and forward them to https://cloudflare-dns.com. With Nginx is really easy:
location ~* /dns-query {
modsecurity on;
modsecurity_rules_file conf/modsecurity.conf;
proxy_redirect off;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host "cloudflare-dns.com";
proxy_pass https://cloudflare-dns.com:443;
}
after reloading the nginx configuration, it works like a charm!
My libModSecurity logs every request in JSON, and that logs are collected by logstash and sent to elasticsearch. One of the first coolest thing of DoH is that I can immediately see all queries on my Kibana ๐
ModSecurity DNS Filter
Now the fun part, let's create a ModSecurity rule that blocks DNS query for something like accounts.google.com.signin.rev3rse.it
but allows something like login.google.com
SecRule REQUEST_HEADERS:Content-Type "application/dns-udpwireformat" \
"id:31,\
pass,\
phase:2,\
nolog,\
chain"
SecRule REQUEST_BODY "@rx (\x06google\x03com(?!\x00).*)" \
"id:31,\
phase:2,\
log,\
capture,\
t:none,\
msg:'Blocked DNS Query',\
logdata:'%{tx.0}'"
As you can see in the two rules above, at fist I check if the Content-Type is application/dns-udpwireformat
then I look for something different then \x00 at the end of com
TLD. This regular expression should match everything like google.com.something and should allow something.google.com. Let's do a test:
IT COULD WORK! ๐ The first query is sent to the Google DNS 8.8.8.8 and, as you can see, it returns the correct value for the accounts.google.com.signin.rev3rse.it
hostname. The second query is sent to my local DNS server (cloudflared) and forwarded to my Nginx + ModSecurity and, as you can see, Nginx reply with a 403 Forbidden response status and I haven't received any response from my DNS server!
Conclusion
If you are thinking that DoH is not the best approach in terms of performance you might be surprised to learn that it passes Mozilla performance test! Their blog shows that "During July 2018, about 25,000 Firefox Nightly 63 users who had previously agreed to be part of Nightly experiments participated in some aspect of this study. Cloudflare operated the DoH servers [...] The experiment generated over a billion DoH transactions and is now closed."
"Using HTTPS with a cloud service provider had only a minor performance impact on the majority of non-cached DNS queries as compared to traditional DNS. Most queries were around 6 milliseconds slower, which is an acceptable cost for the benefits of securing the data. However, the slowest DNS transactions performed much better with the new DoH based system than the traditional one โ sometimes hundreds of milliseconds better."
Maybe, if in the future the DNS protocol will be always more delivered over HTTPS and the TCP protocol as we know it will be replaced by QUIC (or something else anyway better than TCP), IMHO this could be a cool way to protect users using something that already exists!
Useful links
- curl wiki about DNS over HTTPS https://github.com/curl/curl/wiki/DNS-over-HTTPS
- Mozilla DoH performance test https://blog.nightly.mozilla.org/2018/08/28/firefox-nightly-secure-dns-experimental-results/
- CloudFlare DoH daemon cloudflared https://developers.cloudflare.com/1.1.1.1/dns-over-https/
- How Argo Tunnel works https://developers.cloudflare.com/argo-tunnel/reference/how-it-works/
- How to install libModSecurity on Nginx https://www.nginx.com/blog/compiling-and-installing-modsecurity-for-open-source-nginx/