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:
2
LICENSE
2
LICENSE
@@ -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
143
README.md
@@ -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
|
||||
|
||||

|
||||
|
||||
## [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
1436
dnssec_report.ndjson
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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
@@ -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
14
nsec3
@@ -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
378
nwalk
@@ -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
71
tldsec
Executable 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"
|
||||
Reference in New Issue
Block a user