305 lines
9.4 KiB
Bash
Executable File
305 lines
9.4 KiB
Bash
Executable File
#!/bin/sh
|
|
# NSEC[3] Walking for DNSSEC Enabled Zones - Developed by acidvegas (https://github.com/acidvegas/nsecx)
|
|
|
|
# Usage:
|
|
# Walk a single domain:
|
|
# ./nwalk <domain>
|
|
# Walk a list of domains:
|
|
# cat domain_list.txt | ./nwalk
|
|
# Walk a list of domains using 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
|
|
|
|
CYAN="\033[1;36m"
|
|
GREEN="\033[1;32m"
|
|
GREY="\033[1;90m"
|
|
PINK="\033[1;95m"
|
|
PURPLE="\033[0;35m"
|
|
RED="\033[1;31m"
|
|
YELLOW="\033[1;33m"
|
|
RESET="\033[0m"
|
|
|
|
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 +noidnin +answer +retry=10 +time=10 @${ns_ip} -q "${domain}." -t SOA 2>/dev/null | grep -v ';;' | relativize "$domain")
|
|
[ -n "$soa" ] && apex="$soa"
|
|
|
|
ns_records=$(dig +noall +noidnin +answer +retry=10 +time=10 @${ns_ip} -q "${domain}." -t 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 +noidnin +authority +additional +retry=10 +time=10 @${ns_ip} -q "${name}." -t NS 2>/dev/null | grep -v ';;' | awk '$4 != "SOA"' | relativize "$domain")
|
|
if [ -z "$result" ]; then
|
|
result=$(dig +noall +noidnin +answer +retry=10 +time=10 @${ns_ip} -q "${name}." -t NS 2>/dev/null | grep -v ';;' | awk '$4 != "SOA"' | relativize "$domain")
|
|
fi
|
|
if [ -z "$result" ]; then
|
|
result=$(dig +noall +noidnin +answer +additional +retry=10 +time=10 -q "${name}." -t 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 +noidnin +answer +retry=10 +time=10 @${ns_ip} -q "${name}." -t 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 +noidnin +retry=10 +time=10 @${ns_ip} -q "$current_domain" -t 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 +noidnin +authority +dnssec +retry=10 +time=10 @${ns_ip} -q "${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 +noidnin +authority +dnssec +retry=10 +time=10 @${ns_ip} -q "${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
|
|
}
|
|
|
|
output_dir="nsecx_out"
|
|
mkdir -p $output_dir
|
|
|
|
if [ -t 0 ]; then
|
|
[ $# -ne 1 ] && echo "Usage: $0 <domain> or cat domain_list.txt | $0" && exit 1
|
|
nwalk_zone $1
|
|
else
|
|
while IFS= read -r line; do
|
|
nwalk_zone $line
|
|
done
|
|
fi
|