Subdomain Enumeration with DNSSEC

Overview

In my previous blog post [1] I described how subdomain enumeration and subdomain bruteforce in particular could be enhanced by taking 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 like DNSKEY, RRSIG and NSEC.

DNSSEC allows to prove 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 [2] 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 resource records. 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".

NSEC3

To overcome the problems regarding zone enumeration, NSEC3 was introduced and described in RFC5155 [3].

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.

Like for 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:

DNS Zone using DNSSEC with NSEC3
DNS Zone using DNSSEC with NSEC3

The zone output above looks quite messy, but the inner working is straight forward. 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
NSEC3 and NSEC3PARAM of lab.test

This list shows an NSEC3PARAM record and 8 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 file. 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, e.g. "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 and salt are used. The last important component of an NSEC3 record is the next hashed owner name, e.g. "0J6SCENU0P4HERE1AAKA0IEC1FLT3OU0".

For the calculation of the hashes, the values for 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 section below) concatenated with the raw salt bytes [3]:

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 RFC1035, Section 3.1 [4].

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 domain name into labels, using "." as a delimiter
  2. Estimate the length for each label and prepend the length to each label
  3. For the last label (null label of the root), add a zero-length to the sequence

An example for domain data.lab.test is shown below:

  1. data lab test
  2. \x04data\x03lab\x04test
  3. \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 demo purposes, we take a 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:

Linked list of chained NSEC records
Linked list of chained NSEC records

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 were developed, which are explained in the subsequent chapters.

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 slighly different approach has to be choosen.

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 error code NXDOMAIN and that is 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. Because of this behaviour a different approach for enumeration is necessary.

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", the second hash to "ww​w.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 rainbowtable 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 worked on this topic, developed a methodology to approximate zone sizes and found that "[...] in approximately 75% of cases, the methodology would yield a an estimate that is within 20% of the actual zone size, with only 18 queries." [5]

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 rainbowtables are leveraged to crack as many hashes as possible. Because the domain name is always included, custom rainbowtables need to be created for each domain.

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 rainbowtables.

Therefore, a couple of mechanisms were suggested that render zone walking impossible. While RFC4470 [6] and "Black Lies" describe a general approach to modify NSEC records, "White Lies" is a concept that is predominantly used with NSEC3 and also described in the following subsections.

RFC 4470

In order to not disclose actual next names within a DNS zone, RFC4470 suggests to "[...] list any name that falls lexically after the NSEC's owner name and before the next instantiated name in the zone. [...]".

In order to achieve this, DNS names need to be canonically ordered as per RFC4034 Section 6.1 [7].

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. RFC4470 does not suggest what epsilon function should be used, but provides an example on how such a function could look like:

Incrementing a name

"To increment a name, add a leading label with a single null (zero-value) octet." (RFC4470 Section 4)

Decrementing a name

"fill the leftmost label to its maximum length with zeros (numeric, not ASCII zeros) and subtract one." (RFC4470 Section 4)

Example

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 RFC4470 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 deals as a basis for the concepts of "White Lies" and "Black Lies" that are described in the following chapters.

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 RFC4470 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 RFC4470. 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 to 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

Like in the previous NSEC3 example, 3 NSEC3 records are sent back. Compared to regular NSEC3 handling, these look a bit different this time:

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 belong to an existing subdomain and could therefore be regarded as a fake response.

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 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 introduced by Cloudflare and implemented within their nameserver software RRDNS. The inner workings are described in their blog post [8]. 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 to be using DNSSEC with Black Lies as well [9]. Black Lies enables authenticated denial of existence for NSEC records, without revealing the actual zone contents.

The mechanism behind Black Lies is based on RFC4470 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" [10]. 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 a 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 a NSEC record would only contain the resource records that are present for the queried name. The decision to add the complete set of resource records 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." [8].

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, a NSEC record is returned that was constructed by appending a null-byte to the QNAME. Classic nameserver implementations would have returned a 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 to 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 describe 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." [8]

This has an implication for active subdomain enumeration techniques like subdomain bruteforce, 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 [1] 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, that 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 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.data-exfil.de nsec @ns-1341.awsdns-39.org
[...]
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 23030
[...]
doesnotexist.data-exfil.de.	86400	IN	NSEC	\000.doesnotexist.data-exfil.de. 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
[...]
level1.data-exfil.de.	86400	IN	NSEC	\000.level1.data-exfil.de. 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" [11].

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 the 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 - 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 "Empty Non-Terminal Sentinel for Black Lies" [11].

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 omit DNSSEC, by setting the "DO-bit" ("DNSSEC Okay") to zero:

DO-bit set to zero
DO-bit set to zero

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 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:

  • (CF, NS1, Route53) Black Lies prevent NSEC Walking efficiently
  • (CF, NS1, Route53) DNS status codes cannot be used to show a domains existence
  • (CF, NS1) Existence of domains could be induced by looking at the RR bitmap
  • (NS1) Existing record types could be identified using a single request
  • (NS1) ENTs could be identified using a single request
  • (Route 53) ENTs cannot be distinguished from non-existing nodes
  • 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:

  • amass [12]
  • dnsrecon [13]
  • dnssecwalk [14]
  • ldns [15]

NSEC3

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

  • nsec3map [16]
  • nsec3walker [17]

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 the own DNS zones are treated. Especially for internet-based, public zones, hiding sensitive or internal content behind hard-to-guess subdomains is security by obscurity, and 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 rainbowtables need to be constructed for each domain. Although in cryptography salts are used as an additional layer of protection, this does not apply for NSEC3, since domain names are hashed within the initial round of the hashing function and attackers need to create custom rainbowtables for each domain name anyway. [18]

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 is heavily driven by organizations like Cloudflare, Amazon and NS1 and would only be applicable if the zone is hosted by one of these providers.

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 bruteforce 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 bruteforce does not work any different as for zones without DNSSEC.

References

[1]: https://www.securesystems.de/blog/enhancing-subdomain-enumeration-ents-and-noerror/

[2]: https://www.rfc-editor.org/rfc/rfc4034#section-4

[3]: https://www.rfc-editor.org/rfc/rfc5155

[4]: https://datatracker.ietf.org/doc/html/rfc1035

[5]: https://casey.byu.edu/papers/2019_pam_dnssec_lies.pdf

[6]: https://www.rfc-editor.org/rfc/rfc4470

[7]: https://www.rfc-editor.org/rfc/rfc4034#section-6.1

[8]: https://blog.cloudflare.com/black-lies/

[9]: https://ns1.com/blog/ns1-announces-dnssec

[10]: https://datatracker.ietf.org/doc/html/draft-valsorda-dnsop-black-lies-00#section-2

[11]: https://www.ietf.org/archive/id/draft-huque-dnsop-blacklies-ent-01.html

[12]: https://github.com/OWASP/Amass

[13]: https://github.com/darkoperator/dnsrecon

[14]: https://github.com/vanhauser-thc/thc-ipv6

[15]: https://www.nlnetlabs.nl/projects/ldns/download/

[16]: https://github.com/anonion0/nsec3map

[17]: http://dnscurve.org/nsec3walker.html

[18]: https://www.rfc-editor.org/rfc/rfc9276

Bastian Kanbach
Bastian is part of our Offensive Security Team delivering tailored security assessments and Red Team exercises that fit the requirements of our clients. He specializes in network and infrastructure security.