#!/bin/sh # NSEC[3] Walking for DNSSEC Enabled Zones - Developed by acidvegas (https://github.com/acidvegas/nsecx) # Usage: # Walk a single domain: # ./nwalk # 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 or cat domain_list.txt | $0" && exit 1 nwalk_zone $1 else while IFS= read -r line; do nwalk_zone $line done fi