公司动态
OSINT实战:从whois到子域枚举,搭建最小开源情报收集管道
先聊一个很多安全初学者都会遇到的问题第一次听说 OSINT 时以为它就是“用几个现成工具去网上扒公开信息”。这个理解不能算错但真正动手之后你大概率会卡在两个地方工具跑出来几百条数据每条看起来都像线索组合在一起却得不出结论换一个新目标之后之前写过的脚本、调过的参数、输出的结果又要全部重新来一遍根本没有沉淀。这篇以 K2SOsint / Legendary_OSINT 为引子的文章想讲清楚一个比“装工具”更重要的问题OSINT 的核心门槛不在于“能不能查到信息”而在于“能不能把信息收集做成一条可复用、可验证、有边界的情报生产线”。开源情报不是一堆工具集的堆叠它本质上是一条把公开数据转换为结构化情报结论的管道。读完这篇文章你会理解 OSINT 完整的信息生命周期会拿到一套包含 whois、DNS 记录、证书透明日志和子域枚举的最小可运行 Python 脚本也会知道在合法合规的前提下如何把这些能力应用到安全测试、暴露面管理和应急响应工作中。全文约 6000 字建议收藏后按照章节逐个实践。1. 这篇文章真正要解决的问题OSINT 在真实工程场景里经常出现在四类需求中。第一类是红队或渗透测试的前期信息收集。在拿到授权之后测试人员需要尽快摸清目标网络资产的暴露面有哪些域名、子域、IP、服务历史上有哪些信息泄露哪些公开仓库可能泄露了内部代码。第二类是蓝队和甲方安全团队的暴露面自查定期外查自己的域名证书、DNS 配置、GitHub 仓库是否出现敏感信息。第三类是威胁情报分析需要关联攻击者基础设施、恶意域名、证书指纹等线索。第四类是企业合规或账号安全自查比如检查企业员工是否使用泄露的邮箱密码。这些场景的共同点是信息都是公开可得的但散落在不同数据源里需要有一致的方法论和工具链把它们串起来。不过这里有一个最常见的误区。很多人以为“OSINT 工具越多越好”于是把 GitHub 上能搜到的工具全克隆一遍last seen 了十几个仓库真正该回答的问题一个都没回答。更糟糕的是如果放错了边界OSINT 很容易滑向“人肉搜索”或者“非法收集个人信息”的灰色地带。中国网络安全法、个人信息保护法以及各地对公开数据抓取的限制决定了 OSINT 实践必须在授权范围内进行而且必须以“最小必要”为原则。因此这篇文章真正要解决的问题是怎样正确理解 OSINT 的方法论怎样搭建一套最小但完整的 OSINT 工具链以及怎样在合规前提下把它落地到自己的项目或团队工作中。适合安全工程师、渗透测试人员、基础平台研发和刚开始接触威胁情报的读者阅读。2. OSINT 基础概念与核心原理先给一个稳定的定义OSINT英文全称 Open Source Intelligence中文译为开源情报或公开来源情报指从公开、合法、可访问的来源中获取信息并经过处理、分析和验证之后形成可用情报的过程。这里“公开来源”包括但不限于网站、DNS、whois 数据库、证书透明日志、社交平台公开页面、GitHub 仓库、网盘分享、历史快照、新闻和论坛。OSINT 最接近的类比是破案警方侦查员不会只看一张照片而是会收集现场脚印、目击者描述、监控录像、通话记录、交易流水再把碎片信息拼起来形成一个可验证的完整链条。一个人在公开网络上留下的数字脚印往往是零散的单独看价值有限组合起来却能暴露目标资产的真实面貌。理解 OSINT必须抓住两个核心概念。第一个核心是信息生命周期。OSINT 不是“收集完就结束”而是有完整流程的工作需求定义明确这次调查到底要回答什么问题。问题越具体后面的数据源选择越准确。来源识别根据需求找出哪些公开来源可能包含相关信息。数据收集通过接口、爬虫或查询工具把数据源里的原始数据抓取回来。数据处理去重、清洗、格式化、去噪把不同格式的数据规整成统一结构。分析与验证把处理后的数据关联起来识别模式和关联并通过其他来源验证结论的可靠性。交付与归档输出结构化报告并把原始数据、查询脚本、运行日志归档确保可审计。很多人拿着工具跑几个命令就觉得自己在“做 OSINT”其实那只是走到了“数据收集”这一步后面的处理和分析才是决定情报价值的环节。第二个核心是主动收集与被动收集的差别。被动收集是对目标本身不产生交互请求的收集方式比如查询证书透明日志、DNS 历史记录、whois 数据库、搜索引擎缓存。这种方式的优点是隐蔽、不容易被目标发现而且不会增加目标服务器的负载也更符合最小化原则。主动收集则需要直接向目标发起请求比如访问目标网站、对目标域名做子域枚举的 DNS 查询、扫描目标 IP 的开放端口。主动收集的信息往往更实时、更深入但会给目标留下访问痕迹也更容易触发防护设备告警。实际的 OSINT 实践中应该优先被动后主动严格控制主动请求的频率和并发量。按数据来源划分常见 OSINT 数据类型可以分成几类数据类别典型来源典型用途域名基础设施whois、DNS、注册局了解域名注册者、注册商、NS、MX、TXT域名延伸资产证书透明日志、子域爆破、DNS 历史发现未公示的子域和测试环境暴露面网络基础设施IP 归属查询、路由信息、端口指纹定位 CDN、云厂商、服务器地理区域代码与文档泄露GitHub 搜索、公开网盘、文档分享发现硬编码密钥、内部路径、配置文件组织与人员工公开页面、社交平台、新闻用于授权范围内的组织关系与账号安全分析历史数据网页快照、域名历史、泄露库挖掘已下线或被删除的痕迹理解这些数据源之后再回头看 K2SOsint / Legendary_OSINT 这类项目会有完全不同的感受。3. K2SOsint / Legendary_OSINT 代表的工具化趋势从项目命名和 GitHub 上同类 OSINT 工具集的形态来看K2SOsint / Legendary_OSINT 更像是一个将多种情报源整合在一起的工具或资源集合面向的是需要快速、批量、结构化收集信息的用户。这里不讨论它某一个具体版本的功能细节因为项目可能持续更新不同时间看到的界面和命令都不一样。更有价值的做法是解构这类项目到底代表了什么。这类项目通常会把前面提到的数据源拆成若干模块。最常见的模块划分是域名信息模块调用 whois 服务获取域名注册信息、注册商、创建时间、过期时间、NS 记录。DNS 侦察模块查询 A、AAAA、NS、MX、TXT、CNAME 记录用于理解目标域名的解析结构和邮件服务配置。子域发现模块结合证书透明日志、DNS 爆破、搜索引擎、DNS 历史数据库找出目标域下所有可解析的子域。服务与端口模块对已发现的 IP 做端口识别和服务指纹识别判断是否有非预期服务的暴露。GitHub/代码泄露模块通过 GitHub 代码搜索 API 或其他公开接口检查是否有人在代码中提交了目标域名的配置、密钥或内部路径。历史快照模块使用网站快照服务查看目标网页在历史时间点的内容变化。需要特别强调的是模块齐全并不代表情报质量高。这类项目的真正价值在于它把“收集线索”和“组织线索”两件事做成了可重复执行的脚本输入一个目标域名输出一份结构化报告下次再来目标时直接拿上次的结果做对比。这比每次临时敲十几条命令要可靠得多。从工程视角看这类工具集的兴起反映了 OSINT 领域一个明显趋势安全团队不再满足于“有没有信息”而在意“信息能否批量、持续、自动化地产出”。暴露面分析从一次性项目变成了需要周期更新的常态化工作因此工具必须模块化、可编排、可复用。这也解释了为什么 Censys、Shodan、crt.sh、SecurityTrails 这些数据源的反查能力远比单一命令重要。所以与其纠结某一天某个工具仓库更新了什么不如把注意力放在它的管道设计上数据从哪里来如何清洗如何存储如何输出。下面的章节就用一个最小项目把你自己的 OSINT 管道搭出来。4. OSINT 环境准备与前置条件在开始写代码之前先确认环境。这个最小项目的技术栈非常轻量只依赖 Python 3 和三个第三方库。建议使用 Python 3.10 或更高版本不同小版本不影响本示例逻辑。需要安装的依赖如下pip install requests dnspython python-whois如果你有多个 Python 项目不建议直接往全局环境里装依赖最好先创建一个独立虚拟环境python3 -m venv venv source venv/bin/activate # Windows 下运行 venv\Scripts\activate pip install requests dnspython python-whoisrequests用于请求 crt.sh 等 HTTP API。dnspython用于执行 A、MX、NS、TXT 等 DNS 记录查询。python-whois用于解析 whois 服务返回的域名注册信息。准备一个子域名字典文件subdomains.txt用于最后的子域枚举。字典内容是一行一个子域前缀先放一个最小示例www api app blog mail dev test stage admin ftp建议目录结构如下osint-lab/ ├── venv/ ├── subdomains.txt ├── osint_collector.py └── osint_result.json环境完备的同时还必须完成一项重要前置工作确认授权。OSINT 信息收集涉及目标系统、目标组织甚至目标个人的数据在动手之前请确认三件事目标的所有者是谁是否已获得书面授权授权的范围是否包含你计划测试的域名、IP 段和方式收集的数据如何使用、保存多久、是否会进行去标识化处理。明确边界不是过度谨慎而是这项技术能长期使用的根本保障。下面的示例代码全部写明了“仅用于已授权目标”请在实际工作中严格遵守。5. 核心流程拆解从域名到暴露面的情报收集接下来的内容我们以“你拥有或已获得授权测试的某个域名”为前提从零开始走一遍 OSINT 的核心流程。这个过程会从公开数据源中提取四类信息whois 注册信息、DNS 记录、证书透明日志中的历史域名、以及子域爆破结果。5.1 第一步收集 whois 注册信息whois 是查询域名注册信息的标准协议。通过 whois可以获得域名注册商、创建时间、过期时间、当前 NS 服务器等基础数据。这些数据对判断目标域名的使用年限、托管位置和基础架构非常有帮助。在 Python 里使用python-whois库import whois w whois.whois(example.com)需要留意的是这个库对部分域名可能返回空值或无法解析的字段实际使用时必须做异常处理。另外某些顶级域名会隐藏注册人信息查询不到注册人是很正常的现象不代表脚本出错。真正容易踩坑的地方在于不少用户把“查询不到注册人”当成失败状态然后反复重试反而触发 whois 服务器限流。5.2 第二步查询 DNS 记录DNS 记录是理解目标域名与基础设施关系的重要入口。A 记录指向服务器 IPNS 记录指向域名的 DNS 服务商MX 记录指向邮件服务TXT 记录中经常包含 SPF、DMARC、域名验证等配置信息很多安全团队还会把内部服务登录地址以 CNAME 或其他记录暴露出来。使用dnspython可以一次性查询多种记录类型import dns.resolver answers dns.resolver.resolve(example.com, A) for r in answers: print(r.to_text())这里容易混淆的是A 记录查询的是根域名的 IP而不是子域的。如果要枚举子域需要把每个前缀和根域名拼接。DNS 查询也不建议一次性并发太多公共 DNS 和部分递归服务器有 QPS 限制超过限制会直接返回 SERVFAIL。5.3 第三步查询证书透明日志证书透明日志Certificate TransparencyCT是强制公开的证书签发日志表。任何 CA 签发的 SSL/TLS 证书都会被记录到 CT 日志中而证书里面往往包含域名和子域名信息。因此CT 日志是寻找“你之前不知道的子域名”的最佳被动数据源。最常用的免费接口是 crt.sh它提供了基于域名模糊匹配的查询接口curl -s https://crt.sh/?q%25.example.comoutputjson这个接口返回 JSON 数组每个元素包含证书的 name_value 字段。由于一张证书里可能包含多个域名而且不同证书之间会有重复拿到结果之后需要按域名切分、去重、过滤掉泛域名符号。这个步骤是后续子域聚类的核心。5.4 第四步子域枚举子域枚举有两种思路一种是基于字典的主动爆破把字典里的前缀逐个拼接到目标域名下通过 DNS 查询判断是否存在另一种是基于数据源的反查聚合把 crt.sh、DNS 历史库、搜索引擎等外部数据源中的子域全部汇总。主动爆破的优势是速度快、可以发现未经外部索引的新子域缺点是会产生大量 DNS 请求容易触发告警。所以本文的示例会使用线程池控制并发量并且只查询 A 记录不做额外的端口扫描。整个流程的逻辑顺序是先用 whois 和 DNS 拿到目标域名的静态属性再用证书透明日志做一次被动的子域盘点最后用字典爆破补充未被日志收录的子域。四步信息汇总到同一个 JSON 结构里就是一次最小可用的 OSINT 收集结果。6. 完整示例代码实现下面是一个完整可运行的 Python 脚本。将代码保存为osint_collector.py。#!/usr/bin/env python3 # 文件路径osint_collector.py OSINT Collector - 一个最小可运行的开源情报收集脚本。 仅限用于已获得书面授权的目标信息收集。 依赖pip install requests dnspython python-whois import argparse import json from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import datetime import dns.resolver import requests import whois def query_dns_records(domain): 查询 A、AAAA、NS、MX、TXT 五类 DNS 记录。 records {} for rdtype in [A, AAAA, NS, MX, TXT]: try: answers dns.resolver.resolve(domain, rdtype) records[rdtype] [str(r.to_text()) for r in answers] except dns.resolver.NXDOMAIN: records[rdtype] [] except dns.resolver.NoAnswer: records[rdtype] [] except dns.resolver.LifetimeTimeout: records[rdtype] [TIMEOUT] return records def query_crtsh(domain): 从证书透明日志中收集子域名。 url fhttps://crt.sh/?q%25.{domain}outputjson headers {User-Agent: Mozilla/5.0 (compatible; OSINT-Colletor/1.0)} names set() try: resp requests.get(url, headersheaders, timeout30) if resp.status_code ! 200: print(f[!] crt.sh returned HTTP {resp.status_code}) return [] data resp.json() for entry in data: name_value entry.get(name_value, ) for n in name_value.split(\n): n n.strip().lstrip(*.) if n.endswith(. domain) or n domain: if n ! domain: names.add(n) except Exception as exc: print(f[!] crt.sh query failed: {exc}) return sorted(names) def check_subdomain(word, domain): 检查子域前缀是否解析出 A 记录。 sub f{word}.{domain} try: answers dns.resolver.resolve(sub, A, lifetime5) if answers: return sub except Exception: return None return None def brute_subdomains(domain, wordlist_path, threads8): 基于字典和线程池的子域爆破。 found set() try: with open(wordlist_path, r, encodingutf-8) as f: words [line.strip() for line in f if line.strip()] except FileNotFoundError: print(f[!] wordlist not found: {wordlist_path}) return [] with ThreadPoolExecutor(max_workersthreads) as executor: futures [executor.submit(check_subdomain, w, domain) for w in words] for fut in as_completed(futures): result fut.result() if result: print(f[] {result}) found.add(result) return sorted(found) def main(): parser argparse.ArgumentParser(descriptionMinimal OSINT collector) parser.add_argument(domain, help目标域名仅限已授权目标) parser.add_argument(-w, --wordlist, defaultsubdomains.txt, help子域字典路径) parser.add_argument(-o, --output, defaultosint_result.json, help输出 JSON 文件路径) parser.add_argument(-t, --threads, typeint, default8, help子域爆破线程数) args parser.parse_args() domain args.domain.strip().lower() print(f[*] Target domain: {domain}) print(f[*] Started at: {datetime.now().isoformat()}) result { domain: domain, timestamp: datetime.now().isoformat(), whois: {}, dns: {}, crt_subdomains: [], brute_subdomains: [], } print(\n[1/4] Querying WHOIS ...) try: w whois.whois(domain) for key in [domain_name, registrar, creation_date, expiration_date, name_servers, org, country]: val getattr(w, key, None) if isinstance(val, (datetime, list)): val str(val) if val: result[whois][key] val except Exception as exc: print(f[!] WHOIS query failed: {exc}) print(\n[2/4] Querying DNS records ...) result[dns] query_dns_records(domain) print(\n[3/4] Querying certificate transparency logs ...) result[crt_subdomains] query_crtsh(domain) print(f[-] crt.sh found {len(result[crt_subdomains])} unique names) print(\n[4/4] Brute-forcing subdomains ...) result[brute_subdomains] brute_subdomains(domain, args.wordlist, args.threads) print(f[-] brute found {len(result[brute_subdomains])} subdomains) with open(args.output, w, encodingutf-8) as f: json.dump(result, f, ensure_asciiFalse, indent2) print(f\n[*] Done. Output written to {args.output}) if __name__ __main__: main()这段代码把整个流程分成了四个函数分别对应 whois 查询、DNS 查询、CT 日志查询和子域爆破。重点说明几个设计选择第一DNS 记录查询把异常单独处理。NXDOMAIN表示域名不存在NoAnswer表示该类型记录不存在这两类情况都直接记为[]而不是抛出异常。LifetimeTimeout单独记成TIMEOUT方便你定位是在哪一步网络超时。第二crt.sh 的结果解析做了三件事把一行多个域名按换行切分、把通配符*.去掉、用集合去重。证书日志中最常见的问题就是重复和通配符不处理的话后续分析会被污染。第三子域爆破只查询 A 记录。一个子域如果只配置了 CNAME 而没有实际解析 IP在此脚本中不会被认为“存在”。这是刻意为之——在暴露面分析中可解析到 IP 的资产比纯 CNAME 更值得关注。7. 运行结果与效果验证安装依赖并准备好subdomains.txt之后运行脚本python osint_collector.py your-authorized-domain.com -w subdomains.txt -t 8注意your-authorized-domain.com只是一个占位符请替换为你实际有权测试的域名。举例演示的预期输出如下[*] Target domain: your-authorized-domain.com [*] Started at: 2025-01-15T10:24:18 [1/4] Querying WHOIS ... [2/4] Querying DNS records ... [3/4] Querying certificate transparency logs ... [-] crt.sh found 23 unique names [4/4] Brute-forcing subdomains ... [] mail.your-authorized-domain.com [] blog.your-authorized-domain.com [-] brute found 2 subdomains [*] Done. Output written to osint_result.json脚本运行成功后打开osint_result.json可以看到一个结构化的 JSON 文件{ domain: your-authorized-domain.com, timestamp: 2025-01-15T10:24:18, whois: { registrar: Example Registrar, creation_date: 2020-01-01 00:00:00, expiration_date: 2026-01-01 00:00:00 }, dns: { A: [93.184.216.34], NS: [ns1.example.com.], MX: [mail.your-authorized-domain.com.], TXT: [vspf1 include:_spf.example.com ~all] }, crt_subdomains: [blog.your-authorized-domain.com, mail.your-authorized-domain.com], brute_subdomains: [blog.your-authorized-domain.com] }如何判断运行成功有三个标准。第一脚本没有抛未捕获异常输出末尾出现Done字样。第二crt_subdomains和brute_subdomains两个数组非空除非目标确实没有任何子域否则至少会有一个结果。第三把 JSON 中的 DNS 记录与dig命令的结果相互对照确认数据一致。如果运行失败第一步应该看控制台输出的[!]行它会明确告诉你是哪一类问题。比如 crt.sh 返回 HTTP 429就是请求太频繁需要等待一段时间再试WHOIS 查询失败通常是网络到 whois 服务器不通或端口被防火墙拦截。8. 常见问题与排查思路问题现象可能原因排查方式解决方案WHOIS 查询超时或报错whois 服务器连接不稳定或模块版本不兼容检查网络单独执行whois.whois(example.com)降低请求频率升级python-whois或改用公开 whois APIDNS 查询大量 TIMEOUT本地 DNS 服务质量差或并发过高用nslookup手动验证目标域名改用公共 DNS减少线程数增加超时时间crt.sh 返回 429请求频率超限查看 HTTP 状态码和响应头增加 User-Agent拉大请求间隔或使用离线 CT 日志镜像子域爆破结果明显偏少字典太小或 DNS 过滤部分请求或目标只存在少量子域将结果与 crt.sh 结果对比确认是否完全是主动查询导致的漏报扩充字典使用多 DNS 轮询结合被动数据源Windows 控制台输出乱码Python 默认编码为 GBKJSON 中文或特殊字符打印异常检查控制台编码设置PYTHONIOENCODINGutf-8或直接查看 utf-8 编码的 JSON 文件结果中大量重复子域crt.sh 证书条目包含多个重复 name_value打印原始 name_value 观察统一用集合去重规范化小写去掉*.通配符脚本被目标防护设备拦截主动 DNS 爆破产生高频请求查看目标告警或请求日志降低线程数延长间隔优先使用被动数据源必要时停止主动爆破实际工程中crt.sh 返回 429 是你最可能遇到的问题。这个免费接口并没有承诺任何 QPS高频调用经常被限流。稳妥的做法是在查询函数里增加缓存同一个域名一天只查询一次并把结果存储下来后续直接复用。另一个高频问题是 DNS 解析结果和 whois 结果在部分字段类型上有差异。python-whois返回的creation_date可能是 datetime 对象也可能是 datetime 列表代码里做了兼容。如果你在别的脚本里直接序列化这个对象会报TypeError: Object of type datetime is not JSON serializable这就是为什么示例代码里先做了一次类型转换。9. 最佳实践与工程建议把最小脚本跑通只是第一步真正把它用到生产或团队工作中还需要注意以下实践。第一合规红线要写进代码和文档而不是只写在 README 里。脚本的--help描述、默认输出目录、运行日志都要明确标注“仅限授权目标”。如果你是团队负责人建议在脚本内部加入一个目标域名白名单不在白名单中的域名直接拒绝运行用工程手段降低误用风险。第二模块化设计是这类脚本长期维护的关键。每个数据源封装成独立函数输入是统一的domain参数输出是dict或list这样后续新增 Shodan、GitHub 搜索等数据源时不需要改动主流程。统一的 schema 也有利于结果入库。第三使用环境变量管理 API key绝不写进代码或 Git 历史。比如后续接入 Censys、Shodan、GitHub Token 时可以使用.env文件export CENSYS_API_IDyour_id export CENSYS_API_SECRETyour_secret export GITHUB_TOKENyour_token不要提交.env文件到仓库。在.gitignore里添加.env避免密钥泄露。第四主动探测要控制节奏。DNS 爆破的线程数要按目标网络和授权范围调整建议从 4 到 8 开始不要一上来就上 100 线程。对有 CDN 或云防火墙的目标更推荐以证书透明日志、DNS 历史查询等被动数据源为主。第五结果存储推荐使用带时间戳的 JSON Lines 或 SQLite而不是一次性覆盖写入。每次运行记录timestamp、domain、collector三个字段形成增量历史。暴露面管理是持续过程只有历史数据才能回答“这周新增了哪些子域”这类问题。第六日志必须完整。每一条外部查询都应该包含查询时间、查询源、目标、响应状态和耗时这样在做合规审计和故障回溯时才有依据。建议至少保留 6 个月的运行日志。10. 总结与后续学习方向回到开头的问题为什么 K2SOsint / Legendary_OSINT 这类项目值得关注因为它们把 OSINT 从“临时敲命令”变成了“可重复执行的管道”。真正产生价值的不是某一条命令而是管道本身的设计数据源如何接入结果如何清洗报告如何沉淀。本文给出的最小脚本已经覆盖了 whois、DNS、证书透明日志和子域爆破四类基础数据源。接下来你可以顺着三个方向继续深入。第一个方向是扩展数据源。接入 GitHub Search API搜索目标域名在代码中的出现情况接入 Censys 或 Shodan对已发现的 IP 做服务和端口指纹反查接入 DNS 历史服务找出曾经解析但已删除的记录。每新增一个数据源都重复“独立模块 统一 schema 日志审计”的模式。第二个方向是自动化与可视化。把脚本接入定时任务每周对自有资产跑一次把结果写入数据库再用简单的表格或图表展示变化趋势。暴露面管理的价值在时间维度上才真正体现出来。第三个方向是分析能力。收集到的数据如果不做关联永远是原始数据。试着把同一子域在不同数据源的结果做交叉验证把证书日志中出现频率高的域名标记为重点资产把 suddenly 出现的新子域加入变更告警。这一步才是从“收集”跨越到“情报”的地方。最后再强调一次边界所有 OSINT 实践都必须以合法合规为前提只收集与目标资产相关的公开信息不触碰可识别自然人的隐私数据不进行未授权的高频请求。工具的边界就是使用者的边界方法和代码永远要跑在规则之内。建议把这套最小脚本跑在自己拥有或已授权的域名上先形成完整流程再逐步叠加数据源和分析逻辑你会逐渐理解 OSINT 的真正价值。