One of the most relevant techniques during the reconnaissance phase of a security engagement is subdomain enumeration. Various tools, blog posts, and talks have covered different techniques in the past. This post won’t go into much detail about those, or how subdomain enumeration can be done in general. Instead, this post aims to enhance subdomain enumeration by including a special DNS node that is often ignored.
Generally, one may distinguish two types of subdomain enumeration:
- Passive subdomain enumeration
- Active subdomain enumeration
Passive subdomain enumeration can be performed by querying public information that is available in databases like censys.io, crt.sh, and so on. Passive subdomain enumeration is completely silent to the target since no DNS requests are sent at all.
In active subdomain enumeration, DNS queries are sent towards the nameserver of the target, in order to construct a list of valid subdomains. Simple queries like AXFR for DNS Zone Transfer target misconfigured DNS servers. Another active enumeration technique is called subdomain brute force, where large lists of subdomains are prepended to the target domain and sent to the resolver in order to retrieve DNS Resource Records (RR) like A for IPv4 addresses, CNAME for aliases or AAAA for IPv6 addresses. Further resource record types exist, however, which are not covered in this post.
While most tools that perform subdomain brute force focus on retrieving DNS RRs, a special type of node called Empty Non-Terminal (ENT) exists within the DNS that is rarely recognized by common enumeration tools. ENTs could be very helpful to find subdomains that might have remained hidden when focusing on RRs only.
Relevant status codes
The DNS defines a few status codes that indicate whether a DNS query was answered successfully. For everything related to this article, only the following two status codes are relevant: NXDOMAIN and NOERROR. NXDOMAIN indicates that the requested domain name either does not exist at all, or the name server is not aware of its existence. NOERROR is returned if the requested domain name is present within the DNS tree, no matter if the requested DNS Record Type exists. Alternatively, the status might appear as NODATA, when the response code is NOERROR, and no RR is set.
Empty Non-Terminals (ENT)
In the following examples, we control the domain exampledomain.test. If we wanted to add subdomains, there are two options:
- Add the subdomain to a separate zone, handled by a different nameserver
- Add the subdomain to the zone of exampledomain.test
In this example, we go for the second option, which means that we simply modify the zone file of exampledomain.test. This change consists of a new A record, referring to blog.dev.exampledomain.test and resolves to the IPv4 address 192.168.10.5. The resulting zone file is shown below:
;## test authoritative zone $ORIGIN test. $TTL 86400 @ IN SOA ns.test. webmaster.test. ( 2011100501 ; serial 28800 ; refresh 7200 ; retry 86400 ; expire 86400 ; min TTL ) NS ns.test. exampledomain IN A 192.168.10.1 www.exampledomain IN A 192.168.10.1 blog.dev.exampledomain IN A 192.168.10.5
Now that the zone file is in place, we can execute a few queries against the DNS server, starting with exampledomain.test:
dig @172.17.0.2 exampledomain.test ; <<>> DiG 9.16.1-Ubuntu <<>> @172.17.0.2 exampledomain.test ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 63605 ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 1 ;; WARNING: recursion requested but not available ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 1232 ;; QUESTION SECTION: ;exampledomain.test. IN A ;; ANSWER SECTION: exampledomain.test. 86400 IN A 192.168.10.1 ;; AUTHORITY SECTION: test. 86400 IN NS ns.test. ;; Query time: 0 msec ;; SERVER: 172.17.0.2#53(172.17.0.2) ;; WHEN: Mi Mai 18 10:08:38 CEST 2022 ;; MSG SIZE rcvd: 84
The query above returns NOERROR, which is the status code that indicates that the request was successful. Furthermore, the A record for exampledomain.test is sent back as a response. So far, this result is as expected.
Next, the subdomain blog.dev.exampledomain.test is queried:
dig @172.17.0.2 blog.dev.exampledomain.test ; <<>> DiG 9.16.1-Ubuntu <<>> @172.17.0.2 blog.dev.exampledomain.test ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 62456 ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 1 ;; WARNING: recursion requested but not available ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 1232 ;; QUESTION SECTION: ;blog.dev.exampledomain.test. IN A ;; ANSWER SECTION: blog.dev.exampledomain.test. 86400 IN A 192.168.10.5 ;; AUTHORITY SECTION: test. 86400 IN NS ns.test. ;; Query time: 0 msec ;; SERVER: 172.17.0.2#53(172.17.0.2) ;; WHEN: Mi Mai 18 10:21:48 CEST 2022 ;; MSG SIZE rcvd: 93
As in the previous example, the status of the query is NOERROR, which indicates that the node exists. Furthermore, the A record for the new subdomain is returned as well.
Now a third query is executed — this time against dev.exampledomain.test:
dig @172.17.0.2 dev.exampledomain.test ; <<>> DiG 9.16.1-Ubuntu <<>> @172.17.0.2 dev.exampledomain.test ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 21838 ;; 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: ;dev.exampledomain.test. IN A ;; AUTHORITY SECTION: test. 86400 IN SOA ns.test. webmaster.test. 2011100501 28800 7200 86400 86400 ;; Query time: 0 msec ;; SERVER: 172.17.0.2#53(172.17.0.2) ;; WHEN: Mi Mai 18 10:22:33 CEST 2022 ;; MSG SIZE rcvd: 104
The status of this query is NOERROR, like in the previous examples, although this subdomain was not explicitly added to the DNS zone file. Normally, for non-existing nodes the DNS server would return NXDOMAIN instead of NOERROR, however, since the DNS is hierarchical, all nodes between a leaf and the root node must exist — no gaps are allowed.
In this case, the leaf node is blog.dev.exampledomain.test — also known as a terminal, because no further nodes exist below this node.
The image below illustrates the DNS hierarchy:
The node dev.exampledomain.test is located on the path between exampledomain.test and blog.dev.exampledomain.test. The node dev was not explicitly defined within the zone file and added implicitly by the DNS server. Since no record types were defined for this node, it’s called an ENT.
The overall consensus that ENTs ‘exist’ wasn’t given for a long time. Although RFC 1034 section 4.3.3 (published in 1987) states that wildcards do not apply “When the query name or a name between the wildcard domain and the query name is know[n] to exist. […]” it never defined what ‘exist’ really means.
This issue was resolved with further clarification in RFC 8020 section 3.1, which states: “ENTs are nodes in the DNS that do not have resource record sets associated with them but have descendant nodes that do. The correct response to ENTs is NODATA (that is, a response code of NOERROR and an empty answer section).”
Another clarification was provided in RFC 4592 section 2.2.2: “The parenthesized ‘which may be empty’ specifies that empty non-terminals are explicitly recognized and that empty non-terminals ‘exist’.”
If all descendants of an ENT are deleted, the ENT node itself is deleted as well, as per RFC 2136 section 7.16: “There is no provision for empty terminal nodes — so if all RRs of a terminal node are deleted, the name is no longer in use, and queries of any type for that name will result in an NXDOMAIN response.”
Why ENTs matter
Even if an ENT does not contain record types like A, AAAA, CNAME, and so on, it still provides valuable information about the hierarchy of a DNS zone.
Assuming we discover a domain like dev.exampledomain.test, and the DNS server returns NOERROR for this domain, what implications does that have?
- exampledomain.test exists
- RRs might exist for dev.exampledomain.test
- Further subdomains might exist below dev.exampledomain.test
While the first implication is trivial, the second implication indicates that other record types might exist. That means if a query for A records in dev.exampledomain.test is sent to the server and no records are present, but NOERROR is returned, it could be worth checking other common record types for this domain as well (A, AAAA, CNAME, SRV, NS, MX, TXT, DS, and so on).
The third implication is a very interesting one — if no RR is defined for this node, the node definitely contains further nodes within the DNS hierarchy if the DNS server hosting the requested zone is compliant with the RFCs. In our example, the subdomain blog exists below dev.exampledomain.test. We know that because we control the zone file.
How could this be leveraged when performing reconnaissance and active subdomain enumeration?
Figure 2 illustrates the same zone, but without knowledge about the zone layout:
Here we assume that the nodes www, dev, and blog.dev are already known. If the DNS zone is not known, each node could contain other nodes and should be inspected further.
A leaf node (or terminal) does not contain nodes by itself and it is not allowed to be empty. In other words, if active discovery techniques like subdomain brute force reveal a node without any RR, by definition the node can’t be a leaf node, since leaf nodes are not allowed to be empty. However, if a node is discovered that contains RRs, it is not possible to determine whether the node is a non-terminal or a leaf node.
Scanning technique and support by tools
In order to enhance the process of subdomain enumeration and achieve good coverage, the focus should be on the DNS response code rather than on resolving A, AAAA or CNAME records only.
The following steps describe a process to recursively enumerate subdomains, starting with domain.tld:
- Add domain.tld to the queue
- Perform a subdomain brute force attack for the next item in the queue and add subdomains with DNS response code NOERROR (or NODATA) to the queue
- Remove the scanned domain from the queue
- If the queue is not empty, go to step 2
There are some caveats that should be considered when looking for NOERROR or NODATA responses.
A caveat: Technically, NODATA is not a dedicated response code but applies when the response code is NOERROR and no RRs are defined for the node. In environments where DNSSEC is used, NODATA responses contain NSEC or NSEC3 records instead of being empty. NSEC and NSEC3 records disclose information about the zone and could be used for reconnaissance (NSEC walking) as well, however, this is beyond the scope of this post.
Another caveat is the concept of ‘Black Lies’, introduced by Cloudflare in DNSSEC environments, that turn NXDOMAIN responses into NOERROR or NODATA responses. Consequently, it is not possible to distinguish between ENTs and leaf nodes within DNSSEC-enabled zones, hosted by Cloudflare. Furthermore, DNS server implementations that deviate from the standards might also return NOERROR instead of NXDOMAIN. The next post in this series will discuss what techniques exist to enumerate subdomains in a DNSSEC-enabled zone and what countermeasures exist to prevent it.
Unfortunately, most tools focus on resolving A, AAAA, or CNAME records and would discard valid subdomains that return NOERROR and don’t contain any RRs that resolves to an IP address or hostname.
Table 1 shows common tools for subdomain brute force and whether the tools are capable of filtering for DNS response codes:
Massdns supports different output formats and options, as shown in the following excerpt:
[...] Output flags: L - domain list output S - simple text output F - full text output B - binary output J - ndjson output Advanced flags for the domain list output mode: 0 - Include NOERROR replies without answers. [...]
In order to detect ENTs and Non-Empty Non-Terminals without A, AAAA or CNAME records, the flags ‘L0’, ‘B’, ‘F’ and ‘J’ could be used, since the output includes responses with the NOERROR code.
In order to run massdns with the L0 flag, the following command could be used:
massdns -r resolvers.txt urls.txt -o L0 -w urls_resolved.txt
dnsx is very efficient when it’s chained with other reconnaissance tools like subfinder. Besides its ability to detect wildcards, dnsx supports the flag -rcode that includes responses with a certain response code. Since we are interested in all NOERROR responses, the following command could be used:
./dnsx -v -w subdomains.txt -d <domain> -rcode noerror
Most posts and tools that deal with active subdomain enumeration, or subdomain brute force in particular focus on resolving A, AAAA and CNAME records and tend to discard nodes without RRs, or with RRs like SRV, TXT, and so on. Consequently, some subdomains and, therefore, potentially interesting targets might be overlooked during the reconnaissance phase.
To enhance the process of subdomain brute force, it should be checked whether the DNS resolver responds with NOERROR. If that is the case the subdomain should be added to the list of targets. For these targets, all RRs should be queried to determine whether the node is empty or not.
Empty nodes are non-terminals by design and a separate brute forcing process should be started for these nodes, in order to find further nodes below them. If the node is not empty, it still makes sense to query all RRs to get a good overview.
If time allows, separate brute forcing processes could be launched against non-empty nodes, however since it’s not possible to distinguish between leaf nodes and non-empty non-terminals, most of the effort might be a waste of time without knowing it.
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.
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.