Subdomain enumeration with DNSSEC

By on 17 Jan 2023

Category: Tech matters

Tags: , , ,

Blog home

In my previous blog post, I described how subdomain enumeration and subdomain brute force in particular could be enhanced by taking the DNS status code into account, rather than relying on the existence of A or AAAA records only.

This follow-up post describes what techniques exist to enumerate subdomains in a DNSSEC-enabled zone and what countermeasures exist to prevent it. DNSSEC itself is not explained further, however, some relevant record types are briefly described.

Within this post, almost all shown examples are based on the following DNS zone of lab.test:

lab.test.		3600	IN	SOA	ns1.lab.test. hostmaster.lab.test. 1 10800 3600 604800 3600
data.lab.test.		3600	IN	A	192.168.3.12
help.lab.test.		3600	IN	CNAME	labsupport.helpdesk.test.
lab.test.		3600	IN	NS	ns1.lab.test.
marketing.lab.test.	3600	IN	A	192.168.10.15
shop.lab.test.		3600	IN	A	192.168.1.100
transfer.lab.test.	3600	IN	A	192.0.1.170
verify.lab.test.	3600	IN	TXT	"VGhpcyBpcyBhIHNhbXBsZSB0b2tlbg=="
www.lab.test.		3600	IN	A	192.0.1.100

The zone is relatively simple and contains some A records, a CNAME and a TXT record. The zone above does not use DNSSEC yet, however for the following sections it will be enabled and relevant aspects for reconnaissance will be described.

NSEC

In order to show the differences between a DNS zone without DNSSEC and a DNS zone with DNSSEC, the zone layout is shown below, after DNSSEC was enabled:

lab.test.		3600	IN	SOA	ns1.lab.test. hostmaster.lab.test. 1 10800 3600 604800 3600
lab.test.		3600	IN	RRSIG	SOA 13 2 3600 20220929000000 20220908000000 35470 lab.test. lF26RvKT4Rrv2xDUjMG9eZh1ows/jWVj9iCW1oYDSv1awbdnYzMEpC9k aiRPYSDirQP868/zgo7M+sA+WkjrKA==
lab.test.		3600	IN	NS	ns1.lab.test.
lab.test.		3600	IN	RRSIG	NS 13 2 3600 20220929000000 20220908000000 35470 lab.test. RubSaWioCVZZLJG3UFrd3UZdAGK8iGzT+oo5VDvPhXlkZuVf71fA84nd Q2wU3RC0JtZptgWcyVUDGnyaPFIJpw==
data.lab.test.		3600	IN	A	192.168.3.12
data.lab.test.		3600	IN	RRSIG	A 13 3 3600 20220929000000 20220908000000 35470 lab.test. 3330Hj9zneyl3OlHAD0I2ot/8gUiCxo7M7gMY8e4yWE61RXaHIhI/C9o 30yxzqEbQf5m66evr5EBKsqUqQXfnA==
help.lab.test.		3600	IN	CNAME	labsupport.helpdesk.test.
help.lab.test.		3600	IN	RRSIG	CNAME 13 3 3600 20220929000000 20220908000000 35470 lab.test. V8VEbkTOeAtLYJMhnIVBBIJ7+2Ez4yjGxJq+RPBmwKqEHG9jP493rsOa sQMA5/axe3lBNCN3c0CDN/CVbE0ZZg==
lab.test.		3600	IN	DNSKEY	257 3 13 eOaz5dZkzyOOJtIjVBO3Q66BRu22pH0f2iJUiiR6340S6OyOH4omyhBT 8Awt8hoc5jv1YDcsjjdoGfoPJbA1jg==
lab.test.		3600	IN	RRSIG	DNSKEY 13 2 3600 20220929000000 20220908000000 35470 lab.test. RslPp0ENzZG1SKhQxHCZnoIFSCV0BjvmHsD6Ze0cGfZCzfeM7xg/gwKB 4fRentzDl3KPbQDOxg/gyaxiGI2l4A==
marketing.lab.test.	3600	IN	A	192.168.10.15
marketing.lab.test.	3600	IN	RRSIG	A 13 3 3600 20220929000000 20220908000000 35470 lab.test. b2+en4ZspnZJcSU8ZGKiWZwSEomJ8bibs2pVSMA5yOYBWoTSAnvM6bh7 r4N+1ZNRu4gpssYSH2Q3PKdNtioxXw==
shop.lab.test.		3600	IN	A	192.168.1.100
shop.lab.test.		3600	IN	RRSIG	A 13 3 3600 20220929000000 20220908000000 35470 lab.test. S6QR7rOJqMjImzIDjHXxV3F35pKpwqpS2+IFIfYuO2+xS855n5cxD/Vt cXm2f2U58ZmlNoHd8rSCPAxcH4bsbA==
lab.test.		3600	IN	NSEC	data.lab.test. NS SOA RRSIG NSEC DNSKEY
lab.test.		3600	IN	RRSIG	NSEC 13 2 3600 20220929000000 20220908000000 35470 lab.test. cf5XoEi++4UAdQsmHfEauk4iUwDu+BsuN+2aynpuaChZBf8Hi0DpE8Yf 0MphpHHWKR5YwznfQ7AELOuw5TIXmw==
www.lab.test.		3600	IN	A	192.0.1.100
www.lab.test.		3600	IN	RRSIG	A 13 3 3600 20220929000000 20220908000000 35470 lab.test. fN32aPfXV4HxTPPOy1TppkdD5miojmdX7IN5cmVEqBpr4MPVrimYc94Z 6cQ8kPzHukxmyhrgUmh1iUChWi9pgw==
transfer.lab.test.	3600	IN	A	192.0.1.170
transfer.lab.test.	3600	IN	RRSIG	A 13 3 3600 20220929000000 20220908000000 35470 lab.test. EosrpfjvPuyJDbO7rTQaqM7uNznRFXO8fhxGbI80wxhxNAW+TLUShw30 hd6s+zCxRmXMpld7WQjFGiF1aCJ1OQ==
shop.lab.test.		3600	IN	NSEC	transfer.lab.test. A RRSIG NSEC
shop.lab.test.		3600	IN	RRSIG	NSEC 13 3 3600 20220929000000 20220908000000 35470 lab.test. AzVZJsrtJPODIXTx/yw3zsE48SMZm7DVTWM6kw3GhVppxNaVTy37Xcfd eH6BKABZA8RyCMsweSRPs2HqLT+KVw==
verify.lab.test.	3600	IN	TXT	"VGhpcyBpcyBhIHNhbXBsZSB0b2tlbg=="
verify.lab.test.	3600	IN	RRSIG	TXT 13 3 3600 20220929000000 20220908000000 35470 lab.test. W8wza0P9RpYcX4bil76iPIhIM+aFwDcodx73jlP8k9RiCzrThqTnO7a6 NS4NoS6RZVFK1TLRJPLUSueFZ3aVcA==
transfer.lab.test.	3600	IN	NSEC	verify.lab.test. A RRSIG NSEC
transfer.lab.test.	3600	IN	RRSIG	NSEC 13 3 3600 20220929000000 20220908000000 35470 lab.test. A8+dWc5WYZuFQeHgwIMS6YhiX0El8zihV0oPfaA295SROZZ5kTa2sAEr voo1cioOlTi3b2p04QCGmhfqX/wo5A==
data.lab.test.		3600	IN	NSEC	help.lab.test. A RRSIG NSEC
data.lab.test.		3600	IN	RRSIG	NSEC 13 3 3600 20220929000000 20220908000000 35470 lab.test. cTkp23XUJwU+4q6h4tdq4oFFch7tV0zg5ZX7X3FuvyuMVF2GsrMGLx6R lnbPo4YI9Dk7qnN31lFiGtHhs7bR/g==
help.lab.test.		3600	IN	NSEC	marketing.lab.test. CNAME RRSIG NSEC
help.lab.test.		3600	IN	RRSIG	NSEC 13 3 3600 20220929000000 20220908000000 35470 lab.test. Fym5uQ1izdip/7kFpkjm4U2pz4vQCGTynsztEpsuuH6sDDElutgBKFL1 YOIADLnF5dby6WHONDNomqVWm8CmbA==
verify.lab.test.	3600	IN	NSEC	www.lab.test. TXT RRSIG NSEC
verify.lab.test.	3600	IN	RRSIG	NSEC 13 3 3600 20220929000000 20220908000000 35470 lab.test. blwL2ZeAC9j7rf03ESUrLBMVaeswMddva0RIG8vJZgehV9Czk/qOnanR YR4jiLsq9zDr70og7vs/rYUEyP6uXA==
marketing.lab.test.	3600	IN	NSEC	shop.lab.test. A RRSIG NSEC
marketing.lab.test.	3600	IN	RRSIG	NSEC 13 3 3600 20220929000000 20220908000000 35470 lab.test. Jk9IMIsz9VrvelU9ftNiuhGQMkUHBQJjPpvbtfdBHmfkygSLleKqXXt/ jD+D6/DVOyqa55zOPigNTZORl6RHrw==
www.lab.test.		3600	IN	NSEC	lab.test. A RRSIG NSEC
www.lab.test.		3600	IN	RRSIG	NSEC 13 3 3600 20220929000000 20220908000000 35470 lab.test. Wuxx8PShzKQ7H8E+5QPEa/Z+D9Hd4S8Tm9uTBCP97uZTyDcHvrEyWLXx GYecxDttdiQ2AIPQF4IRq9WfCqOc9w==

Compared to the previous zone, this one is significantly larger and contains some additional resource records (RRs) like DNSKEY, RRSIG and NSEC.

DNSSEC allows proving the non-existence of nodes or the non-existence of record types belonging to existing nodes. If, for example, the nameserver is asked for the subdomain doesnotexist.lab.test, the nameserver would respond with an answer similar to:

‘The name doesnotexist.lab.test does not exist. The previous entry in the zone is data.lab.test and the next entry is help.lab.test. No entry exists in between.’

This is exactly where NSEC records (RFC 4034) come into play, and for this procedure to work, the DNS zone needs to be lexicographically ordered. The informal statement above corresponds to the following response after sending a request to the DNSSEC-enabled nameserver:

dig +dnssec nsec doesnotexist.lab.test @172.17.0.2

; <<>> DiG 9.18.1-1ubuntu1.2-Ubuntu <<>> +dnssec nsec doesnotexist.lab.test @172.17.0.2
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 27814
;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 6, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
;; QUESTION SECTION:
;doesnotexist.lab.test.		IN	NSEC

;; AUTHORITY SECTION:
lab.test.		3600	IN	SOA	ns1.lab.test. hostmaster.lab.test. 1 10800 3600 604800 3600
lab.test.		3600	IN	RRSIG	SOA 13 2 3600 20221124000000 20221103000000 23827 lab.test. RdxgZrWEPjtojxsjQY94+pxjoPKfq36ldX2reJ7DiAKgyU7EdXdAmqb8 JTay7LiP151UEaPjjFZ8z7oFFR2llA==
data.lab.test.		3600	IN	NSEC	help.lab.test. A RRSIG NSEC
data.lab.test.		3600	IN	RRSIG	NSEC 13 3 3600 20221124000000 20221103000000 23827 lab.test. 2qYQfphXlIqT/eOOVcR6fgUVn3BoJkGAMdr+g7Zu1KsaF8RIRVdlKidD dJkiOGnH57MsZE444/fgFlPxSnphyg==
lab.test.		3600	IN	NSEC	data.lab.test. NS SOA RRSIG NSEC DNSKEY
lab.test.		3600	IN	RRSIG	NSEC 13 2 3600 20221124000000 20221103000000 23827 lab.test. aRWVF7Tos8KJlpI5kWneSAhjaZhlDmoYRsMXZphFsPxb8T1hDf/oo8Kl AyZUdKFkBJkH9wn7Zqpd9qzq/Pk8EA==

;; Query time: 3 msec
;; SERVER: 172.17.0.2#53(172.17.0.2) (UDP)
;; WHEN: Sun Nov 13 14:47:36 CET 2022
;; MSG SIZE  rcvd: 489

The relevant line in the response above is:

data.lab.test.                     3600       IN           NSEC     help.lab.test. A RRSIG NSEC

This NSEC record states, that no node exists between data.lab.test and help.lab.test, and since doesnotexist.lab.test would fall into that range, the statement implies that doesnotexist.lab.test does not exist. The RRSIG records are cryptographic signatures of RRs. Clients that verify these signatures could make sure that the DNS communication has not been tampered with.

As NSEC records point to the next lexicographic entry within the DNS zone, it is possible to enumerate the whole DNS zone in linear time. This approach, which is called ‘zone walking’, is described in the section ‘NSEC zone walking’ below.

NSEC3

To overcome the problems regarding zone enumeration, NSEC3 was introduced and described in RFC 5155. NSEC3 is using the linked list approach as well, however, the owner names and the next owner names are cryptographic hashes of the original name. Similar to NSEC, the linked list is lexicographically ordered, taking the hashed name as a basis.

In order to provide an overview, the same zone as before is printed below, with DNSSEC and NSEC3 enabled:

Zone output of a DNS zone using DNSSEC with NSEC3.
Figure 1 — DNS zone using DNSSEC with NSEC3.

In Figure 1, the zone output above looks quite messy, but the inner working is straightforward. The zone is relatively similar to the one with NSEC enabled, however, the labels are no longer present in clear text but in their hashed form.

To improve readability, the NSEC3 and NSEC3PARAM records were extracted and shown below:

NSEC3 and NSEC3PARAM of lab.test.
Figure 2 — NSEC3 and NSEC3PARAM of lab.test.

The list in Figure 2 shows an NSEC3PARAM record and eight NSEC3 records.

The NSEC3PARAM record provides information about the configured mode for NSEC3. Generally, it comprises the following elements:

<hash_algorithm> <flags> <iterations> <salt>

The hash algorithm is always SHA1, denoted by the value 1.

‘Flags’ refer to the opt-out feature, which influences whether delegations within the zone are signed as well. This feature is relevant for large operators, who include thousands of names within their zone files. With the opt-out flag being set, unsigned delegations do not require additional NSEC3 records and can be covered by a single NSEC3 record.

The hash function is applied at least once, but could go through additional iterations, determined by the value of ‘iterations’.

Finally, an optional salt could be defined, or just left empty by adding a ‘-‘.

The NSEC3 records above start with the hashed label, followed by the domain name, for example, ouf8moqrb9nfsoua7epuapeb4tgitpmo.lab.test. Furthermore, the record contains the NSEC3PARAM value as well, which in this case is 1 0 0 –. It indicates that the hash function is set to SHA1, the opt-out flag is cleared and no additional iterations or salt are used. The last important component of an NSEC3 record is the next hashed owner name, for example, 0J6SCENU0P4HERE1AAKA0IEC1FLT3OU0.

For the calculation of the hashes, the values for the algorithm (H(x)), iterations (k) and salt are taken into account. The final hash is calculated by hashing the domain name, denoted as x (needs to be in wire format; see the section below) concatenated with the raw salt bytes (RFC 5155):

IH(salt, x, 0) = H(x || salt), and
IH(salt, x, k) = H(IH(salt, x, k-1) || salt), if k > 0

The resulting hash is encoded using Base32hex and is then ready to be used in NSEC3 records.

DNS wire format

When using domain names in DNS messages, they need to be converted into a byte sequence, defined in RFC 1035, Section 3.1.

In order to convert a domain name into a sequence that could be used in DNS messages, the following steps should be performed:

  1. Split the fully qualified domain name into labels (at each ‘.’ delimiter)
  2. Count the length of each label and prepend it to the label

An example of domain data.lab.test. is shown below (note how the empty root label results in a NUL byte):

  1. Data lab test
  2. \x04data\x03lab\x04test\x00

Zone walking techniques

NSEC zone walking

NSEC walking is a technique that leverages the design of NSEC records to obtain the full DNS zone of a chosen domain. As all subdomains within a DNSSEC-enabled zone that uses NSEC are connected via pointers, it’s possible to query all subdomains in a row, since every subdomain reveals its successor.

For demonstration purposes, let’s look at all NSEC records of the zone file above:

lab.test.		    3600	IN	NSEC	data.lab.test. NS SOA RRSIG NSEC DNSKEY
shop.lab.test.		3600	IN	NSEC	transfer.lab.test. A RRSIG NSEC
transfer.lab.test.	3600	IN	NSEC	verify.lab.test. A RRSIG NSEC
data.lab.test.		3600	IN	NSEC	help.lab.test. A RRSIG NSEC
help.lab.test.		3600	IN	NSEC	marketing.lab.test. CNAME RRSIG NSEC
verify.lab.test.	3600	IN	NSEC	www.lab.test. TXT RRSIG NSEC
marketing.lab.test.	3600	IN	NSEC	shop.lab.test. A RRSIG NSEC
www.lab.test.		3600	IN	NSEC	lab.test. A RRSIG NSEC

Within this list, it can easily be seen that every subdomain within the zone has a successor. The successor is determined by the lexicographic order:

Illustration of a NSEC linked-list.
Figure 3 — Illustration of a NSEC linked-list.

To start NSEC walking, the root domain/apex is queried first, which reveals its successor data.lab.test. Next data.lab.test is queried, with its successor being help.lab.test.

This sequence is repeated until the root domain itself will be printed again. This operation could be performed within O(n) and results in the following list:

. -> data -> help -> marketing -> shop -> transfer -> verify -> www -> .

To avoid full DNS zone disclosure, countermeasures like NSEC3, White Lies, and Black Lies have been developed.

NSEC3 zone enumeration

Zone walking with NSEC records is quite easy and could be done in linear time since it’s only following a linked list. With NSEC3 it’s still possible to enumerate the zone contents, but a slightly different approach has to be chosen. In order to demonstrate this, let’s start with the classic NSEC approach first, and see what happens.

Like in the NSEC examples, we start by querying the NSEC3 record of the root domain lab.test:

dig +dnssec @172.17.0.2 nsec3 lab.test

; <<>> DiG 9.18.1-1ubuntu1.2-Ubuntu <<>> +dnssec @172.17.0.2 nsec3 lab.test
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 15167
;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
;; QUESTION SECTION:
;lab.test.			IN	NSEC3

;; AUTHORITY SECTION:
lab.test.		3600	IN	SOA	ns1.lab.test. hostmaster.lab.test. 1 10800 3600 604800 3600
lab.test.		3600	IN	RRSIG	SOA 13 2 3600 20221117000000 20221027000000 36912 lab.test. amoct+XKEjDIiN77gTRmkH3dDk12h1M75el/rlr3CfC49IA5CUeD7Akq rnFfCAlsahuiS8LfL5697HMF8O3LSA==
i2kfv8ko2gacgetvv4h5di8jfnfb92lp.lab.test. 3600	IN NSEC3 1 0 0 - M3L9I3DTSH1VLT9VIVT4B92GS4JRF1RL NS SOA RRSIG DNSKEY NSEC3PARAM
i2kfv8ko2gacgetvv4h5di8jfnfb92lp.lab.test. 3600	IN RRSIG NSEC3 13 3 3600 20221117000000 20221027000000 36912 lab.test. oURGlwqML5oVJZZcLGgv5v+ix8/bwR8VtvmQht86t/X/YYmbele26I70 E4Vru7Y6UgK6r3Qz8xv56r93HuJntg==

;; Query time: 0 msec
;; SERVER: 172.17.0.2#53(172.17.0.2) (UDP)
;; WHEN: Tue Nov 08 23:20:08 CET 2022
;; MSG SIZE  rcvd: 376

The NSEC3 record indicates, that the successor of i2kfv8ko2gacgetvv4h5di8jfnfb92lp.lab.test (hash of lab.test) is M3L9I3DTSH1VLT9VIVT4B92GS4JRF1RL.

The naive approach would be to just take that label as a basis for the next NSEC3 query:

dig +dnssec @172.17.0.2 nsec3 M3L9I3DTSH1VLT9VIVT4B92GS4JRF1RL.lab.test

; <<>> DiG 9.18.1-1ubuntu1.2-Ubuntu <<>> +dnssec @172.17.0.2 nsec3 M3L9I3DTSH1VLT9VIVT4B92GS4JRF1RL.lab.test
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 54431
;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 6, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
;; QUESTION SECTION:
;M3L9I3DTSH1VLT9VIVT4B92GS4JRF1RL.lab.test. IN NSEC3

;; AUTHORITY SECTION:
lab.test.		3600	IN	SOA	ns1.lab.test. hostmaster.lab.test. 1 10800 3600 604800 3600
lab.test.		3600	IN	RRSIG	SOA 13 2 3600 20221117000000 20221027000000 36912 lab.test. amoct+XKEjDIiN77gTRmkH3dDk12h1M75el/rlr3CfC49IA5CUeD7Akq rnFfCAlsahuiS8LfL5697HMF8O3LSA==
i2kfv8ko2gacgetvv4h5di8jfnfb92lp.lab.test. 3600	IN NSEC3 1 0 0 - M3L9I3DTSH1VLT9VIVT4B92GS4JRF1RL NS SOA RRSIG DNSKEY NSEC3PARAM
i2kfv8ko2gacgetvv4h5di8jfnfb92lp.lab.test. 3600	IN RRSIG NSEC3 13 3 3600 20221117000000 20221027000000 36912 lab.test. oURGlwqML5oVJZZcLGgv5v+ix8/bwR8VtvmQht86t/X/YYmbele26I70 E4Vru7Y6UgK6r3Qz8xv56r93HuJntg==
6nh4qhnu8jalvaolcrpd3ofctqs6ho8o.lab.test. 3600	IN NSEC3 1 0 0 - B7B8CJSRF9MHA4NRKPSKE4R803G3T8TF A RRSIG
6nh4qhnu8jalvaolcrpd3ofctqs6ho8o.lab.test. 3600	IN RRSIG NSEC3 13 3 3600 20221117000000 20221027000000 36912 lab.test. NgL69/mR/xCaLeUtUtK6VZgPJbCdxOISobb9854JC7IfwXWOrY6NB0E9 DqGnbz0spwinQEiTExbF7rW2hzaxDQ==

;; Query time: 3 msec
;; SERVER: 172.17.0.2#53(172.17.0.2) (UDP)
;; WHEN: Tue Nov 08 23:25:05 CET 2022
;; MSG SIZE  rcvd: 592

This request failed with the error code NXDOMAIN because the hashed label cannot be queried directly in order to get the next NSEC3 record. When looking back to the first request to lab.test, the queried NSEC3 record is also not present, although the nameserver returned NOERROR. The non-existence is indicated by the returned NSEC3 record (proof of non-existence) and the returned SOA record.

Another interesting behaviour can be observed: Two NSEC3 records were returned. Due to the hashing, structural information about the zone is lost and the resolver wouldn’t know if a wildcard is to be expanded. To mitigate this, the resolver needs to be told about the ‘closest encloser‘ and ‘next closer name‘ to determine the source of wildcard synthesis in order to deny the existence of a wildcard. A third record might be necessary to cover the wildcard itself (RFC 7129 Section 5.5, Authenticated Denial of Existence in the DNS).

Since it is no longer possible to follow a chain of records like with NSEC, a different approach to enumeration is necessary.

Collecting NSEC3 hashes

By sending large amounts of requests for arbitrary domain names the nameserver will respond with many different next-owner names. To start with an example, a request for e84xr9m1.lab.test (Hashed: H3DV1EJ5TMSMQO8VEV3F1814ALRRFMQ4) is sent, for which the nameserver returns the following NSEC3 record:

ctrchs9nacks8mo44438n9g2uhdprfbn.lab.test. 3600         IN NSEC3 1 0 0 - HE7FM31MLIE0OV8VB37MQG584GUM9BOM A RRSIG

The non-existence proof above indicates that no nodes exist between ctrchs9nacks8mo44438n9g2uhdprfbn.lab.test and HE7FM31MLIE0OV8VB37MQG584GUM9BOM.lab.test. The first hash corresponds to transfer.lab.test, and the second hash to www.lab.test. In this case, we know what plain text the hash corresponds to because we have full insights into the DNS zone.

An attacker without any knowledge about the zone contents requires a rainbow table to retrieve the plain text values. The steps above showed that a query for a random name revealed two new hashes, which could be stored in a list for further processing. This step is repeated with another random name o6rwq2ut.lab.test:

ouf8moqrb9nfsoua7epuapeb4tgitpmo.lab.test. 3600       IN NSEC3 1 0 0 - 0J6SCENU0P4HERE1AAKA0IEC1FLT3OU0 CNAME RRSIG

As this random name also does not exist, the lexicographically close predecessor and successor are returned, which are ouf8moqrb9nfsoua7epuapeb4tgitpmo (help.lab.test) and 0J6SCENU0P4HERE1AAKA0IEC1FLT3OU0 (data.lab.test). Again, two new hashes are returned that could be appended to the list of hashes.

This procedure is repeated until no or very few new hashes are returned. Jonathan Demke and Casey Deccio researched this topic, developed a methodology to approximate zone sizes and found that “[…] in approximately 75% of cases, the methodology would yield an estimate that is within 20% of the actual zone size, with only 18 queries.”

The phase described above could be described as the ‘online phase’ since requests are sent to nameservers. If the list of hashes is complete, the ‘offline phase’ could be started during which rainbow tables are leveraged to crack as many hashes as possible. Because the zone name is always part of the hash input, custom rainbow tables need to be computed for each zone.

The result of the offline phase is a list of retrieved subdomains.

Zone walking countermeasures

In order to prevent zone enumeration, hashing the owner names as per NSEC3 is not sufficient since it is still possible to conduct offline brute-force attacks using rainbow tables.

Therefore, a couple of mechanisms were suggested that render zone walking impossible. While RFC 4470 and Black Lies describe a general approach to modifying NSEC records, ‘White Lies’ is a concept that is predominantly used with NSEC3 and is also described in the following subsections.

RFC 4470

In order to not disclose actual next names within a DNS zone, RFC 4470 suggests to “[…] list any name that falls lexically after the NSEC’s owner name and before the next instantiated name in the zone. […]”.

To achieve this, DNS names need to be canonically ordered as per RFC 4034 Section 6.1.

Generating a DNS record that is lexically close to the requested name requires the nameserver to sign the new record on-the-fly. Online signing could be problematic if many requests have to be processed in a short amount of time since each signing operation requires computational effort. Furthermore, in order to sign the records, the generating nameserver requires permanent access to the private key, which increases the impact in case of a successful nameserver breach.

If a next name needs to be generated, an ‘epsilon function’ calculates a name that is lexically close to the requested name, but not identical to any existing name. RFC 4470 does not suggest what epsilon function should be used, but provides an example of how such a function could look:

Incrementing a name: “To increment a name, add a leading label with a single null (zero-value) octet.”

Decrementing a name: “Fill the leftmost label to its maximum length with zeros (numeric, not ASCII zeros) and subtract one.”

Following the approach above, the nameserver might return the following responses for queries to the non-existing name notthere.lab.test:

nottherd\255\255\255\255\255\255\255\255\255\255\255\255\255\255
\255\255\255\255\255\255\255\255\255\255\255\255\255\255\255
\255\255\255\255\255\255\255\255\255\255\255\255\255\255\255
\255\255\255\255\255\255\255\255\255\255\255\255\255\255\255
\255.lab.test 3600 IN NSEC \000.notthere.lab.test ( NSEC RRSIG )
\)\255\255\255\255\255\255\255\255\255\255\255\255\255\255\255
\255\255\255\255\255\255\255\255\255\255\255\255\255\255\255
\255\255\255\255\255\255\255\255\255\255\255\255\255\255\255
\255\255\255\255\255\255\255\255\255\255\255\255\255\255\255
\255\255.lab.test 3600 IN NSEC \000.*.lab.test ( NSEC RRSIG )

The first result indicates that notthere.lab.test does not exist. The second result proves that no wildcard exists for lab.test.

The epsilon function that is described in RFC 4470 does not take length constraints into account and is not optimal for production use. In this exact form, it does not seem to be used in any major DNSSEC implementation, however, it serves as a basis for the concepts of White Lies and Black Lies.

NSEC3 White Lies

Even though NSEC3 was making the process of subdomain enumeration more difficult, the techniques mentioned above could still be used to approximate the size of the zone and discover subdomains by cracking NSEC3 hashes in offline brute-force attacks.

To stop zone enumeration by gathering NSEC3 hashes, Dan Kaminsky adopted the mechanisms as described in RFC 4470 and applied them to NSEC3, naming the approach White Lies. It is specific to NSEC3 and implemented within his software Phreebird. It could be used instead of the classic ways to prove non-existence.

When using NSEC3 White Lies, fake NSEC3 records are generated on-the-fly that surround the requested name, similar to the procedure described in RFC 4470. Existing records that surround the NSEC3 hash of the requested subdomain are no longer disclosed. Although fake NSEC3 records are being sent to the requesting party, the mechanism is still compliant with all RFCs, since the non-existence proof is still valid.

In order to demonstrate the inner workings of NSEC3 White Lies, a DNS query for nodedoesnotexist.lab.test is sent to an authoritative nameserver that runs PowerDNS in ‘narrow mode’, which is how NSEC3 White Lies is called in this specific implementation:

dig +dnssec @172.17.0.2 a nodedoesnotexist.lab.test

; <<>> DiG 9.18.1-1ubuntu1.1-Ubuntu <<>> +dnssec @172.17.0.2 a nodedoesnotexist.lab.test
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 12660
;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 8, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
;; QUESTION SECTION:
;nodedoesnotexist.lab.test.	IN	A

;; AUTHORITY SECTION:
lab.test.		3600	IN	SOA	ns1.lab.test. hostmaster.lab.test. 1 10800 3600 604800 3600
lab.test.		3600	IN	RRSIG	SOA 13 2 3600 20220929000000 20220908000000 23452 lab.test. HMO2lJjzIVEB5LA4rsFQWybmcMnXkvOIIjLZHwHvl2Lqu2+jd/ugFkI0 foehSesKrafK8u3RAzTOYJ+B+u8+3Q==
i2kfv8ko2gacgetvv4h5di8jfnfb92lp.lab.test. 3600	IN NSEC3 1 0 0 - I2KFV8KO2GACGETVV4H5DI8JFNFB92LQ NS SOA RRSIG DNSKEY NSEC3PARAM
i2kfv8ko2gacgetvv4h5di8jfnfb92lp.lab.test. 3600	IN RRSIG NSEC3 13 3 3600 20220929000000 20220908000000 23452 lab.test. aaZAyvspE56Ty9pJJRbUohMTJ18QbW3UIuNkwrLB0qYO6+ds7YZdrzdY j9Vtq5YmvbEA+U8JBcko5MoRyCPPIA==
dli7u1udp62ok2r60nbbupl27bn0qh55.lab.test. 3600	IN NSEC3 1 0 0 - DLI7U1UDP62OK2R60NBBUPL27BN0QH57
dli7u1udp62ok2r60nbbupl27bn0qh55.lab.test. 3600	IN RRSIG NSEC3 13 3 3600 20220929000000 20220908000000 23452 lab.test. 4YKUWsz4d5OyT3xUFyD0aMPKd9hMKkpedR0eK+sKHm6tlitc8YgUGgLP 6706cH3BS+R6Z7sisoiqniV2ztiyFg==
lhpuceck180r4ab4vnkhib4s7fc35t8j.lab.test. 3600	IN NSEC3 1 0 0 - LHPUCECK180R4AB4VNKHIB4S7FC35T8L
lhpuceck180r4ab4vnkhib4s7fc35t8j.lab.test. 3600	IN RRSIG NSEC3 13 3 3600 20220929000000 20220908000000 23452 lab.test. 9BCoZQX6WglkBkUeQbBstH2lZtPU3pMqQl3kWEE4jsVQvy0VVVMmpedd tTDVbi3cQRpRjJDmFtXk88if4P/Y2Q==

;; Query time: 0 msec
;; SERVER: 172.17.0.2#53(172.17.0.2) (UDP)
;; WHEN: Sun Sep 18 13:11:55 CEST 2022
;; MSG SIZE  rcvd: 743

In section ‘NSEC3 zone enumeration‘, two NSEC3 records were returned after requesting M3L9I3DTSH1VLT9VIVT4B92GS4JRF1RL.lab.test. The response above contains three NSEC3 records of which two are required to deny the existence of a wildcard. Compared to NSEC3 without White Lies, the hashes above look a bit different:

i2kfv8ko2gacgetvv4h5di8jfnfb92lp.lab.test. 3600	IN NSEC3 1 0 0 - I2KFV8KO2GACGETVV4H5DI8JFNFB92LQ NS SOA RRSIG DNSKEY NSEC3PARAM

dli7u1udp62ok2r60nbbupl27bn0qh55.lab.test. 3600	IN NSEC3 1 0 0 - DLI7U1UDP62OK2R60NBBUPL27BN0QH57

lhpuceck180r4ab4vnkhib4s7fc35t8j.lab.test. 3600	IN NSEC3 1 0 0 - LHPUCECK180R4AB4VNKHIB4S7FC35T8L

In this setup the hashing algorithm is set to SHA1, no additional iterations are performed, and no salt is used.

The first entry refers to the root domain lab.test, which after applying SHA1 hashing and Base32 encoding is I2KFV8KO2GACGETVV4H5DI8JFNFB92LP. The next owner hash, as per the response, is I2KFV8KO2GACGETVV4H5DI8JFNFB92LQ. If you look closely, this hash is almost identical to the hash of lab.test and only differs in the last character, which is ‘Q’ instead of ‘P’. This exactly corresponds to the next possible ASCII value. I2KFV8KO2GACGETVV4H5DI8JFNFB92LQ does not correspond to the hash of an existing subdomain and was generated on-the-fly.

The next answer corresponds to the subdomain we requested, nodedoesnotexist.lab.test, which corresponds to DLI7U1UDP62OK2R60NBBUPL27BN0QH56. The surrounding NSEC3 records are returned as dli7u1udp62ok2r60nbbupl27bn0qh55 and DLI7U1UDP62OK2R60NBBUPL27BN0QH57, whose last bytes have a positive and negative distance of 1 to the requested hash:

(hash(domain) - 1) < hash(domain) < (hash(domain) + 1)

Although the surrounding NSEC3 records are forged, the mechanism is still producing valid non-existence proofs, as no record exists between dli7u1udp62ok2r60nbbupl27bn0qh55 and DLI7U1UDP62OK2R60NBBUPL27BN0QH57.

The last response corresponds to the wildcard domain *.lab.test. The previous and next owner names are returned as lhpuceck180r4ab4vnkhib4s7fc35t8j and LHPUCECK180R4AB4VNKHIB4S7FC35T8L, which minimally surround the hash of *.lab.test (LHPUCECK180R4AB4VNKHIB4S7FC35T8K).

This mechanism does not disclose any hashes of existing DNS records, except for the root domain and the wildcard.

Black Lies

The concept of Black Lies was implemented within Cloudflare’s nameserver software RRDNS. The inner workings are described in their blog post. Amazon adopted Black Lies for zones hosted by Route 53 and uses a slightly different implementation. Besides Cloudflare and Amazon, the operator NS1 announced on 16 January 2018 it was using DNSSEC with Black Lies as well. Black Lies enables authenticated denial of existence for NSEC records, without revealing the actual zone contents.

The mechanism behind Black Lies is based on RFC 4470 and works by prepending a lexicographically close successor of the QNAME. As per an early draft, the successor should be set to the ‘immediate lexicographic successor of the QNAME’. As opposed to NSEC3 White Lies, Cloudflare nameservers are omitting the previous node for performance reasons. Reducing the overall response size was one of the main drivers for their Black Lies implementation. In fact, there are a few differences between the implementations of Cloudflare, Amazon, and NS1. These differences will be highlighted shortly.

Cloudflare

First, we investigate the behaviour when the NSEC record of an existing subdomain is queried. For this example, cloudflare.com is used:

dig +dnssec @ns6.cloudflare.com NSEC ocsp.cloudflare.com

; <<>> DiG 9.16.1-Ubuntu <<>> +dnssec @ns6.cloudflare.com NSEC ocsp.cloudflare.com
; (4 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 21846
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
;; QUESTION SECTION:
;ocsp.cloudflare.com.		IN	NSEC

;; ANSWER SECTION:
ocsp.cloudflare.com.	300	IN	NSEC	\000.ocsp.cloudflare.com. A HINFO MX TXT AAAA LOC SRV NAPTR CERT SSHFP RRSIG NSEC TLSA SMIMEA HIP OPENPGPKEY TYPE64 TYPE65 URI CAA
ocsp.cloudflare.com.	300	IN	RRSIG	NSEC 13 3 300 20221109113135 20221107093135 34505 cloudflare.com. gagtUKOvxNJx2JXirD0bNaYF7aust/lyrpT4wxEWfBUVakrWw82rtQpH xyEcHpGtVSGSTapTMkQYBsm3wkv+QQ==

;; Query time: 15 msec
;; SERVER: 162.159.3.11#53(162.159.3.11)
;; WHEN: Di Nov 08 11:31:35 CET 2022
;; MSG SIZE  rcvd: 207

The request to ocsp.cloudflare.com returns a response that contains an NSEC record pointing to \000.ocsp.cloudflare.com. This next name is a fake record that is the immediate lexicographic successor of the requested name. Besides the prefix, the RR bitmap of the NSEC record comprises a large number of record types:

A HINFO MX TXT AAAA LOC SRV NAPTR CERT SSHFP RRSIG NSEC TLSA SMIMEA HIP OPENPGPKEY TYPE64 TYPE65 URI CAA

Without Black Lies, the RR bitmap of an NSEC record would only contain the RRs that are present for the queried name. The decision to add the complete set of RRs to the RR bitmap was made by Cloudflare for performance reasons since it prevents unnecessary database lookups.

In their blog post about Black Lies they call this approach ‘DNS Shotgun’ and state that their DNS servers “[…] set all the types. We say, this name does exist, just not on the one type you asked for.”

The RR bitmap above actually includes the NSEC type as well but that is because our queried NSEC record exists. The following example shows a query for an MX record. Please note that the MX record does not exist for this node:

; <<>> DiG 9.16.1-Ubuntu <<>> +dnssec @ns6.cloudflare.com MX ocsp.cloudflare.com
; (4 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 62679
;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
;; QUESTION SECTION:
;ocsp.cloudflare.com.         IN    MX

;; AUTHORITY SECTION:
cloudflare.com.         300   IN    SOA   ns3.cloudflare.com. dns.cloudflare.com. 2297138083 10000 2400 604800 300
ocsp.cloudflare.com.    300   IN    NSEC  \000.ocsp.cloudflare.com. A HINFO TXT AAAA LOC SRV NAPTR CERT SSHFP RRSIG NSEC TLSA SMIMEA HIP OPENPGPKEY TYPE64 TYPE65 URI CAA
cloudflare.com.         300   IN    RRSIG SOA 13 2 300 20230113085857 20230111065857 34505 cloudflare.com. dKvpv7Kl5pZKkmcStVzPnuiY/4khBH+d/6RXvsjoNskaT1gmtwIrScKy Gwl8ih/J65kscXIH5IiUmVs4O0PNZA==
ocsp.cloudflare.com.    300   IN    RRSIG NSEC 13 3 300 20230113085857 20230111065857 34505 cloudflare.com. G/3y2ukga47BKY1U29ZbZfVfSiPqaeDM4Jg0O66ETwfVbpFfmE5/pbTf T38Gsff9vcGb/bnrrbLoosF+dWSuyQ==

;; Query time: 8 msec
;; SERVER: 162.159.5.6#53(162.159.5.6)
;; WHEN: Do Jan 12 08:58:57 CET 2023
;; MSG SIZE  rcvd: 361

The RR bitmap looks very similar to the previous example and contains most record types but is lacking the MX record type we asked for.

Next, a non-existing name is requested:

dig +dnssec @ns6.cloudflare.com a doesnotexist.cloudflare.com

; <<>> DiG 9.16.1-Ubuntu <<>> +dnssec @ns6.cloudflare.com a doesnotexist.cloudflare.com
; (4 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 35293
;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
;; QUESTION SECTION:
;doesnotexist.cloudflare.com.	IN	A

;; AUTHORITY SECTION:
cloudflare.com.		300	IN	SOA	ns3.cloudflare.com. dns.cloudflare.com. 2292982229 10000 2400 604800 300
doesnotexist.cloudflare.com. 300 IN	NSEC	\000.doesnotexist.cloudflare.com. RRSIG NSEC
doesnotexist.cloudflare.com. 300 IN	RRSIG	NSEC 13 3 300 20221109113220 20221107093220 34505 cloudflare.com. 0m3Sjui3RTfHzCDb/EBtt/+Q5n79sQr9nqX9bgmmedOr+WggkXcclok7 +BqD+cnfdYeZ8UjuAnxYIBUnsW0Eww==
cloudflare.com.		300	IN	RRSIG	SOA 13 2 300 20221109113220 20221107093220 34505 cloudflare.com. 04F7eWCf2024Wfz9SIh/hD5zWnox80ZXAUfKc9LVkw6QpPsrc+Las5eq vfreCRT7oEAEJzkaYxzxcFC4pgm3mg==

;; Query time: 19 msec
;; SERVER: 162.159.3.11#53(162.159.3.11)
;; WHEN: Di Nov 08 11:32:20 CET 2022
;; MSG SIZE  rcvd: 371

Like in the first example, an NSEC record is returned that was constructed by appending a null byte to the QNAME. Classic nameserver implementations would have returned an NXDOMAIN status code, however, in this case Cloudflare returns NOERROR (NODATA in particular). The main reason for this behaviour is that Cloudflare intends to prevent unnecessary database lookups. Since DNSSEC aims to prove non-existence, rather than the existence of nodes, this approach is compliant with DNSSEC standards.

The RR bitmap of the generated NSEC records for non-existing nodes is set to RRSIG NSEC without setting any other bits. In their blog post, Cloudflare describes that they “[…] only have to return SOA, SOA RRSIG, NSEC and NSEC RRSIG, and we do not need to search the database or precompute dynamic answers.”

This has an implication for active subdomain enumeration techniques like subdomain brute force, as it is no longer possible to use the NOERROR code alone to find existing domains. However, due to the RR bitmap of RRSIG NSEC, it is easy to distinguish between non-existing and existing nodes.

In my previous blog post, I described empty non-terminals and why they are important for reconnaissance. Let’s check the Cloudflare behaviour when querying an ENT:

dig +dnssec @bella.ns.cloudflare.com a issuer.cloudflare.com 
[...]
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 59112
[...]
issuer.cloudflare.com.	300	IN	NSEC	\000.issuer.cloudflare.com. HINFO MX TXT AAAA LOC SRV NAPTR CERT SSHFP RRSIG NSEC TLSA SMIMEA HIP OPENPGPKEY SVCB HTTPS URI CAA
[...]

The status code is NOERROR, as expected. The RR bitmap is set to all supported types, except the one that was specified in the query. This behaviour is the same when requesting a non-empty node. This means that it’s not possible to distinguish between ENTs and non-empty nodes with a single request. However, it’s still possible to request every single record type to check whether a node is an ENT or not.

Route 53

Next, the implementation of Amazon for their Route 53 nameservers is described. First, an existing leaf node is queried, which carries a single A record:

dig +dnssec terminal.ent.lab.test nsec @ns-1341.awsdns-39.org
[...]
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 10663
[...]
terminal.ent.lab.test. 86400 IN	NSEC	\000.terminal.ent.lab.test. A PTR HINFO MX TXT RP AAAA SRV NAPTR DNAME RRSIG SPF IXFR AXFR CAA
[...]

The response above looks quite familiar and indicates that Route 53 is handling this kind of request like Cloudflare does. The string \000 is prepended to the next owner’s name and the RR bitmap shows all supported record types except for the one that was requested.

The following example uses a query for a node that does not exist within the zone:

dig +dnssec doesnotexist.lab.test nsec @ns-1341.awsdns-39.org
[...]
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 23030
[...]
doesnotexist.lab.test.	86400	IN	NSEC	\000.doesnotexist.lab.test. RRSIG NSEC
[...]

Again, this behaviour could also be seen in the Cloudflare examples. The bitmap of RRSIG and NSEC indicates that the node does not exist.

In the next example, an ENT is queried:

dig +dnssec ent.lab.test nsec @ns-1341.awsdns-39.org
[...]
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 40803
[...]
ent.lab.test.	86400	IN	NSEC	\000.ent.lab.test. RRSIG NSEC
[...]

The response is a bit surprising since only the RRSIG and NSEC record types are part of the NSEC bitmap. This means that non-existing nodes cannot be distinguished from ENTs, as the nameserver would handle both in the same way. This could be problematic if client software relies on the correct detection of ENTs and this issue was highlighted in the IETF draft “Empty Non-Terminal Sentinel for Black Lies

NS1

Now we will check how the NS1 implementation works and start with an existing, non-empty node:

dig +dnssec mail.nsone.net nsec @dns4.p01.nsone.net

[...]
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 45447
[...]
mail.nsone.net.		3600	IN	NSEC	\000.mail.nsone.net. CNAME RRSIG NSEC
[...]

Like Cloudflare, NS1 is returning a NOERROR status code. The RR bitmap of the NSEC record, however, does not include all record types, but only the types that are set for this particular node. This is classic NSEC behaviour. A quick query is sent to the nameserver to verify if the RR bitmap is correct:

dig +dnssec mail.nsone.net cname @dns4.p01.nsone.net

[...]
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 25752
[...]
mail.nsone.net.		3600	IN	CNAME	ghs.googlehosted.com.
[...]

As returned by the previous response, the CNAME record is present. Now we request a non-existing node:

dig +dnssec doesnotexist.nsone.net nsec @dns4.p01.nsone.net
[...]
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 38561
[...]
doesnotexist.nsone.net.	3600	IN	NSEC	\000.doesnotexist.nsone.net. RRSIG NSEC
[...]

The structure of this response is identical to Cloudflare’s response — the status code is NOERROR and the bitmap is set to RRSIG NSEC. As a last check, a node is queried that is known to be an ENT:

dig +dnssec dev.svc.nsone.net nsec @dns4.p01.nsone.net

; <<>> DiG 9.18.1-1ubuntu1.2-Ubuntu <<>> +dnssec dev.svc.nsone.net nsec @dns4.p01.nsone.net
[...]
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 46526
[...]
dev.svc.nsone.net.	3600	IN	NSEC	\000.dev.svc.nsone.net. RRSIG NSEC TYPE65281
[...]

This behaviour is interesting. Besides the usual record types RRSIG and NSEC, a third record type is returned: TYPE65281, which represents the byte sequence \xFF\x01. This record type is used by NS1 to represent an ENT and was proposed in the draft ‘Empty Non-Terminal Sentinel for Black Lies’.

Skipping DNSSEC for enumeration

Although there are some ways to circumvent status code inspection, some tools still rely on different codes like NXDOMAIN and NOERROR. If a tool is used that would produce a lot of false positives due to Black Lies behaviour, it might still be possible to fall back to non-DNSSEC mode by setting the DO-bit (DNSSEC Okay) to zero:

Cleared DO-bit in DNS packet.
Figure 4 — Cleared DO-bit in DNS packet.

A request with dig without setting the option +dnssec shows the difference:

dig @ns6.cloudflare.com a doesnotexist.cloudflare.com

; <<>> DiG 9.18.1-1ubuntu1.2-Ubuntu <<>> @ns6.cloudflare.com a doesnotexist.cloudflare.com
; (4 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 33333
;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;doesnotexist.cloudflare.com.	IN	A

;; AUTHORITY SECTION:
cloudflare.com.		300	IN	SOA	ns3.cloudflare.com. dns.cloudflare.com. 2293597908 10000 2400 604800 300

;; Query time: 15 msec
;; SERVER: 162.159.3.11#53(ns6.cloudflare.com) (UDP)
;; WHEN: Sun Nov 13 23:06:37 CET 2022
;; MSG SIZE  rcvd: 100

This time we got a response, with the DNS status code set to NXDOMAIN.

Black Lies implementation summary

After inspecting different implementations, this is a brief summary of the relevant aspects of Black Lies:

  • (Cloudfare, NS1, Route 53) Black Lies prevent NSEC walking efficiently
  • (Cloudfare, NS1, Route 53) DNS status codes cannot be used to show the existence of a domain
  • (Cloudfare, NS1) The existence of domains could be induced by looking at the RR bitmap
  • (Cloudfare) ENTs cannot be distinguished from non-empty nodes
  • (Route 53) ENTs cannot be distinguished from non-existing nodes
  • (NS1) Existing record types could be identified using a single request
  • (NS1) ENTs could be identified using a single request
  • If DNS status codes matter and Black Lies is used, try again with DO-bit set to 0

Tool support

NSEC

NSEC Zone walking is supported by the following tools:

NSEC3

For NSEC3, classic zone walking is not possible, but some tools try to approximate the zone size as well as possible:

Conclusion

After describing the mechanisms of NSEC walking, NSEC3 zone enumeration and their countermeasures, what is the final conclusion? Should unprotected NSEC records be considered a vulnerability? This cannot be easily answered, as the answer to this question completely depends on how DNS zones are treated. This is especially so for Internet-based, public zones, hiding sensitive or internal content behind hard-to-guess subdomains, which is security by obscurity; it would be a better approach to not expose sensitive content at all.

That being said, if the owner of a zone is fully aware of the zone contents and assets that are located behind subdomains and if the owner made sure that these assets are properly secured, zone enumeration wouldn’t be a big deal. On the other hand, if unprotected applications or applications affected by vulnerabilities are located behind subdomains of a public DNS zone, zone enumeration would help attackers to identify and potentially compromise these applications.

NSEC3 makes this process a little bit harder, as hashes need to be cracked and custom rainbow tables need to be constructed for each domain. In cryptography, even though salts are used as an additional layer of protection, this does not apply for NSEC3, since the zone name is hashed in the initial round of the hashing function, and attackers need to create a custom rainbow tables for each zone anyway (RFC 9276).

The best protection, however, could be achieved by using either NSEC3 White Lies or Black Lies. Both are efficient mechanisms to prevent zone enumeration. However, it should be noted that Black Lies do not appear to have been deployed outside of large providers like Cloudflare, Amazon and NS1.

Although NSEC3 White Lies is not proprietary, it is still not included in all implementations. PowerDNS supports NSEC3 White Lies (narrow mode) and uses an efficient epsilon function.

Is the classic way of subdomain brute force affected by DNSSEC? That is only the case if Black Lies is used, and only if the enumeration process relies on status codes like NXDOMAIN and NOERROR. For Black Lies, a slightly different strategy needs to be adopted that takes RR bitmaps into account. Apart from Black Lies, subdomain brute force does not work any differently for zones without DNSSEC.

Bastian Kanbach (Twitter, Mastodon) works as a senior security consultant for Secure Systems Engineering (SSE), conducting penetration tests, red team exercises, and audits for international clients. Bastian’s special areas of interest include network and infrastructure security and active directory environments.

This post is adapted from the original at SSE Blog.

Rate this article

The views expressed by the authors of this blog are their own and do not necessarily reflect the views of APNIC. Please note a Code of Conduct applies to this blog.

Leave a Reply

Your email address will not be published. Required fields are marked *

Top