子域名枚举脚本工程化:从 4 万候选到 12 个真资产(个人开源工具,欢迎交流~)
上一篇讲了怎么把子域名枚举的假阳性从 85% 干到 0,核心是验证。这篇接着聊更实操的:怎么把枚举流程本身工程化。 内容包括五块硬货:一是预筛先行,30 秒判断域名值不值得跑,别盲目开炮;二是环境铁律,Kali 的 UDP 53 出站被限制,dig 必须加 +tcp,否则结果随机丢失;三是多轮枚举的核心逻辑,subfinder 被动挖内部名 + 分层爆破补通用名;四是候选爆炸控制,实测 53 个
子域名枚举脚本工程化:从 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 ,欢迎交流。