子域名枚举脚本工程化:从 4 万候选到 12 个真资产

上一篇写了怎么把子域名枚举的假阳性从 85% 干到 0,核心是"验证"。
这篇接着聊更实操的东西:怎么把枚举流程本身工程化——预筛、多轮、断点续跑、候选爆炸控制。
配套脚本都开源在 GitHub(subdomain-enum-toolkit),这里只讲思路和踩过的坑。

一、先说结论:别一上来就跑枚举

我一开始的流程是:拿到域名 → 直接上 subfinder + 大字典爆破。后来发现效率低到离谱——很多域名裸域连 A 记录都没有,跑几十分钟爆破纯属浪费时间。

所以现在第一步永远是预筛:30 秒内判断这个域名值不值得跑枚举。

预筛逻辑很简单,就四个判定:

判定

条件

怎么办

废弃

裸域无 A + 被动源 0 命中

直接跳过,跑了也白跑

半死

裸域无 A 但被动源有命中

验活命中再跑

有效

裸域有 A 记录

值得跑,资产常集中在裸域 IP 的非标端口

泛解析

随机子域能解析出 IP

爆破结果全假,跳过爆破只信被动收集

预筛脚本(prescreen.sh)就干四件事:

# 1. 查 DNS 基础信息(全部 dig +tcp,原因见下节)
dig +tcp NS 域名 @223.5.5.5
dig +tcp A  域名 @223.5.5.5
dig +tcp MX 域名 @223.5.5.5

# 2. 泛解析检测:3 次随机子域
rand="wc$(head -c 8 /dev/urandom | md5sum | cut -c1-8)"
wip=$(dig +tcp +short "$rand.域名" @223.5.5.5 2>/dev/null | grep -E '^([0-9]{1,3}\.){3}[0-9]{1,3}$')
# 能解析出 IP → 泛解析,爆破结果不可信

# 3. 被动源聚合:rapiddns + hackertarget + crt.sh 三源并行,整体限时 12 秒
(timeout 12 curl -s -m 10 "https://rapiddns.io/subdomain/域名?full=1" ...) &
(timeout 10 curl -s -m 8  "https://api.hackertarget.com/hostsearch/?q=域名" ...) &
(timeout 15 curl -s -m 10 "https://crt.sh/?q=%25.域名&output=json" ...) &
wait

关键点:三个被动源必须并行跑。串行的话,rapiddns 对不存在的域名会挂起很久,能把整个脚本拖死。每个源单独 timeout 包裹,谁卡了不影响别人。

二、环境铁律:Kali 的 UDP 53 出站是被限制的

这是最坑的一个发现。Kali VM 的 UDP 53 出站被防火墙限制,dnsx 默认用 UDP 查询,结果随机丢失——同一个域名查 3 次,结果完全不一样。

所以所有验证一律用 TCP:

dig +tcp +short +time=3 +tries=2 域名 @223.5.5.5 | grep -E '^([0-9]{1,3}\.){3}[0-9]{1,3}$'

massdns 这种纯 UDP 的工具在这个环境必死,别浪费时间。

resolver 用这批(实测过国内可达):223.5.5.5 / 114.114.114.114 / 119.29.29.29 / 8.8.8.8。注意 1.1.1.1 国内经常不通,9.9.9.9 部分域名解析不稳。

三、多轮枚举的核心逻辑

预筛通过后,上 multi_round_enum_v5.sh。两轮设计:

第 1 轮:subfinder 被动收集

  • 不碰目标,只查 CT 证书日志、公开 DNS、搜索引擎
  • 价值是挖出字典里没有的内部专属名:idp-eagleyun、zhaopin、aisvc 这种
  • 有个容易忽略的坑:种子域名本身必须加进验证候选。subfinder 只返回子域,裸域不会出现在输出里。如果裸域有 A 记录但没配任何子域解析,不加种子会把域名误判成"0 存活"。实测某家居企业裸域 → 222.x.x.x:81 上有 nginx,却曾被报告 0 存活。

第 2 轮:分层爆破

  • 第一层:主域 × 100 高频词(www/mail/api/admin...)快筛
  • 第二层:已发现的一级子域 × 字典套娃
  • 只对主域一级爆破,不对二级子域再套娃——8 个种子 × 5000 词 = 4 万个候选,这就是候选爆炸

自适应轮数:第 2 轮新增 > 50 自动扩到 5 轮,< 5 收敛到 2 轮。别死板固定轮数。

四、候选爆炸:最大的性能杀手

"种子 × 字典"全组合爆破是我踩过最大的坑。8 个种子 × 5000 词字典 = 4 万候选,每个候选 dig +tcp 带 3 秒超时,全跑完要几小时到一天。

真实案例:某啤酒企业 53 个种子 × 5000 词 = 14.5 万候选,10 并发 dig +tcp 跑 1 小时只出 1 个存活——大部分候选 NXDOMAIN 吃满 3 秒超时。

结论:第 1 轮被动收集已经挖到 50+ 高质量资产时,第 2 轮套娃边际收益极低,直接砍掉,或者只对高价值种子(dms/tms/crm)用小字典套娃。别让后台任务空转烧时间。

五、断点续跑:三个防呆设计

枚举经常跑一半断了(SSH 断了、timeout 掐了、手动 Ctrl+C),断点续跑做不好就是反复从头跑。

三个防呆点:

# 1. 已有结果保留不清空(别无条件 : > 清空)
[ -s "$ALL" ] || : > "$ALL"

# 2. 每轮开头清掉上一轮的临时文件
rm -f "$OUTDIR"/_cand_*.tmp "$OUTDIR"/_alive_*.tmp

# 3. resolver 实测失败不要覆盖原文件(DNS 抖动时列表会被清成 1 个)
# 实测 < 3 个就保留原列表

还有个排查顺序问题:结果丢了先看 all_subs.txt 是不是被清空了(断点续跑 bug),再看 _alive.tmp 有没有结果(被 timeout 掐断),最后看 resolvers.txt 是不是只剩 1 个。

旧版本残留的 all_subs.txt 是另一个坑:行数 ≈ 字典大小(5000/5002 行)说明是旧版未严格验活的候选,直接续跑会对 5000 个假种子全量套娃。重跑前先 rm -rf enum_<域名>/ 全新开跑。

六、验证标准:必须解析出合法 IPv4

存活判断必须是"解析出合法 IPv4",不是"有 DNS 应答"。

grep -qv '^$' 这种判断会把 CNAME / NOERROR 空应答当成存活——之前某家居企业 580 个结果里 85% 是假阳性,就是这个原因。

# 正确姿势
ip=$(dig +tcp +short +time=3 +tries=2 "$d" @223.5.5.5 | grep -E '^([0-9]{1,3}\.){3}[0-9]{1,3}$' | head -1)
[ -n "$ip" ] && echo -e "$d\t$ip"

双 resolver 重试:第一个超时/失败就换第二个,减少 DNS 抖动误判。

七、泛解析:别把"一个 IP 多个虚拟主机"当假阳性

泛解析检测要 3 次随机子域名(单次可能因 DNS 抖动漏判)。但更关键的是别误判:

某市医院枚举时第 2 轮爆破"发现"4460 个存活子域,明显异常。排查:

  • 随机子域全部解析到同一个 IP → 泛解析确认
  • 但这个 IP 上带不同 Host 头访问,响应完全不同:docker-registry→403,患者端→301,微信端→302 跳转

结论:这是同一台 nginx 跑多个虚拟主机,不是纯泛解析假阳性。 随机子域 403 是默认虚拟主机,业务 Host 有专属配置。

所以看到泛解析 IP 先别急着全删,带不同 Host 头测一遍,301/302 跳转 = 有真实站点。

八、被动源使用经验

源

特点

rapiddns

最稳,免限流,HTML 表格正则提取;对不存在的域名会挂起,必须 timeout

crt.sh

JSON 格式好解析,但会随机 502,还会批量限流(连续查几个域后全失败,退避重试 sleep 12*attempt)

hackertarget

返回"域名,IP"格式,直接能挖 IP

OTX alienvault

JSON 免 key,但连发必 429,只能单发

tscan 内置源

anubis/digitorus/dnsdumpster 常 403,commoncrawl 常 EOF,属正常,crtsh 源可用即可

批量收集时 rapiddns 是首选:实测一次查 7 个域 329 个子域全成功。

九、一个容易忽略的坑:CNAME 跨域残留

被动源命中 ≠ 本域资产。某企业 precreen 判定"半死"(裸域无 A 但被动源有命中),结果查了下命中的子域是 CNAME 跨域残留——vpn 子域的 A 记录查询返回的是另一域名。

验活时 dig +tcp +short 看到 xxx.其他域名 结尾就是 CNAME,归属要按目标域名重新确认,别把跨域残留当本域资产打。

十、脚本版本管理

只保留当前生效版本(v5),旧版本迭代脚本全部删掉,避免工作目录混乱、误用旧版本。enum_* 目标数据目录保留(含历史测试结果),脚本版本升级不影响断点续跑。

总结

原则

做法

预筛先行

30 秒判断值不值得跑,别盲目开炮

验证严格

dig +tcp,必须解析出合法 IPv4

被动打底、主动补深

第 1 轮 OSINT 挖内部名,第 2 轮爆破补通用名

控制候选爆炸

只对主域一级爆破,第 1 轮够了就砍第 2 轮

深度优先于广度

12 个高质量资产 > 500 个假阳性

断点可续

保留已有结果,清理临时文件,resolver 防呆

脚本在 https://github.com/WzzZh-zh/subdomain-enum-toolkit ,欢迎交流。