Combined nsec and nsec3 crawling into one unified script. validated and tested on zones with no nsec[3] and some gaps of no delegations, etc. major repo cleanup

This commit is contained in:
2026-03-22 00:40:33 -04:00
parent f5d7eb654f
commit 4bbd9e39fe
10 changed files with 1876 additions and 1699 deletions

View File

@@ -1,6 +1,6 @@
ISC License
Copyright (c) 2024, acidvegas <acid.vegas@acid.vegas>
Copyright (c) 2026, acidvegas <acid.vegas@acid.vegas>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above

143
README.md
View File

@@ -1,71 +1,104 @@
# NSECX
> Research project on NSEC[3] walking for DNSSEC enabled Zones
> Research project on NSEC/NSEC3 walking for DNSSEC enabled zones
![](./.screens/preview.gif)
## [Work in Progress]
## Overview
DNSSEC zone enumeration via NSEC & NSEC3 walking. This can be used to enumerate every single name in a domain's zone, including entire TLD zones. Automatically detects the DNSSEC type and proceeds accordingly.
The repository contains utilities for DNSSEC zone enumeration and subdomain discovery via NSEC/NSEC3 walking. It focuses on extracting and analyzing DNSSEC records for TLDs and specific target domains. Meant for educational purposes, security research, and sanctioned penetration testing, these tools aid in uncovering the underlying mechanisms of DNS security.
- **NSEC** zones are walked by following the chain and reconstructing the zone file in RFC 1035 format.
## DNSSEC Statistics
| Status | Percentage | TLDs |
| ---------------------------------------- | ---------- | ----- |
| [NSEC3](./dnssec_stats/nsec3.txt) | 90% | 1313 |
| [NSEC](./dnssec_stats/nsec.txt) | 3% | 51 |
| [NO DNSSEC](./dnssec_stats/nodnssec.txt) | 7% | 98 |
- **NSEC3** zones are enumerated by collecting hashed records into NDJSON for offline cracking.
###### These statistics are updated daily at midnight UTC via Github Actions.
There may be more efficient ways to do this. This project is a simple proof of concept designed to make the methodology clear for people to learn from.
## NSEC Pitfalls
- Results inconsistent, must hop dns servers on ALL issues to continue the crawl.
- Running into \000 *(null)* characters in sub-domains *(strange bind version [issue](https://gitlab.isc.org/isc-projects/bind9/-/issues/2779) missing "w" character in the charmap)*
## DNSSEC Statistics by TLDs
A `tldsec` script is also included that generates a DNSSEC statistics report for all TLDs *(see [dnssec_report.ndjson](dnssec_report.ndjson), last generated 2026-03-21)*.
| DNSSEC Status | TLDs | Percentage |
| ------------- | ---- | ---------- |
| NSEC3 | 1305 | 90.8% |
| NSEC | 53 | 3.7% |
| No DNSSEC | 78 | 5.4% |
## NSEC Walking
NSEC records form a linked chain of every name in a zone, sorted in canonical order. Each NSEC record points to the next name that exists. By querying a name's NSEC record and following the chain, you can enumerate every name in the zone from start to finish. The chain wraps back to the zone apex when the walk is complete.
- Running into *.domain.tld issues creates a crawling loop :
```
Next domain: myfreedom.auto.
Next domain: ne.auto.
Next domain: neom.auto.
Next domain: netdirector.auto.
Next domain: netprophet.auto.
Next domain: netto.auto.
Next domain: newjersey.auto.
Next domain: nexteer.auto.
Next domain: nextev.auto.
Next domain: nh.auto.
Next domain: nic.auto.
Next domain: *.nic.auto.
Next domain: _c311ff38bcd400b0adf7fa2b71732858.nic.auto.
Next domain: a.nic.auto.
Next domain: b.nic.auto.
Next domain: c.nic.auto.
Next domain: d.nic.auto.
Next domain: web1.nic.auto.
Next domain: web2.nic.auto.
Next domain: whois.nic.auto.
Next domain: _aa5536969dd3a62238209b6b2b750c1c.whois.nic.auto.
Next domain: www.nic.auto.
Next domain: _b529263a31adafb2e3be5d632e66c16b.www.nic.auto.
Next domain: nic.auto.
Next domain: *.nic.auto.
Next domain: _c311ff38bcd400b0adf7fa2b71732858.nic.auto.
Next domain: a.nic.auto.
Next domain: b.nic.auto.
Next domain: c.nic.auto.
Next domain: d.nic.auto.
Next domain: web1.nic.auto.
Next domain: web2.nic.auto.
Next domain: whois.nic.auto.
Next domain: _aa5536969dd3a62238209b6b2b750c1c.whois.nic.auto.
Next domain: www.nic.auto.
Next domain: _b529263a31adafb2e3be5d632e66c16b.www.nic.auto.
Next domain: nic.auto.
Next domain: *.nic.auto.
Next domain: _c311ff38bcd400b0adf7fa2b71732858.nic.auto.
$ dig +short ripe.net NS
ns3.lacnic.net.
ns3.afrinic.net.
ns4.apnic.net.
manus.authdns.ripe.net.
rirns.arin.net.
$ dig +short ns3.lacnic.net A
200.3.13.14
$ dig +short @200.3.13.14 ripe.net NSEC
1password-bridge-1.ripe.net. A NS SOA MX TXT AAAA RRSIG NSEC DNSKEY CAA
$ dig +short @200.3.13.14 1password-bridge-1.ripe.net NSEC
_acme-challenge.1password-bridge-1.ripe.net. A AAAA RRSIG NSEC
$ dig +short @200.3.13.14 _acme-challenge.1password-bridge-1.ripe.net NSEC
_00489a358ae3e70d565ae6ef7ddaa39d.ripe.net. CNAME RRSIG NSEC
...chain continues until it wraps back to ripe.net, completing the walk
```
## NSEC3 Walking
NSEC3 replaced NSEC to prevent direct zone enumeration by hashing the owner names. Instead of revealing plaintext names, NSEC3 records contain hashed names and point to the next hash in the chain. By querying random non-existent names, the authoritative server returns NSEC3 records in denial-of-existence responses. Collecting enough of these responses reveals all the hashes in the zone, which can then be cracked offline with a dictionary or brute force to recover the original names.
```
$ dig +short ac NS
a0.nic.ac.
a2.nic.ac.
b0.nic.ac.
c0.nic.ac.
$ dig +short a0.nic.ac A
65.22.160.1
$ dig +noall +authority +dnssec @65.22.160.1 doesnotexist.ac A
89L2PMI49P4NKDQKVOUN1DU1MLGH481V.ac. 3600 IN NSEC3 1 1 0 73 8A4STVTE8MVBGVSG7HLMDRQ9GEUANRR4 NS SOA RRSIG DNSKEY NSEC3PARAM
$ dig +noall +authority +dnssec @65.22.160.1 zzzznotreal.ac A
EFUMREL3KGT3GMAEFPAUALNNN64NPAML.ac. 3600 IN NSEC3 1 1 0 73 EI4FMHPDUQ8NEIGK8VEIATEDQUG12DF6 A AAAA RRSIG
```
Each query for a non-existent name returns NSEC3 records proving the name doesn't exist, leaking hashed names in the process. Repeat with random queries until no new hashes appear.
The NSEC3 record fields `1 1 0 73` are the NSEC3 parameters:
- **Algorithm** (`1`) — hash algorithm, 1 = SHA-1
- **Flags** (`1`) — opt-out flag, 1 = unsigned delegations may be omitted
- **Iterations** (`0`) — number of additional times the hash is applied, 0 = just once
- **Salt** (`73`) — hex-encoded salt appended before hashing
The hash is computed as `SHA1(salt + SHA1(salt + name))` repeated for the iteration count. These parameters are needed to crack the hashes offline with tools like hashcat *(mode 8300)*.
## Usage
```sh
# Walk a single domain
./nwalk <domain>
# Walk a list of domains
cat domain_list.txt | ./nwalk
# Walk a list of domains in parallel
parallel -a domain_list.txt -j 10 ./nwalk
# Walk all TLDs
curl -s 'https://data.iana.org/TLD/tlds-alpha-by-domain.txt' | tail -n +2 | tr '[:upper:]' '[:lower:]' | ./nwalk
```
## References
- https://www.rfc-editor.org/rfc/rfc5155.html
- [RFC 1035 - Domain Names: Implementation and Specification](https://www.rfc-editor.org/rfc/rfc1035.html)
- [RFC 4034 - Resource Records for the DNS Security Extensions](https://www.rfc-editor.org/rfc/rfc4034.html)
- [RFC 4035 - Protocol Modifications for the DNS Security Extensions](https://www.rfc-editor.org/rfc/rfc4035.html)
- [RFC 5155 - DNS Security (DNSSEC) Hashed Authenticated Denial of Existence](https://www.rfc-editor.org/rfc/rfc5155.html)
- [RFC 7129 - Authenticated Denial of Existence in the DNS](https://www.rfc-editor.org/rfc/rfc7129.html)
___
###### Mirrors for this repository: [acid.vegas](https://git.acid.vegas/nsecx) • [SuperNETs](https://git.supernets.org/acidvegas/nsecx) • [GitHub](https://github.com/acidvegas/nsecx) • [GitLab](https://gitlab.com/acidvegas/nsecx) • [Codeberg](https://codeberg.org/acidvegas/nsecx)
###### Mirrors for this repository: [SuperNETs](https://git.supernets.org/acidvegas/nsecx) • [GitHub](https://github.com/acidvegas/nsecx) • [GitLab](https://gitlab.com/acidvegas/nsecx) • [Codeberg](https://codeberg.org/acidvegas/nsecx)

1436
dnssec_report.ndjson Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,98 +0,0 @@
ae
al
ao
aq
as
ba
bb
bf
bi
bo
bs
bw
cd
cf
cg
ck
cm
cu
cv
cw
dj
do
eg
fk
ga
gb
ge
gf
gh
gm
gp
gq
gt
gu
hm
im
iq
jm
jo
kh
km
kn
kp
ls
mh
mk
ml
mo
mp
mq
ms
mt
mu
mv
mw
mz
ne
ng
ni
np
nr
pa
pf
pg
pk
pn
ps
qa
sd
sm
sr
st
sv
sy
sz
tc
td
tg
tj
tk
to
va
vi
xn--d1alf
xn--j1amh
xn--lgbbat1ad8j
xn--mgba3a4f16a
xn--mgbaam7a8h
xn--mgbayh7gpa
xn--mgbc0a9azcg
xn--mgbpl2fh
xn--mgbtx2b
xn--mix891f
xn--node
xn--ogbpf8fl
xn--wgbl6a
ye
zw

View File

@@ -1,51 +0,0 @@
arpa
audio
auto
ax
bd
br
bt
car
cars
ch
christmas
ci
diet
dz
ee
er
flowers
game
gdn
gn
gov
guitars
hosting
id
ir
kg
kz
lb
li
lk
lol
lr
mc
mom
nu
pics
pr
ruhr
se
sl
tn
tz
ve
xn--54b7fta0cc
xn--80ao21a
xn--fzc2c9e2c
xn--l1acc
xn--mgbai9azgqp6j
xn--pgbs0dh
xn--xkc2al3hye2a
xn--ygbi2ammx

File diff suppressed because it is too large Load Diff

View File

@@ -1,69 +0,0 @@
#!/bin/sh
# NSEC Statistics for TLDs - developed by acidvegas (https://git.acid.vegas/nsecx)
# tldsec
# This script will check the DNSSEC status of all TLDs and output the results separated by NSEC, NSEC3, and NODNSSEC.
# NSEC3 records will also include the NSEC3PARAM parameters for the zone as well for cracking in Hashcat.
# ANSI color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
PURPLE='\033[0;35m'
GRAY='\033[1;30m'
NC='\033[0m'
# Create the output directory if it doesn't exist
mkdir -p output
# Parse the tld list from a root nameserver
tld_list=$(dig AXFR . @g.root-servers.net | grep -E 'IN\s+NS' | awk '{print $1}' | sed 's/\.$//' | sort -u)
# Get the total number of TLDs, excluding comments and empty lines
total_tlds=$(echo "$tld_list" | grep -v '^#' | grep -v '^$' | wc -l | tr -d ' ')
# Initialize TLD count
current_tld=0
nsec_total=0
nsec3_total=0
nodnssec_total=0
# Read through each TLD in the list
echo "$tld_list" | while read -r tld; do
# Increase TLD count
current_tld=$((current_tld + 1))
# Convert TLD to lowercase using tr
tld=$(printf "%s" "$tld" | tr '[:upper:]' '[:lower:]')
# Check for DNSSEC records
output=$(dig +short ${tld}. DNSKEY)
if [ -z "$output" ]; then
nodnssec_total=$((nodnssec_total + 1))
echo "$tld" >> output/nodnssec.txt
else
nsec_output=$(dig +short ${tld}. NSEC)
nsec3_output=$(dig +short ${tld}. NSEC3PARAM)
if [ -n "$nsec_output" ]; then
nsec_total=$((nsec_total + 1))
echo "$tld" >> output/nsec.txt
elif [ -n "$nsec3_output" ]; then
nsec3_total=$((nsec3_total + 1))
nsec3_params=$(echo "$nsec3_output" | awk '{print $1,$2,$3,$4}')
echo "${tld}:${nsec3_params}" >> output/nsec3.txt
else
nodnssec_total=$((nodnssec_total + 1))
echo "$tld" >> output/nodnssec.txt
fi
fi
# Output the summarized status line with color
printf "\r${CYAN}%s/%s${NC} ${GRAY}|${NC} ${GREEN}NSEC: ${NC}%s ${GRAY}|${NC} ${YELLOW}NSEC3: ${NC}%s ${GRAY}|${NC} ${RED}NODNSSEC: ${NC}%s ${GRAY}|${NC} Checking ${PURPLE}%s${NC}... " \
"$current_tld" "$total_tlds" \
"$nsec_total" "$nsec3_total" "$nodnssec_total" "$tld"
done
echo "\nCheck completed! Data written to the output directory."

14
nsec3
View File

@@ -1,14 +0,0 @@
#!/bin/bash
# NSEC walk script for DNSSEC - developed by acidvegas (https://git.acid.vegas/nsecx)
# https://github.com/anonion0/nsec3map
#python3 -m pip install n3map[predict]
TARGET=$1 # Can simply be a TLD (no dot)
mkdir -p output
for NS in $(dig @8.8.8.8 +short NS $TARGET | sort -R); do
IPADDR=$(dig +short "$1" A || dig +short "$1" AAAA)
echo "Targeting $TARGET on $NS ($IPADDR)..."
n3map -avpl --output=output/$TARGET_$NS.txt $IPV4 --ignore-overlapping $TARGET
echo "-------------------------"
done

378
nwalk
View File

@@ -1,20 +1,16 @@
#!/bin/sh
# NSEC Walking for DNSSEC enabled zones - developed by acidvegas (https://git.acid.vegas/nsecx)
# NSEC/NSEC3 Zone Walking - developed by acidvegas (https://github.com/acidvegas/nsecx)
# Usage:
# NSEC walk on a single domain:
# Walk a single domain:
# ./nwalk <domain>
# NSEC walk on a list of domains:
# Walk a list of domains:
# cat domain_list.txt | ./nwalk
# NSEC walk on a list of domains using parallel:
# Walk a list of domains using parallel:
# parallel -a domain_list.txt -j 10 ./nwalk
# NSEC walk on all TLDs:
# Walk all TLDs:
# curl -s 'https://data.iana.org/TLD/tlds-alpha-by-domain.txt' | tail -n +2 | tr '[:upper:]' '[:lower:]' | ./nwalk
# NSEC walk on all PSL TLDs:
# curl -s https://publicsuffix.org/list/public_suffix_list.dat | grep -vE '^(//|.*[*!])' | grep '\.' | awk '{print $1}' | ./nwalk
# Colors
BLUE="\033[1;34m"
CYAN="\033[1;36m"
GREEN="\033[1;32m"
GREY="\033[1;90m"
@@ -24,99 +20,285 @@ RED="\033[1;31m"
YELLOW="\033[1;33m"
RESET="\033[0m"
nsec_crawl() {
domain=$1
domain=$(echo "$domain" | sed -e 's|^\(https\?://\)\?||' -e 's|^www\.||' -e 's|/.*||')
echo "${PINK}Looking up nameservers for ${CYAN}${domain}${RESET}"
nameservers=$(dig +short +retry=3 +time=10 $domain NS | sed 's/\.$//')
[ -z "$nameservers" ] && echo " ${GREY}No nameservers found for ${CYAN}${domain}${RESET}" && return
total_nameservers=$(echo "$nameservers" | wc -l)
echo " ${BLUE}Found ${total_nameservers} nameservers for ${CYAN}${domain}${RESET}"
ns_ip_list=""
for ns in $nameservers; do
echo " ${PINK}Looking up IP addresses for ${PURPLE}${ns}${RESET}"
ns_ip=$(dig +short +retry=3 +time=10 $ns A && dig +short +retry=3 +time=10 $ns AAAA)
[ -z "$ns_ip" ] && echo " ${GREY}No IP addresses found on ${PURPLE}${ns}${GREY} for ${CYAN}${domain}${RESET}" && continue
total_ip=$(echo "$ns_ip" | wc -l)
echo " ${BLUE}Found ${total_ip} IP addresses on ${PURPLE}${ns}${BLUE} for ${CYAN}${domain}${RESET}"
for ip in $ns_ip; do
ns_ip_list="${ns_ip_list}${ns} ${ip}\n"
done
done
[ -z "$ns_ip_list" ] && echo " ${GREY}No IP addresses found for ${CYAN}${domain}${RESET} nameservers" && return
total_ns_ip=$(echo -e "$ns_ip_list" | wc -l)
echo " ${BLUE}Found ${total_ns_ip} IP addresses for ${CYAN}${domain}${BLUE} nameservers${RESET}"
current_domain=$domain
count=0
error=0
ns=$(echo "$ns_ip_list" | shuf -n 1)
while true; do
[ -z "$nameservers" ] && echo "${GREY}No nameservers left for ${CYAN}${domain}${RESET}" && return
[ -z "$ns" ] && echo "${GREY}No nameservers left for ${CYAN}${domain}${RESET}" && return
ns_domain=$(echo $ns | awk '{print $1}')
ns_ip=$(echo $ns | awk '{print $2}')
nsec=$(dig +short +retry=3 +time=10 @${ns_ip} $current_domain NSEC | awk '{print $1}' | sed 's/\.$//')
if [ -z "$nsec" ]; then
error=`expr $error + 1`
if [ $error -eq 3 ]; then
echo " ${RED}Failed to communicate with ${PURPLE}${ns_domain} ${GREY}(${ns_ip})${RED} for ${CYAN}${domain}${RESET}"
nameservers=$(echo "$nameservers" | grep -v "$ns_ip")
ns=$(echo "$ns_ip_list" | shuf -n 1)
error=0
fi
continue
fi
error=0
[ "$nsec" = "$domain" ] || [ "$nsec" = "$current_domain" ] && break
case $nsec in "\000."*) break;; esac
count=`expr $count + 1`
echo "$nsec" >> "${output_dir}/nsec-${domain}.txt"
echo " ${GREEN}NSEC record for ${CYAN}${domain}${GREEN} from ${PURPLE}${ns_domain} ${GREY}(${ns_ip})${GREEN} found ${YELLOW}${nsec}${RESET}"
current_domain=$nsec
done
if [ $count -eq 0 ]; then
echo "${RED}No NSEC records found for ${CYAN}${domain}${RED} from ${PURPLE}${ns}${RESET}"
else
echo "${GREEN}Found ${count} NSEC records for ${CYAN}${domain}${RESET}"
fi
rand_str() {
head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' | head -c 12
}
relativize() {
sed -e "s/^${1}\./@ /" -e "s/\.${1}\.//g" | tr -s ' \t' ' '
}
print_records() {
label=$1
pad=$(printf '%*s' ${#label} '')
first=1
while IFS= read -r line; do
[ -z "$line" ] && continue
set -- $line
name=$1; ttl=$2; class=$3; type=$4; shift 4; rdata="$*"
if [ $first -eq 1 ]; then
printf "${PINK}%s ${CYAN}%s ${GREY}%s ${PURPLE}%s ${GREEN}%s ${YELLOW}%s${RESET}\n" "$label" "$name" "$ttl" "$class" "$type" "$rdata"
first=0
else
printf "%s ${CYAN}%s ${GREY}%s ${PURPLE}%s ${GREEN}%s ${YELLOW}%s${RESET}\n" "$pad" "$name" "$ttl" "$class" "$type" "$rdata"
fi
done
}
resolve_apex() {
ns_ip=$1
domain=$2
outfile=$3
apex=""
soa=$(dig +noall +answer +retry=10 +time=10 @${ns_ip} ${domain}. SOA 2>/dev/null | grep -v ';;' | relativize "$domain")
[ -n "$soa" ] && apex="$soa"
ns_records=$(dig +noall +answer +retry=10 +time=10 @${ns_ip} ${domain}. NS 2>/dev/null | grep -v ';;' | relativize "$domain")
[ -n "$ns_records" ] && apex=$(printf '%s\n%s' "$apex" "$ns_records")
if [ -n "$apex" ]; then
echo "$apex" >> "$outfile"
echo "$apex" | print_records "[apex]"
fi
}
resolve_name() {
name=$1
ns_ip=$2
outfile=$3
domain=$4
count=$5
result=$(dig +noall +authority +additional +retry=10 +time=10 @${ns_ip} ${name}. NS 2>/dev/null | grep -v ';;' | awk '$4 != "SOA"' | relativize "$domain")
if [ -z "$result" ]; then
result=$(dig +noall +answer +retry=10 +time=10 @${ns_ip} ${name}. NS 2>/dev/null | grep -v ';;' | awk '$4 != "SOA"' | relativize "$domain")
fi
if [ -z "$result" ]; then
result=$(dig +noall +answer +additional +retry=10 +time=10 ${name}. NS 2>/dev/null | grep -v ';;' | awk '$4 != "SOA"' | relativize "$domain")
fi
if [ -n "$result" ]; then
echo "$result" >> "$outfile"
echo "$result" | print_records "[$count]"
else
result=$(dig +noall +answer +retry=10 +time=10 @${ns_ip} ${name}. NSEC 2>/dev/null | grep -v ';;' | relativize "$domain")
if [ -n "$result" ]; then
echo "$result" >> "$outfile"
echo "$result" | print_records "[$count]"
else
relative=$(echo "$name" | sed "s/\.${domain}$//")
line="$relative 0 IN NSEC ?"
echo "$line" >> "$outfile"
echo "$line" | print_records "[$count]"
fi
fi
}
walk_nsec() {
printf " ${YELLOW}Detected NSEC${RESET}\n"
outfile="${output_dir}/${domain}.zone"
: > "$outfile"
current_domain=$domain
count=0
error=0
apex_done=0
while true; do
ns=$(printf '%b' "$ns_ip_list" | grep -v '^$' | shuf -n 1)
[ -z "$ns" ] && printf "${RED}No nameservers for ${CYAN}${domain}${RESET}\n" && return
ns_domain=$(echo $ns | awk '{print $1}')
ns_ip=$(echo $ns | awk '{print $2}')
nsec_raw=$(dig +short +retry=10 +time=10 @${ns_ip} $current_domain NSEC 2>/dev/null)
dig_rc=$?
nsec=$(echo "$nsec_raw" | grep -v ';;' | awk '{print $1}' | sed 's/\.$//')
if [ -z "$nsec" ]; then
if [ $dig_rc -eq 0 ]; then
label=$(echo "$current_domain" | sed "s/\.${domain}$//")
printf " ${YELLOW}NSEC gap at ${CYAN}${current_domain}${YELLOW} — jumping past${RESET}\n"
nsec=$(dig +noall +authority +dnssec +retry=10 +time=10 @${ns_ip} "${label}\\001.${domain}." 2>/dev/null | awk -v name="${current_domain}." '$1 == name && $4 == "NSEC" {print $5; exit}' | sed 's/\.$//')
if [ -n "$nsec" ]; then
current_domain=$nsec
error=0
continue
fi
printf " ${RED}Could not jump past ${CYAN}${current_domain}${RED} — zone walk incomplete${RESET}\n"
break
fi
error=$((error + 1))
[ $((error % 100)) -eq 0 ] && printf " ${RED}[${error}] Failed ${error} times on ${CYAN}${current_domain}${RED} — still retrying${RESET}\n"
continue
fi
error=0
if [ $apex_done -eq 0 ]; then
resolve_apex "$ns_ip" "$domain" "$outfile"
apex_done=1
fi
[ "$nsec" = "$domain" ] && break
case $nsec in "\000."*) break;; esac
if [ "$nsec" = "$current_domain" ]; then
label=$(echo "$current_domain" | sed "s/\.${domain}$//")
printf " ${YELLOW}NSEC gap at ${CYAN}${current_domain}${YELLOW} — jumping past${RESET}\n"
nsec=$(dig +noall +authority +dnssec +retry=10 +time=10 @${ns_ip} "${label}\\001.${domain}." 2>/dev/null | awk -v name="${current_domain}." '$1 == name && $4 == "NSEC" {print $5; exit}' | sed 's/\.$//')
if [ -n "$nsec" ] && [ "$nsec" != "$domain" ]; then
current_domain=$nsec
continue
fi
break
fi
count=$((count + 1))
resolve_name "$nsec" "$ns_ip" "$outfile" "$domain" "$count"
current_domain=$nsec
done
if [ $count -eq 0 ]; then
rm -f "$outfile"
printf "${RED}No NSEC records found for ${CYAN}${domain}${RESET}\n"
else
total_records=$(wc -l < "$outfile")
printf "${GREEN}Done — ${count} names, ${total_records} records written to ${outfile}${RESET}\n"
fi
}
walk_nsec3() {
printf " ${YELLOW}Detected NSEC3${RESET}\n"
algo=$(echo "$nsec3param" | awk '{print $1}')
flags=$(echo "$nsec3param" | awk '{print $2}')
iterations=$(echo "$nsec3param" | awk '{print $3}')
salt=$(echo "$nsec3param" | awk '{print $4}')
printf " ${GREEN}NSEC3PARAM: ${YELLOW}algo=${algo} flags=${flags} iter=${iterations} salt=${salt}${RESET}\n"
outfile="${output_dir}/${domain}.jsonl"
: > "$outfile"
query_count=0
printf " ${PINK}Gathering NSEC3 hashes...${RESET}\n"
while true; do
query_count=$((query_count + 1))
nsec3_records=$(dig +noall +authority +dnssec +retry=3 +time=5 @${detect_ns_ip} "$(rand_str).${domain}." A 2>/dev/null | grep ' IN NSEC3 ')
if [ -n "$nsec3_records" ]; then
echo "$nsec3_records" | while IFS= read -r line; do
[ -z "$line" ] && continue
hash=$(echo "$line" | awk '{print $1}' | sed "s/\.${domain}\.$//" | tr '[:lower:]' '[:upper:]')
next_hash=$(echo "$line" | awk '{print $9}' | tr '[:lower:]' '[:upper:]')
types=$(echo "$line" | awk '{for(i=10;i<=NF;i++) printf "%s ", $i}')
if ! grep -qF "$hash" "$outfile" 2>/dev/null; then
printf '{"hash":"%s","next":"%s","types":"%s","domain":"%s","nsec3param":"%s"}\n' "$hash" "$next_hash" "$types" "$domain" "$nsec3param" >> "$outfile"
cur_count=$(wc -l < "$outfile" | tr -d ' ')
printf " ${GREEN}[%s] ${CYAN}%s ${GREY}-> ${YELLOW}%s${RESET}\n" "$cur_count" "$hash" "$next_hash"
fi
done
fi
if [ $((query_count % 100)) -eq 0 ]; then
count=$(wc -l < "$outfile" | tr -d ' ')
if [ "$count" -gt 0 ]; then
missing=$(awk -F'"' '{hashes[$4]=1; nexts[$8]=1} END {for (n in nexts) if (!(n in hashes)) {print n; exit}}' "$outfile")
if [ -z "$missing" ]; then
printf " ${GREEN}Chain complete — ${count} hashes cover the full zone${RESET}\n"
break
fi
fi
fi
done
count=$(wc -l < "$outfile" | tr -d ' ')
if [ "$count" -eq 0 ]; then
rm -f "$outfile"
printf "${RED}No NSEC3 hashes gathered for ${CYAN}${domain}${RESET}\n"
else
printf "${GREEN}Done — ${count} unique NSEC3 hashes for ${CYAN}${domain}${RESET}\n"
printf "${GREY}Output: ${outfile}${RESET}\n"
fi
}
nwalk_zone() {
domain=$1
domain=$(echo "$domain" | sed -e 's|^\(https\?://\)\?||' -e 's|^www\.||' -e 's|/.*||')
printf "${PINK}Looking up nameservers for ${CYAN}${domain}${RESET}\n"
nameservers=$(dig +short +retry=3 +time=10 $domain NS | grep -v ';;' | sed 's/\.$//')
[ -z "$nameservers" ] && printf " ${GREY}No nameservers found for ${CYAN}${domain}${RESET}\n" && return
ns_ip_list=""
for ns in $nameservers; do
ns_ip=$(dig +short +retry=3 +time=10 $ns A 2>/dev/null | grep -v ';;' && dig +short +retry=3 +time=10 $ns AAAA 2>/dev/null | grep -v ';;')
[ -z "$ns_ip" ] && continue
for ip in $ns_ip; do
ns_ip_list="${ns_ip_list}${ns} ${ip}\n"
done
done
[ -z "$ns_ip_list" ] && printf " ${RED}No IP addresses found for ${CYAN}${domain}${RESET} nameservers\n" && return
dnssec_type=""
detect_ns=""
detect_ns_ip=""
nsec3param=""
for ns in $nameservers; do
probe_ip=$(dig +short +retry=3 +time=10 $ns A 2>/dev/null | grep -v ';;' | head -1)
[ -z "$probe_ip" ] && continue
nsec3param=$(dig +short +retry=3 +time=10 @${probe_ip} $domain NSEC3PARAM 2>/dev/null | grep -v ';;')
if [ -n "$nsec3param" ]; then
dnssec_type="nsec3"
detect_ns=$ns
detect_ns_ip=$probe_ip
break
fi
nsec_test=$(dig +short +retry=3 +time=10 @${probe_ip} $domain NSEC 2>/dev/null | grep -v ';;')
if [ -n "$nsec_test" ]; then
dnssec_type="nsec"
detect_ns=$ns
detect_ns_ip=$probe_ip
break
fi
done
if [ -z "$dnssec_type" ]; then
printf " ${GREY}No DNSSEC found for ${CYAN}${domain}${RESET}\n"
return
fi
if [ "$dnssec_type" = "nsec3" ]; then
walk_nsec3
else
walk_nsec
fi
}
# Set output directory
output_dir="nwalk_out"
mkdir -p $output_dir
if [ -t 0 ]; then
[ $# -ne 1 ] && echo "Usage: $0 <domain> or cat domain_list.txt | $0" && exit 1
nsec_crawl $1
[ $# -ne 1 ] && echo "Usage: $0 <domain> or cat domain_list.txt | $0" && exit 1
nwalk_zone $1
else
while IFS= read -r line; do
nsec_crawl $line
done
fi
while IFS= read -r line; do
nwalk_zone $line
done
fi

71
tldsec Executable file
View File

@@ -0,0 +1,71 @@
#!/bin/sh
# NSEC Statistics for TLDs - developed by acidvegas (https://github.com/acidvegas/nsecx)
# nsecx/extras/tldsec
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
PURPLE='\033[0;35m'
GRAY='\033[1;30m'
NC='\033[0m'
OUTPUT_FILE="dnssec_report_$(date +%Y-%m-%d).json"
TMP_FILE=$(mktemp)
echo "Fetching TLD list from IANA..."
curl -s 'https://data.iana.org/TLD/tlds-alpha-by-domain.txt' | tail -n +2 | tr '[:upper:]' '[:lower:]' > "$TMP_FILE"
total=$(wc -l < "$TMP_FILE" | tr -d ' ')
if [ "$total" -eq 0 ]; then
echo "Failed to fetch TLD list from IANA"
rm -f "$TMP_FILE"
exit 1
fi
echo "Got ${total} TLDs from IANA"
current=0
nsec_total=0
nsec3_total=0
skip_total=0
> "$OUTPUT_FILE"
while IFS= read -r tld || [ -n "$tld" ]; do
[ -z "$tld" ] && continue
tld=$(printf "%s" "$tld" | tr '[:upper:]' '[:lower:]')
current=$((current + 1))
printf "\r${CYAN}%s/%s${NC} ${GRAY}|${NC} ${GREEN}NSEC: ${NC}%s ${GRAY}|${NC} ${YELLOW}NSEC3: ${NC}%s ${GRAY}|${NC} ${RED}SKIP: ${NC}%s ${GRAY}|${NC} ${PURPLE}%s${NC}" "$current" "$total" "$nsec_total" "$nsec3_total" "$skip_total" "$tld"
dnskey=$(dig +short +time=10 +tries=10 "${tld}." DNSKEY 2>/dev/null | grep -v ';;')
if [ -z "$dnskey" ]; then
skip_total=$((skip_total + 1))
printf '{"tld":"%s","dnssec":null}\n' "$tld" >> "$OUTPUT_FILE"
continue
fi
nsec3param=$(dig +short +time=10 +tries=10 "${tld}." NSEC3PARAM 2>/dev/null | grep -v ';;')
if [ -n "$nsec3param" ]; then
nsec3_total=$((nsec3_total + 1))
params=$(printf '%s' "$nsec3param" | head -1 | awk '{print $1,$2,$3,$4}')
printf '{"tld":"%s","dnssec":"NSEC3","params":"%s"}\n' "$tld" "$params" >> "$OUTPUT_FILE"
continue
fi
nsec=$(dig +short +time=10 +tries=10 "${tld}." NSEC 2>/dev/null | grep -v ';;')
if [ -n "$nsec" ]; then
nsec_total=$((nsec_total + 1))
printf '{"tld":"%s","dnssec":"NSEC"}\n' "$tld" >> "$OUTPUT_FILE"
continue
fi
skip_total=$((skip_total + 1))
printf '{"tld":"%s","dnssec":null}\n' "$tld" >> "$OUTPUT_FILE"
done < "$TMP_FILE"
rm -f "$TMP_FILE"
printf "\n\nDone — NSEC: %s | NSEC3: %s | No DNSSEC: %s | Total: %s\nReport: %s\n" "$nsec_total" "$nsec3_total" "$skip_total" "$total" "$OUTPUT_FILE"