跳到正文

目录

Findomain:5 秒挖掘 8 万子域名——域名发现与监控从入门到精通

Findomain:用证书透明日志做被动子域名发现

快速信息卡

项目信息
GitHubFindomain/Findomain
Stars3,796+
Forks398+
LicenseGPL-3.0
语言Rust
最新稳定版10.0.1(2025-07)
最后更新2026-09-17(开发线 11.0.0-beta.2)

在子域名发现这件事上,工具可以分成两类:一类向目标发包,一类不向目标发包。Findomain 的主体属于后者,它解决的是"被动子域名发现"问题——通过查询公开的证书透明日志(Certificate Transparency Logs,CT)和第三方威胁情报 API,把已经存在过的子域名捞回来,全过程不向目标发送任何探测包。词表暴力枚举是一个可选补充(-w 参数),不开就是纯被动。

这种被动定位对应几类典型场景:Bug Bounty 猎人在 recon 阶段需要快速摸清目标资产面;企业安全团队需要监控自有域名的新子域名出现;红队在打点前需要被动收集而不暴露指纹。这些场景的共同诉求是快、广、不留痕。代价也明确——召回率受限于数据源,一个从未申请过证书、也从未被任何威胁情报平台收录的内网子域名,Findomain 发现不了。

学习目标

读完这篇,你将能:

  • 区分被动子域名发现和主动枚举的边界,并说明 CT 日志为什么是被动发现的首选数据源。
  • 解释 Merkle Tree 如何保证 CT 日志的不可篡改性和可审计性,以及 O(log N) 验证的具体含义。
  • 描述 Findomain 用"每个数据源一个线程"实现并发查询的工程机制,以及单请求超时在这套机制里的位置。
  • 复现 findomain -t example.com 从参数解析到结果输出的完整流程,指出通配符检测和 HashSet 去重的位置。
  • 设计一条 Findomain + dnsgen + massdns 的组合 recon 流水线,并说明每一步解决的是哪类子域名发现问题。

目录

子域名发现的三条技术路线

在进入 Findomain 之前,先把子域名发现的整体技术版图理清楚。目前主流的做法有三条路线,它们的边界很明确。

第一条是证书透明日志查询。CA 在签发证书时必须把证书提交到公开的 CT 日志,否则主流浏览器会拒绝信任。这意味着任何申请过 TLS 证书的子域名都会在 CT 日志里留下记录,而且是公开可查的。这条路线的特点是免费、无需授权、覆盖广,但只能发现"有证书的"子域名。

第二条是第三方 API 聚合。Virustotal、SecurityTrails、Urlscan.io 这类平台长期爬取和聚合互联网资产数据,它们的数据库里往往有大量历史 DNS 记录和扫描结果。这条路线能补充 CT 日志覆盖不到的部分(比如曾经解析过但没证书的子域名),但受限于 API 的速率限制和数据新鲜度,部分 API 还需要付费 key。

第三条是暴力枚举。拿一个字典文件去拼装候选子域名,然后逐个发 DNS 查询看是否解析。这条路线理论上能发现任何"曾经或正在解析"的子域名,包括内网子域名,但代价是流量大、噪音多、容易被目标感知。massdns 是这条路线的代表工具。

Findomain 的策略是前两条路线的组合:以 CT 日志为主力,辅以多个第三方 API,把暴力枚举作为可选补充。这个组合在大多数 recon 场景下能拿到 80% 以上的子域名,而且不向目标发包。

证书透明日志:为什么是首选数据源

要理解 Findomain 为什么把 CT 日志当主力,得先搞清楚 CT 日志到底是什么。

CT 日志的架构

证书透明(Certificate Transparency)是 Google 在 2013 年主导推动的开放框架,目的是解决 CA 错误签发或恶意签发证书的问题。在 CT 出现之前,如果一个 CA 被攻破或者操作失误,签发了一张 *.google.com 的证书给攻击者,外界几乎无法发现。CT 改变了这个流程:它要求 CA 把签发的每一张证书都提交到公开的、只能追加(append-only)的日志里,否则 Chrome 等主流浏览器会拒绝信任这张证书。

CT 日志的核心数据结构是 Merkle Tree(默克尔树)。每张证书被提交后,日志会把它作为一个叶子节点插入树中,然后重新计算从该叶子到根的哈希路径。整个树的根哈希会被公开发布并签名。这种结构保证了两个性质:第一,日志只能追加不能修改历史记录,因为任何修改都会改变根哈希;第二,任何人都可以通过验证一条 Merkle 路径来确认某张证书确实在日志里。这两个性质让 CT 日志成为可公开审计、不可篡改的证书数据库。

具体来说,Merkle Tree 的验证过程是这样的。假设日志里有 N 张证书,每张证书是一个叶子节点。叶子节点两两配对做哈希,得到 N/2 个中间节点;中间节点再两两配对做哈希,直到最后只剩一个根节点。要验证某张证书 C 确实在日志里,你只要拿到从 C 到根路径上的兄弟节点哈希(这叫 Merkle proof),然后本地重新计算根哈希,看是否和公开的根哈希一致。这个验证过程是 O(log N) 的,即使日志里有几亿张证书,验证也只要几十次哈希计算。

对子域名发现来说,这套设计带来的结果是:CT 日志公开、可程序化查询、不可篡改。任何人都能拉取整个日志或查询特定域名的记录,不需要授权,也不需要付费。CT 日志因此成了被动子域名发现最顺手的起点。

CT 日志生态里有几个角色分工。日志运营商(如 Google、Cloudflare、Sectigo)负责维护日志本身,接收 CA 提交的证书并更新 Merkle Tree。监控者(monitor)定期拉取日志的根哈希和新增条目,检测是否有可疑证书被签发(比如监控者发现了一张不属于域名所有者的证书,就可以告警)。审计者(auditor)验证日志的一致性,确保日志运营商没有篡改历史记录。这三个角色共同保证了 CT 日志的可信度。对于 Findomain 这类工具来说,它扮演的是"查询者"角色——不参与日志的维护和验证,只查询日志内容来发现子域名。

CT 日志的查询入口

CT 日志本身是分布式的,全球有多个日志运营商(Google、Cloudflare、Sectigo 等)各自维护日志。要查询某个域名的证书记录,不需要直接去遍历这些日志,有几个聚合查询入口可用。

crt.sh 是最常用的入口,由 Sectigo(原 Comodo CA)维护。它把多个 CT 日志的证书数据聚合到一个 PostgreSQL 数据库里,提供 SQL 查询接口和 Web 界面。访问 https://crt.sh/?q=example.com 就能看到所有包含 example.com 的证书记录,包括证书的 SAN(Subject Alternative Name)字段里列出的所有子域名。crt.sh 免费、无需 API key、覆盖最广,是 Findomain 的主力数据源。

crt.sh 还提供 JSON 输出接口,加 &output=json 参数就能拿到结构化数据。每条记录包含证书的 issuer_ca_id、common_name、name_value(SAN 列表,换行分隔)、entry_timestamp、not_before、not_after 等字段。Findomain 解析的就是 name_value 字段,把它按换行符拆开就得到子域名列表。

Certspotter 由 SSLMate 提供,是另一个 CT 日志监控和查询服务。它提供 REST API,免费账号每小时可以查询 100 次,付费档位限额更高但也并非无限制;不带账号只能做少量查询,适合个人评估。Certspotter 的优势是 API 响应结构化、适合程序化调用。它的 API 端点是 https://api.certspotter.com/v1/issuances?domain=<domain>&include_subdomains=true&expand=dns_names,返回的 JSON 里每条记录对应一张证书,dns_names 字段是子域名数组。

Facebook CT 是 Facebook 维护的 CT 日志查询接口,需要 Facebook 账号签发的 access token 才能使用。它的覆盖范围和 crt.sh 有重叠但也有差异,作为补充数据源有价值。配置比 crt.sh 复杂,但能补充一些 crt.sh 索引延迟的记录。

Findomain 接入的 CT 类数据源就是这三个:crt.sh、Certspotter、Facebook CT,把返回结果合并去重。

CT 日志的边界

CT 日志不是万能的。它只能发现"申请过证书的"子域名。如果一个子域名只跑了 HTTP(没上 TLS),或者用了自签名证书,或者只在内网解析,CT 日志里就没有它的记录。这就是为什么 Findomain 还要接入 Virustotal、SecurityTrails 这类 API——它们能补充 CT 日志覆盖不到的部分,比如曾经被扫描器扫到过但没证书的子域名。

还有一个细节:CT 日志的索引有延迟。CA 签发证书后,需要把证书提交到 CT 日志,日志运营商把它加入 Merkle Tree 后才会出现在查询接口里。这个延迟通常是几小时到一天,极端情况下可能更长。所以 CT 日志查不到"刚刚签发的"证书,只能查到"已经索引的"证书。

CT 日志的边界在这里:它免费、广覆盖、无需授权,是首选数据源,但不是唯一数据源。Findomain 的价值在于把多个数据源的结果去重合并,给出一个尽可能完整的子域名清单。

Findomain 的工程实现

Findomain 用 Rust 写成(GitHub 仓库里 Rust 占 97%)。子域名发现是典型的 I/O 密集型任务——大部分时间花在等 HTTP 响应和 DNS 解析上,CPU 计算很少。Rust 在这种场景下能提供接近 C 的性能,同时保持内存安全。

线程模型与并发控制

Findomain 的数据源查询没有用复杂的异步框架,而是选择了最直接的方案:每个数据源开一个独立的操作系统线程,线程里跑同步阻塞的 HTTP 请求。HTTP 客户端是 reqwest 的 blocking 模式,单个请求的超时是 60 秒。

这个设计的直接推论是:所有启用的数据源几乎在同一时刻发出请求,然后各自等各自的响应,源与源之间互不等待。README 给出的量级是"每个目标最多约 15 秒(API 超时的情况除外)"——这不是一个硬编码的超时值,而是"最慢的正常数据源返回所需时间"的经验上界。crt.sh 这类响应慢的源在实现里还有更细的控制:Findomain 直连 crt.sh 的 PostgreSQL 镜像时,把 SQL 语句超时设为 12 秒,避免一个卡住的查询拖住整个进程。

有个细节容易误解:Findomain 的依赖里有 tokio,但它只用在 DNS 解析环节(rusolver 内部),数据源查询本身是"线程 + 阻塞 I/O"。对一个十来个并发任务的 I/O 工具来说,线程方案的代码更简单、行为更好预测——不是所有并发场景都需要异步运行时。

子域名解析的并行化

子域名解析是另一个并行化点。Findomain 在拿到子域名清单后,会并行做 DNS 解析来验证哪些子域名当前是存活的。根据 README 的说明,在良好的网络条件下,Findomain 每分钟可以解析约 3500 个子域名;使用自定义 DNS 服务器时默认超过每秒 60 个,可以用 --threads 参数调整(这个参数已标记为废弃,后续版本由 --lightweight-threads 接管,默认 50)。

DNS 解析用的是 rusolver(也是 Findomain 作者 Edu4rdSHL 写的异步 DNS 解析库)。rusolver 是一个纯 Rust 的异步 DNS 客户端,支持自定义 DNS 服务器、UDP/TCP/DoT 协议。它的设计目标是高并发解析,和 Findomain 的需求匹配。

Findomain 还内置了通配符检测。当一个域名配置了通配符 DNS(比如 *.example.com 解析到同一个 IP),任何随机子域名都会解析成功,这会导致误报。Findomain 会在解析前先检测通配符——向几个随机生成的子域名(比如 a1b2c3d4e5.example.com)发查询,如果它们都解析到同一个 IP,就判定配置了通配符,后续解析结果会过滤掉这个 IP。这个检测避免了把随机字符串当成有效子域名。

速率限制处理

第三方 API 普遍有速率限制。Virustotal 免费 key 每分钟只能查几次,SecurityTrails 付费 key 也有每分钟请求数上限。Findomain 的处理方式是:在配置文件里填了某个数据源的 key,这个源就自动启用;key 留空,对应的源直接跳过;不想用某个源,用 --exclude-sources 显式排除。对免费 API,依赖并行查询和超时机制来规避速率问题——某个 API 响应过慢或失败时,它的线程返回空结果,其余数据源照常出结果。

这种处理方式的代价是:在速率限制严格的场景下,单个数据源可能拿不到完整结果。这也是 Findomain 接入十几个数据源的原因——冗余覆盖,单个数据源失败不影响整体召回。宁可多查几个数据源,也不在单个数据源的速率限制重试上浪费时间。

错误处理与配置管理

Findomain 的错误处理策略是"失败隔离"。每个数据源的查询是独立的线程,一个线程失败(网络错误、API 返回错误码、JSON 解析失败)返回空结果,不影响其他线程。最终结果只包含成功返回的数据源的子域名。这种设计让 Findomain 在部分数据源不可用时仍然能给出结果,而不是整体失败。

配置管理上,Findomain 支持 TOML、JSON、HJSON、INI、YAML 五种配置文件格式,这是为了适配不同用户的偏好。默认在当前目录查找名为 findomain 的配置文件(如 findomain.toml),也可以用 -c 参数或 FINDOMAIN_CONFIG_FILE 环境变量指定路径。配置文件里主要存放 API token 和 Webhook 地址。Findomain 也支持 FINDOMAIN_ 前缀的环境变量传入这些配置,这在 CI/CD 环境里更方便——不需要把 token 写进文件,直接从环境变量读取。

Facebook CT 的凭据要单独说一下。它在配置文件里对应 fb_token 键,填 Facebook 的 access token,可以填多个(逗号分隔)。token 会明文存在配置文件里,建议只在受控环境使用,并且把配置文件权限设为 600。

数据源:覆盖差异和 API 限制

稳定版(10.0.1)接入 13 个数据源,按覆盖类型分四类。它们的覆盖差异和限制直接决定 Findomain 在不同场景下的召回率。

CT 日志类:Crt.sh(无需 key,覆盖最广,是主力源——Findomain 优先直连它的 PostgreSQL 镜像,HTTP API 作为兜底)、Certspotter(token 可选,免费账号每小时 100 次)、Facebook CT(需要 fb_token)。这类数据源的覆盖范围取决于它们聚合了哪些 CT 日志,理论上应该高度重叠,但实际查询接口的索引延迟和查询能力有差异。Crt.sh 因为背后是 PostgreSQL,能做模糊匹配和历史回溯,覆盖最完整。

威胁情报类:Virustotal(virustotal_token,免费 key 可用但有严格速率限制)、Threatminer、Threatcrowd、FullHunt(需 key)、C99(c99_api_key)。这类数据源的价值在于聚合了多源数据——Virustotal 不仅有自己的扫描数据,还接入了大量第三方提交,能发现 CT 日志覆盖不到的子域名。比如某个子域名曾经被安全研究员提交到 Virustotal 做扫描,即使它没有证书,也会出现在 Virustotal 的数据库里。Threatcrowd 是个例外:这个服务 2017 年就并入了 RiskIQ 的 PassiveTotal,接口早已关闭,代码里保留的这个源实际拿不到数据,开发版已将它移除。

被动 DNS 与历史记录类:SecurityTrails(securitytrails_token)、Bufferover(free 和 paid 两个变体,各需 key)。这类数据源保存的是历史 DNS 解析记录,能发现曾经解析过但现在已经不存在的子域名。对资产盘点和历史攻击面分析特别有用。比如一个子域名 old-admin.example.com 曾经解析过,后来下线了,CT 日志里可能没有(如果没申请证书),但 SecurityTrails 的历史记录里会有。

聚合与扫描类:Sublist3r(本身就是一个聚合多个来源的工具,Findomain 直接调它的接口)、AnubisDB、Urlscan.io(网页扫描结果)。这些数据源的覆盖比较杂,作为补充。Urlscan.io 的特点是它记录的是用户主动提交的网页扫描结果,所以能发现一些被研究人员扫描过的子域名。

需要 key 才能启用的源有七个:Facebook、Virustotal、SecurityTrails、C99、Bufferover(free/paid)、FullHunt。key 在配置文件里留空,对应源自动跳过。

数据源的数量还在涨:开发线(11.0.0-beta)把数据源重写并扩展到了 54 个,同时集成了截图、端口扫描和 nuclei/ffuf 调用,Findomain 正在从"子域名发现工具"长成"recon 平台"。本文的机制描述以稳定版为准,新版本的行为以官方 README 为准。

实际使用中,Crt.sh + Certspotter + Threatminer 这几个免费源就能拿到大部分子域名。要追求极限召回,配上 Virustotal 和 SecurityTrails 的 key 会有明显提升。Crt.sh 通常贡献其中最大的一份,具体比例随目标域名而异——老牌大域名证书多,Crt.sh 占比高;新建或小众域名记录少,第三方 API 的补充作用更明显。

性能数据:5.5 秒 84110 子域名意味着什么

Findomain 的 README 里有一个常被引用的性能数据:5.5 秒发现 84110 个子域名。这个数字需要放在上下文里理解,不能直接拿来当通用基准。

测试条件来自 README:测试目标是 aol.com,测试环境是一台 BlackArch 虚拟机(KVM/QEMU,Intel Skylake 4 核 2.904GHz,3943MiB 内存),用 Linux time 命令测量,结果是 real 0m5.515s,CPU 和内存使用都标注为 “Very Low”。README 同时说明,这个测试用的是 Findomain 的 premium 版本,free 版本的表现可能不同。

这个数字测的是什么?它测的是 Findomain 向所有已配置数据源发起查询、拿到结果、去重合并的端到端时间。它不包含 DNS 解析验证的时间——84110 是"发现的子域名数量",不是"解析成功的子域名数量"。如果加上 -r(--resolved)选项做 DNS 验证,时间会显著增加,因为每分钟只能解析约 3500 个,84110 个子域名全部解析需要 20 分钟以上。

这个数字能推出什么?它能说明 Findomain 的多源并行查询设计是有效的——所有数据源同时发请求、互不等待,整体耗时由最慢的正常数据源决定,而不是所有查询时间之和。它不能推出 Findomain 在你的目标域名上的表现,因为 aol.com 是一个老牌大型域名,CT 日志和威胁情报平台对它的覆盖非常完整。换成一个中等规模的域名,发现数量会少很多,但时间也会更短。

这个数字不能推出什么?它不能推出 Findomain 比 massdns 快——两者测的不是同一件事。massdns 测的是 DNS 解析吞吐,Findomain 测的是 API 查询延迟。拿这两个数字直接对比没有意义。它也不能推出 Findomain 在所有网络环境下都这么快——如果你的网络到 crt.sh 的延迟很高,或者某个 API 触发了速率限制,时间会拉长。

正确的理解方式是:5.5 秒这个数字反映了 Findomain 在理想网络条件下、对高覆盖度域名的 API 查询延迟下限。实际使用中,对中等规模域名,Findomain 的典型耗时在几秒到十几秒之间,受最慢数据源的影响。

任务如何流过系统:findomain -t example.com 的完整流程

把前面的机制串起来,看一个完整的任务流程。假设你执行 findomain -t example.com,Findomain 内部会发生这些事。

第一步,Findomain 解析命令行参数,确定目标是 example.com。如果配置文件里填了 API token,会加载这些 token;没有填的源直接跳过。同时为后续的 DNS 解析准备解析器列表。

第二步,Findomain 为每个启用的数据源开一个线程,同时发起查询。对于 Crt.sh,它先尝试直连 crt.sh 公开的 PostgreSQL 镜像(用只读账号执行 SELECT cai.NAME_VALUE FROM certificate_and_identities ... 这类查询),失败时退回 HTTP API:请求 https://crt.sh/?q=%.example.com&output=json(% 在请求里匹配任意前缀,即 *.example.com),拿到一个 JSON 数组,里面每条记录对应一张证书,证书的 name_value 字段包含所有 SAN 子域名。对于 Certspotter,它请求 https://api.certspotter.com/v1/issuances?domain=example.com&include_subdomains=true&expand=dns_names。对于 Virustotal,它请求 https://www.virustotal.com/vtapi/v2/domain/report?apikey=<key>&domain=example.com。这些请求是并行的,Findomain 不会等一个完成再发下一个。

第三步,每个数据源的响应回来后,Findomain 解析响应体,提取子域名。Crt.sh 的响应里可能有 example.com、www.example.com、api.example.com 等等。Findomain 把这些子域名加入 HashSet,自动去重。如果同一个子域名被多个数据源返回(比如 www.example.com 同时出现在 Crt.sh 和 Virustotal 的结果里),HashSet 保证它只出现一次。这个去重是实时进行的——每个数据源的线程一返回就立即合并,不需要等所有数据源都返回。

第四步,所有数据源的线程都结束后,Findomain 得到一个去重后的子域名清单。如果你没有指定 -r(--resolved),Findomain 直接输出这个清单,任务结束。这就是 5.5 秒那个数字测量的范围。

第五步,如果你指定了 -r,Findomain 会对清单里的每个子域名做 DNS 解析。它会先检测通配符 DNS——向几个随机子域名(比如 random123abc.example.com)发查询,如果都解析到同一个 IP,说明配置了通配符,后续解析结果需要过滤掉这个 IP。然后用 rusolver 并行解析所有子域名,只保留解析成功的。如果你启用了 DNS over TLS(--enable-dot 参数),解析会走 DoT 通道,加密 DNS 查询,避免被中间人窥探。

第六步,输出结果。默认输出到 stdout,每行一个子域名。如果指定了 -o results.txt,写入文件。如果指定了 -i(--ip),额外输出每个子域名解析到的 IP 地址。结果也可以写入 PostgreSQL 数据库(监控模式依赖这一步)。

整个流程的关键设计点有三个:查询阶段全并行、去重用 HashSet、解析阶段有通配符检测。查询阶段的并行性让整体耗时只由最慢的数据源决定;HashSet 去重让结果干净;通配符检测避免了把随机字符串当成有效子域名的误报。

与 massdns、dnsgen 的组合使用

Findomain 拿到的是"曾经存在过"的子域名,但攻击面不止于此。一个成熟的 recon 流程通常是三个工具的组合:Findomain 发现种子子域名,dnsgen 基于种子生成变形候选,massdns 对候选做高速 DNS 解析验证。

dnsgen 的工作原理是拿种子子域名做词素变形。它把每个种子拆成词素(按 .、-、_ 和数字切分),再把这些词素两两组合、或与自带词库拼接,生成候选。比如种子里同时有 api.example.com 和 internal.example.com,dnsgen 会生成 api-internal.example.com、internal-api.example.com 这类组合;词素 api 也会拼出 api2.example.com、api-dev.example.com 这样的数字与环境前缀变体。种子里的真实命名习惯(dev/staging/prod、api/admin/portal 这类)决定了候选的质量——变形不是猜,而是把目标自己的命名词素重新组合。

massdns 的工作原理是高速 DNS 解析。它用自定义的网络栈绕过操作系统的 DNS 解析限制,能并发解析数万个域名。它的吞吐量远高于 Findomain 内置的解析器,因为它是专门为 DNS 解析优化的,而 Findomain 的解析只是辅助功能。massdns 的核心技巧是:自己构造 DNS 查询报文直接发送,绕过 libc 的 DNS 解析器(libc 的解析器有线程数限制和缓存开销),直接和递归 DNS 服务器通信。

组合使用的完整流程:

# 第一步:Findomain 被动发现种子子域名
findomain -t example.com -o subdomains.txt

# 第二步:dnsgen 基于种子生成变形候选
dnsgen subdomains.txt > mutations.txt

# 第三步:massdns 高速解析验证候选
massdns -r resolvers.txt -t A -o S mutations.txt > resolved.txt

# 第四步:从 massdns 输出里提取解析成功的子域名
awk '{print $1}' resolved.txt | sed 's/\.$//' > live_subdomains.txt

这个组合的逻辑是:Findomain 解决"已知子域名"的发现问题,dnsgen + massdns 解决"未知但可能存在的子域名"的发现问题。前者快但召回有限,后者慢但能发现 Findomain 漏掉的子域名。两者解决的是不同子集的问题,组合使用才能覆盖更完整的攻击面。

实际使用中,Findomain 的结果质量决定了 dnsgen 的种子质量。如果 Findomain 漏掉了一个关键子域名(比如 internal-api.example.com),dnsgen 就不会生成 internal-api-v2.example.com 这类变形。所以在大规模 recon 里,通常会同时跑 Findomain、subfinder、amass 这几个被动工具,把结果合并后再喂给 dnsgen。

一个更完整的 recon 流水线可能是这样的:

# 多工具被动发现,结果合并
findomain -t example.com -o findomain.txt
subfinder -d example.com -o subfinder.txt
amass enum -passive -d example.com -o amass.txt
cat findomain.txt subfinder.txt amass.txt | sort -u > seeds.txt

# dnsgen 变形 + massdns 解析
dnsgen seeds.txt > mutations.txt
massdns -r resolvers.txt -t A -o S mutations.txt | \
  awk '{print $1}' | sed 's/\.$//' | sort -u > live.txt

# HTTP 存活验证
cat live.txt | httprobe -c 50 > live_http.txt

# 漏洞扫描
nuclei -l live_http.txt -t cves/ -t exposures/

这个流水线里,Findomain 占的位置是"快速被动发现的第一步"——5 秒内拿到第一批种子,让后续步骤可以尽早开始。subfinder 和 amass 会补充更多种子,但它们更慢(amass 的被动枚举可能要几分钟)。

子域名监控:新子域名往往意味着新攻击面

Findomain 的监控功能(-m,即 --monitoring-flag)解决的是另一个问题:持续监控某个域名,当出现新子域名时通过 Webhook 告警。新子域名的出现往往意味着目标开了新业务、部署了新服务,而这些新服务往往还没经过完整的安全测试。

一个例子:一家公司在 example.com 下新部署了 staging-checkout.example.com,用于测试新的支付流程。这个 staging 环境可能用了弱认证、暴露了调试接口、或者运行着未打补丁的依赖。对 Bug Bounty 猎人来说,这种新出现的 staging 环境是高价值目标——它比生产环境更可能有问题,而且往往被忽视。对企业安全团队来说,监控自有域名的新子域名出现,等于监控了新的攻击面。

Findomain 的监控实现是周期性跑一次完整的子域名发现,把结果和数据库里上一次的结果做 diff,新增的部分通过 Webhook 推送。它支持 Discord、Slack、Telegram 三个渠道。Webhook 地址不通过命令行传,写在配置文件里:

# findomain.toml
discord_webhook = "https://discord.com/api/webhooks/xxx"
# slack_webhook = "https://hooks.slack.com/services/xxx"
# telegrambot_token = "123456:ABC-xxx"
# telegram_chat_id = "-100123456789"

监控模式需要在本地或远程跑一个 PostgreSQL 数据库,Findomain 把每个目标的历史子域名存在里面做对比(数据库连接用 --postgres-* 参数或 postgres_connection 配置键指定)。然后加 -m(--monitoring-flag)进入监控模式:

findomain -t example.com -m

这个机制的局限是:它依赖 Findomain 被定期触发(通常配合 cron),不是实时监控。CT 日志本身有索引延迟(从证书签发到可查询通常是几小时到一天),所以监控的端到端延迟是 CT 日志延迟加上 cron 间隔。对于需要分钟级告警的场景,Findomain 的监控不够实时,但对于日常 recon 已经够用。

一个典型的 cron 配置是每小时跑一次:

# crontab
0 * * * * /usr/local/bin/findomain -t example.com -m >> /var/log/findomain.log 2>&1

安装与基本使用

Findomain 提供多平台预编译二进制(Linux、macOS、Windows、ARM),macOS 也可以用 Homebrew。注意:brew install findomain 和 GitHub Releases 的 latest 下载拿到的都是稳定版 10.0.1,本文的命令行参数以它为准:

# Linux
wget https://github.com/Findomain/Findomain/releases/latest/download/findomain-linux.zip
unzip findomain-linux.zip
chmod +x findomain
sudo mv findomain /usr/local/bin/

# macOS
brew install findomain

基本使用命令:

# 基础发现(免费数据源)
findomain -t example.com

# 只输出解析存活的子域名
findomain -t example.com -r

# 额外输出每个子域名的 IP
findomain -t example.com -i

# 保存到文件
findomain -t example.com -o results.txt

# 解析存活 + DNS over TLS
findomain -t example.com -r --enable-dot

# 从文件读取多个目标
findomain -f targets.txt

# 词表暴力枚举(给 -w 自动进入暴力枚举模式)
findomain -t example.com -w wordlist.txt

# 排除指定数据源
findomain -t example.com --exclude-sources crtsh,certspotter

需要 key 的数据源不用额外开开关:在配置文件里填了 key 就自动启用,留空就跳过。Findomain 支持 TOML、JSON、HJSON、INI、YAML 五种配置文件格式,默认在当前目录查找 findomain.toml。以 TOML 为例:

# findomain.toml
virustotal_token = "your_virustotal_key"
securitytrails_token = "your_securitytrails_key"
c99_api_key = "your_c99_key"
bufferover_free_api_key = "your_bufferover_key"
fb_token = "your_facebook_access_token"

配置文件路径可以通过 -c 参数或 FINDOMAIN_CONFIG_FILE 环境变量指定,单个配置项也可以用 FINDOMAIN_ 前缀的环境变量覆盖(比如 FINDOMAIN_VIRUSTOTAL_TOKEN)。出于安全考虑,建议把配置文件的权限设置为 600(只有所有者可读写),避免 API token 泄露。

与其他被动发现工具的对比

Findomain 不是唯一的被动子域名发现工具。在这个领域里,subfinder 和 amass 是另外两个主流选择。三者各有侧重,选错工具会让 recon 流水线的某一段变慢或漏召回。

subfinder 是 ProjectDiscovery 出品的被动子域名发现工具,也是用 Go 写的。它的设计哲学和 Findomain 类似——多 API 聚合,不做主动枚举。subfinder 的数据源列表和 Findomain 高度重叠,但它和 ProjectDiscovery 生态(httpx、nuclei、subjs 等)无缝集成,管道操作很顺畅。

amass 是 OWASP 项目的子域名发现工具,功能最全面。它集成了被动发现、主动枚举、DNS 图分析、SSL 证书查询等多种技术。amass 的被动枚举比 Findomain 和 subfinder 更慢(因为它会做更深入的数据源查询和交叉验证),但召回率最高。amass 还能绘制子域名之间的关系图,适合做深度资产测绘。

三者侧重不同:Findomain 用 Rust 实现,启动快、内存占用低,适合放在 recon 流水线最前面快速拿种子;subfinder 和 ProjectDiscovery 工具链配合顺畅,适合后续管道操作;amass 做更深入的数据源交叉验证和图分析,适合需要完整资产测绘的场景。实际使用中,三者结果合并是常见做法——每个工具的数据源和查询策略有差异,合并能拿到最全的子域名清单。

从性能角度看,Findomain 的线程并行和 Rust 实现让它在启动时间和资源占用上有优势:Findomain 通常几秒到十几秒出结果,subfinder 同量级但通常更慢一些,amass 被动模式因为查询更深入、会做交叉验证,以分钟计。召回率上,amass 的深入查询通常能比单跑 Findomain 多发现一批子域名,幅度取决于目标域名的特征——老牌大型域名差异小,新建小众域名差异大。这两个工具没有官方的统一基准数据,具体数字以自己的目标实测为准。

采用边界与决策建议

Findomain 的能力边界由数据源决定。先分清它能不能用、用在哪儿,再按顺序决定怎么放进 recon 流水线。

能派上用场的场景

Bug Bounty recon 阶段是 Findomain 的主场。5 秒级响应让它适合作为 recon 流水线的第一步,先拿到一批种子再决定后续投入。企业自有资产监控也合适,配合 cron 定期跑,新子域名通过 Webhook 告警。红队被动信息收集同样适用——Findomain 全程不碰目标服务器,不会触发目标的入侵检测。大规模域名清单的初步筛选是另一个典型用法,从几千个域名里用 --quiet 模式快速过滤出有子域名的目标。

它解决不了的问题

内网子域名或无证书子域名的发现,CT 日志和威胁情报 API 都覆盖不到,得换 massdns 暴力枚举。子域名接管(subdomain takeover)检测不在 Findomain 的能力范围内,它只发现子域名,不检测 CNAME 悬挂,结果要喂给 subjack 或 nuclei 的 takeover 模板。非标准端口服务发现也不是它的事,它只发现子域名不做端口扫描,端口扫描要靠 nmap 或 rustscan。这些场景本身不指向 Findomain,直接用对应工具更省事。

决策顺序

把 Findomain 放进流水线,建议按下面顺序判断,每步都对应一种取舍。

先看任务要不要被动发现。 如果允许向目标发包(比如自己公司的资产盘点),可以直接用主动枚举工具,Findomain 不是必需。如果要求不暴露指纹(红队、Bug Bounty 初期),Findomain 是首选。

再看目标域名特征。 老牌大型域名(如 aol.com)CT 日志覆盖完整,单独跑就能拿到大部分子域名。新建或小众域名 CT 日志记录少,必须配合 subfinder、amass 多工具合并,再喂给 dnsgen + massdns 做变形枚举。

然后判断要不要监控。 一次性 recon 用 findomain -t 就够。持续监控用 -m 配合 cron 和 Webhook,但要接受小时级延迟——CT 日志的索引延迟、Findomain 的周期触发共同决定这个下限。需要分钟级告警的场景 Findomain 不合适,得考虑直接监控 DNS 区域传送或其他实时方案。

接着看召回率要求。 Bug Bounty 初期摸底,Findomain + subfinder 合并通常够用。要做完整资产测绘或合规审计,必须加上 amass 被动模式,再跑 dnsgen + massdns 主动变形。

最后组装流水线。 典型顺序:被动 recon 用 Findomain(快)+ subfinder(广)+ amass(深),结果合并去重;主动枚举用 dnsgen 变形 + massdns 解析,补被动工具漏掉的子域名;存活验证用 httpx 或 httprobe 探测 HTTP 服务,nuclei 跑漏洞模板;Takeover 检测用 subjack 或 nuclei 的 takeover 模板;端口扫描用 rustscan 快速扫全端口,nmap 做服务识别。

Findomain 在这条链上占的是"快速被动发现第一步"——5 秒内拿到第一批种子让后续尽早开始,召回率的极致靠 amass 和 dnsgen + massdns 补。

实战技巧

这些技巧来自日常排障和规模化使用里的常见坑。

处理 crt.sh 偶尔的超时。crt.sh 是 Findomain 的主力数据源,但它偶尔会因为负载高而响应慢(HTTP 接口返回 503 并不罕见)。Findomain 对它做了两层设计:优先直连 PostgreSQL 镜像,失败再退回 HTTP API。如果怀疑 crt.sh 拖慢了整体耗时,可以单独跑 curl 'https://crt.sh/?q=%25.example.com&output=json' 看接口状态,或者用 --exclude-sources crtsh 临时跳过它对比差异。

输出格式处理。Findomain 的输出是纯文本,每行一个子域名,适合管道操作;指定 -i 可以附带 IP。它没有 JSON 输出,要做结构化分析有两个选择:把结果写进 PostgreSQL(配合 -x 以 Findomain 自身为解析器等高级用法时顺带入库),或者用文本工具处理:

# 只保留解析成功且带 IP 的结果
findomain -t example.com -i -o resolved.txt

# 从文件里提取纯子域名列表(去掉 IP 部分)
sed 's/,.*$//' resolved.txt

大规模目标处理。如果要从文件读取几千个目标,用 -f 参数。但要注意 Findomain 是串行处理文件里的目标的(每个目标跑完再跑下一个),大规模场景下建议用 GNU parallel 并行化:

cat targets.txt | parallel -j 10 "findomain -t {} -o {}.txt"

避免触发目标告警。Findomain 本身不向目标发包,但如果启用了 -r,DNS 解析会发到递归 DNS 服务器,间接可能被目标感知(如果目标监控 DNS 查询日志)。要完全无感知,只用 findomain -t example.com,不加 -r。

结果去重和合并。多次跑 Findomain 或者和其他工具结果合并时,用 sort -u 去重:

cat findomain.txt subfinder.txt amass.txt | sort -u > all_subdomains.txt

监控多域名。如果要监控多个域名,Webhook 和数据库连接写一次配置文件,脚本只负责遍历目标:

#!/bin/bash
while read domain; do
  findomain -t "$domain" -m
done < targets.txt

注意监控模式下,Findomain 把每个域名的历史子域名存在 PostgreSQL 里用于 diff,数据库连接通过 --postgres-* 参数或配置文件的 postgres_connection 键指定。

常见问题

Findomain 跑出来结果很少怎么办? 首先检查是不是只用免费数据源。findomain -t example.com 默认只查免费源,要启用需要 key 的源,在配置文件里填好对应 token 即可,填了就自动生效。其次检查网络——crt.sh 偶尔会慢或返回 503,它拿不到数据,结果会少一大块。可以单独 curl 'https://crt.sh/?q=%25.example.com&output=json' 测试 crt.sh 是否正常。最后,如果目标域名很新或者很小众,CT 日志和威胁情报平台可能确实没有记录,这种情况下需要配合主动枚举工具。

Findomain 和 subfinder 哪个好? 两者定位不同。Findomain 用 Rust 写,启动快、内存占用低,适合作为 recon 流水线的第一步快速拿到种子。subfinder 用 Go 写,和 ProjectDiscovery 生态(httpx、nuclei)集成更好,适合后续管道操作。实际使用中两者结果合并是常见做法,因为它们的数据源和查询策略有差异,合并能提高召回率。

监控告警延迟很高怎么办? CT 日志本身有索引延迟(几小时到一天),这是机制限制,Findomain 无法绕过。如果对实时性要求高,需要配合其他监控手段,比如直接监控目标域名的 DNS 区域传送(如果允许)、或者用 zone transfer 监控。对于大多数 recon 场景,小时级延迟是可以接受的。

Findomain 会暴露我的 IP 吗? Findomain 本身不向目标发包,只向 CT 日志和第三方 API 发请求。所以目标看不到你的 IP。但你查询的第三方 API(Virustotal、SecurityTrails 等)会记录你的查询行为和 IP。如果需要匿名,走代理或者 Tor,但要注意这会显著增加延迟。

为什么 Findomain 发现的子域名解析不出来? CT 日志记录的是"曾经签发过证书"的子域名,不代表这个子域名现在还在用。很多子域名申请了证书但后来下线了,DNS 解析会失败。这就是为什么 -r(--resolved)选项有用——它过滤掉已经不存在的子域名,只保留当前存活的。但要注意,-r 会显著增加总耗时(每分钟约 3500 个)。

练习与自测

下面 5 道题用来检验对 Findomain 技术细节的掌握。每题先自己答,再对照参考答案。

题目 1:Findomain 的 5.5 秒 84110 子域名性能数据,测的是哪段时间?包含 DNS 解析验证吗?

题目 2:CT 日志的 Merkle Tree 如何保证不可篡改性?验证一张证书在日志里需要多少次哈希计算(数量级)?

题目 3:Findomain 用 HashSet 做去重,为什么选择 HashSet 而不是排序后去重?这个选择对实时合并有什么影响?

题目 4:-r(--resolved)选项启用后,Findomain 为什么要先向随机子域名(如 a1b2c3d4e5.example.com)发查询?这个检测解决的是什么问题?

题目 5:在一个 recon 流水线里,Findomain、subfinder、amass、dnsgen、massdns 各自解决哪类子域名发现问题?为什么不能只用其中一个?

参考答案与自检标准:

  • 题 1:测的是 Findomain 向所有已配置数据源发起查询、拿到结果、去重合并的端到端时间,不含 DNS 解析验证。84110 是"发现的子域名数量",不是"解析成功的子域名数量"。加上 -r 后,按每分钟约 3500 个的解析速度,84110 个全部解析需要 20 分钟以上。自检:能区分"API 查询延迟"和"DNS 解析吞吐"两个概念。
  • 题 2:Merkle Tree 的叶子节点是证书,两两哈希得到中间节点,直到根节点。任何历史记录的修改都会改变根哈希,而根哈希是公开发布并签名的,所以不可篡改。验证一张证书需要提供从该证书到根路径上的兄弟节点哈希(Merkle proof),本地重算根哈希比对,复杂度是 O(log N),几亿张证书也不过几十次哈希。自检:能说出"只能追加"和"O(log N) 验证"两个性质。
  • 题 3:HashSet 的插入和查询是 O(1),每个数据源的响应一回来就能立即合并去重,不需要等所有数据源都返回再排序。排序后去重是 O(N log N),且必须等全部结果到齐。HashSet 让 Findomain 可以流式合并,各源线程的返回顺序不影响结果的正确性。自检:能解释"流式合并"和"批量去重"的差异。
  • 题 4:通配符 DNS(如 *.example.com 解析到同一 IP)会让任何随机子域名都解析成功,导致误报。Findomain 先向几个随机子域名发查询,如果都解析到同一 IP,就判定配置了通配符,后续解析结果会过滤掉这个 IP。这个检测解决的是"把随机字符串当成有效子域名"的误报问题。自检:能说出通配符 DNS 的特征和过滤策略。
  • 题 5:Findomain、subfinder、amass 都是被动发现,靠 CT 日志和第三方 API 拿"曾经存在过"的子域名,三者数据源和查询策略有差异,合并能提高召回。dnsgen 把种子拆成词素再组合变形(如 api 与 internal 组出 api-internal、拼数字得 api2),解决"未知但可能存在"的子域名发现问题。massdns 对候选做高速 DNS 解析验证,吞吐量远高于 Findomain 内置解析器。不能只用其中一个:被动工具漏掉的子域名要靠 dnsgen + massdns 补,主动工具的种子质量又依赖被动工具的结果。自检:能区分"被动发现"和"主动枚举"的边界,并说明互补关系。

学习路径和资源

按"CT 日志机制 → 工具生态 → DNS 解析工程实现"的顺序深入,能从用到懂。

先理解证书透明日志的机制。Google 的 CT 官方文档(certificate-transparency.org)是权威来源,讲清楚了 Merkle Tree 日志的结构和验证机制。crt.sh 的查询界面(crt.sh)可以直接上手查任何域名的证书记录,感性认识 CT 日志能发现什么。如果想深入理解 Merkle Tree 的实现,可以读 RFC 6962(Certificate Transparency)规范,它定义了 CT 日志的数据结构和协议。

再理解子域名发现的工具生态。除了 Findomain,subfinder(ProjectDiscovery 出品)和 amass(OWASP 项目)是另外两个主流被动发现工具。subfinder 的设计哲学和 Findomain 类似(多 API 聚合),amass 更重,集成了主动枚举和图分析。对比这三个工具的实现,能看出不同设计取舍。subfinder 的源码用 Go 写成,可读性好,适合和 Findomain 的 Rust 实现对比。

最后理解 DNS 解析的工程实现。massdns 的 README 和源码值得读,它解释了如何绕过操作系统 DNS 解析栈的限制、自己构造查询报文直接发给递归服务器。dnsgen 的源码不复杂,读一遍能搞清楚子域名变形的常见模式。如果对异步 DNS 解析感兴趣,rusolver(Findomain 用的 DNS 库)的源码也是好材料,它展示了如何用 Rust 的 tokio 实现高并发 DNS 客户端。

进阶路径:读完上述材料后,可以尝试两个方向。一是改造 Findomain 的数据源列表,接入国内可用的 CT 查询入口或自建 CT 日志镜像,对比召回率变化。二是用 Rust 实现一个简化版的被动子域名发现工具,复现多线程并发查询、HashSet 流式去重、通配符检测这三个核心机制,理解工程取舍。

资源清单:

Findomain 的源码本身也是学习 Rust 并发工程的好材料。它用标准库线程做数据源并行,用 reqwest 的 blocking 客户端发请求,用 rusolver(内部是 tokio)做异步 DNS 解析。读它的 src/ 目录——数据源实现在 sources.rs,调度和解析在 networking.rs,结果输出在 logic.rs——能看到多数据源并发查询、结果聚合、通配符检测的完整实现。对于想理解"如何用 Rust 写一个高并发 I/O 工具"的开发者,Findomain 是一个规模适中、复杂度合理的参考项目:它不像生产级 Web 框架那么庞大,但涵盖了并发控制、错误隔离、配置管理等核心主题。

参与讨论

使用 GitHub 登录。欢迎补充事实、异议与实践。