利用证书透明度日志挖掘子域名:原理、工具链与实战指南
1. 项目概述为什么证书透明度日志是子域名发现的“金矿”如果你负责过企业安全、做过渗透测试或者只是单纯好奇自家公司到底有多少个对外暴露的网站入口那你一定对“子域名枚举”这件事不陌生。传统的做法要么是暴力穷举字典要么是爬取搜索引擎结果再高级点就是利用DNS记录、网络空间测绘引擎。这些方法各有各的痛点暴力破解效率低、噪音大搜索引擎结果不全且有频率限制DNS记录可能不完整测绘引擎的API要钱数据也有延迟。今天我要聊的是一种被严重低估但数据源极其权威、免费且实时性相当不错的方法挖掘证书透明度Certificate Transparency 简称CT日志。我第一次接触到这个思路是在一次内部资产梳理的焦头烂额之际传统的扫描器跑了一晚上报告里还是那几十个已知域名。抱着试试看的心态我写了个脚本去拉CT日志结果在半小时内发现了十几个连运维团队自己都忘了的、用于临时测试的子域名其中两个甚至还在跑着带默认密码的老旧应用。那一刻我就意识到这玩意儿是个宝藏。简单来说CT是一个由行业推动的公开审计框架。为了应对过去证书误签发或恶意签发的问题它要求所有公开信任的证书颁发机构CA比如Let‘s Encrypt、DigiCert、Sectigo等必须将它们签发的每一张SSL/TLS证书都提交到全球多个公开的、不可篡改的CT日志服务器上。这意味着每当有人为一个域名比如*.api.internal.yourcompany.com申请了一张证书这个域名信息就会几乎实时地出现在公开日志里。我们的目标就是从这片数据的海洋中捞出所有属于我们目标域名的“珍珠”——也就是子域名。这个方法有几个无可替代的优势被动且隐蔽你只是在读取公开日志没有向目标服务器发送任何探测包完全不会被对方的WAF或IDS察觉。数据权威数据来自全球各大CA的一手签发记录准确性极高。覆盖广泛很多内部系统、临时环境、开发测试站点为了图方便也会申请免费证书尤其是Let‘s Encrypt这些都会留下记录。免费且无限制CT日志是公开的有稳定的API可以查询没有调用次数限制当然要遵守合理的频率。接下来我就手把手带你从零开始搭建一套属于自己的CT日志挖掘工具链。我们不止于调用现成工具更要理解背后的原理、自己动手处理数据并解决实操中一定会遇到的那些坑。2. 核心原理与数据源解析CT日志里到底有什么在动手写代码之前我们必须搞清楚我们要挖的“矿脉”究竟长什么样。否则面对返回的原始数据你只会是一头雾水。2.1 证书透明度CT的运作机制你可以把CT日志想象成一个全球分布的、只允许追加的公共账本。这个账本由多个独立的“日志服务器”Log Server维护比如Google的“Argon2023”、Cloudflare的“Nimbus2023”等。运作流程是这样的证书申请网站管理员向CA如Let‘s Encrypt申请一张证书例如用于blog.example.com。提交日志CA在签发证书后必须将完整的证书或至少是证书的“承诺”即Precertificate提交到一个或多个CT日志服务器。返回SCT日志服务器接收到证书后会对其进行签名并生成一个名为“签名证书时间戳”Signed Certificate Timestamp SCT的凭据返回给CA。交付证书CA将SCT和证书一起交付给申请者。现代浏览器如Chrome在验证网站证书时会同时检查是否存在有效的SCT以确保证书已被公开记录。公开查询所有提交的证书信息都被编码后存入日志并通过公开的API如RFC 6962定义的API供任何人查询。这个机制的核心安全价值在于“公开审计”任何异常签发都无所遁形。而对我们资产发现者来说价值在于证书的“主题备用名称”Subject Alternative Name SAN字段。一个证书不仅可以用于一个域名还可以通过SAN字段列出上百个其他域名。CA在提交时所有这些域名信息都会进入日志。2.2 关键数据字段我们的“寻宝图”当我们从CT日志API获取一条记录时它通常是一个JSON对象其中包含了一个经过编码的证书信息。解析后我们最需要关注的是证书的以下部分subject.commonName(CN)证书的主题通用名。虽然传统上这里写主域名但现在由于安全规范CA通常将主域名也放在SAN中CN字段可能只是一个代表如example.com。不能只依赖CN字段。extensions.subjectAltName(SAN)这是我们的“金矿”所在。它是一个数组列出了该证书所有有效的域名。可能包括通配符域名*.example.com具体子域名www.example.com,api.example.com,dev.test.example.com甚至完全不同的域名在同一个证书里。我们的核心任务就是获取日志 - 解析证书 - 提取SAN数组 - 过滤出我们感兴趣的目标域名的所有子项。2.3 主流CT日志源与API选择并不是所有日志服务器都提供相同便利的查询接口。对于我们的子域名挖掘场景主要推荐以下两种方式Cert Spotter API (由SSLMate运营)这可能是对开发者最友好的入口。它提供了一个RESTful API可以直接根据域名查询证书。它背后聚合了多个日志源的数据。优点是简单直接有免费额度对于个人和小规模使用足够。缺点是免费版有速率限制且无法获取非常历史的数据。Google的Certificate Transparency Log (CRT.sh 底层使用)crt.sh是一个广为人知的CT日志查询网站它背后直接对接Google等日志服务器。它本身也提供公开的PostgreSQL数据库接口允许执行SQL查询功能强大且数据最全但查询方式更底层、更复杂。其他公开日志列表你可以从类似https://www.gstatic.com/ct/log_list/v3/log_list.json这样的地址获取当前所有可信日志服务器的列表。但直接与这些日志服务器交互需要处理它们的“Merkle Tree Hash”数据结构复杂度较高。对于从零开始的我们我建议的路线是先用crt.sh的网站或API进行快速验证和初步探索理解数据格式。当需要大规模、定制化挖掘时再转向使用crt.sh的数据库接口或编写更底层的日志抓取器。本文我们将主要使用crt.sh的API作为实战案例因为它平衡了易用性和能力。注意crt.sh的公开数据库接口压力很大请务必遵守其使用规范避免高频、复杂的查询建议添加合理的延迟如每秒1次请求做一个有道德的“矿工”。3. 实战工具链搭建从环境准备到第一个脚本理论说再多不如动手跑一行代码。我们这就来搭建一个最小化但功能完整的CT子域名挖掘环境。3.1 环境与工具准备你不需要复杂的IDE或服务器一台能上网的电脑安装好Python3环境就足够了。我们将主要使用以下几个Python库requests用于发送HTTP请求到CT日志API。json用于解析API返回的JSON数据。re正则表达式用于从证书信息中精准提取域名。首先创建一个项目目录并安装必要的库mkdir ct-subdomain-miner cd ct-subdomain-miner python3 -m venv venv # 创建虚拟环境非必须但推荐 # 激活虚拟环境 # Linux/macOS: source venv/bin/activate # Windows: .\venv\Scripts\activate pip install requests3.2 编写第一个CT日志查询脚本我们以crt.sh提供的JSON API为例。它的基础查询URL是https://crt.sh/json。我们可以通过传递参数来查询。新建一个文件miner.py写入以下代码import requests import json import re import time def query_crtsh(domain): 查询 crt.sh 获取指定域名的证书记录 url https://crt.sh/json params { q: f%.{domain}, # 使用 % 进行LIKE查询查找所有子域名 output: json } headers { User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 # 添加一个User-Agent头 } try: response requests.get(url, paramsparams, headersheaders, timeout30) response.raise_for_status() # 检查HTTP错误 return response.json() except requests.exceptions.RequestException as e: print(f查询 {domain} 时发生错误: {e}) return None def extract_subdomains_from_json(data, target_domain): 从 crt.sh 返回的JSON数据中提取子域名 subdomains set() # 使用集合自动去重 if not data or not isinstance(data, list): print(未获取到有效数据或数据格式非列表。) return subdomains for entry in data: # crt.sh 返回的条目中域名信息主要在 ‘name_value’ 字段 # 这个字段是一个字符串可能包含多个域名用换行符分隔 name_value entry.get(name_value, ) if not name_value: continue # 分割字符串得到域名列表 names name_value.strip().split(\n) for name in names: name name.strip().lower() # 转为小写统一处理 # 简单的过滤确保域名以目标域结尾并且不是泛域名本身 if name.endswith(f.{target_domain}) or name target_domain: # 移除可能的通配符标记 clean_name name.replace(*., ) subdomains.add(clean_name) # 注意这里可能也会捕获到像 ‘something.target.com’ 这样的域名 # 但如果是 ‘other.com’ 则不会被包含因为不以目标域结尾 return subdomains def main(): target_domain input(请输入要挖掘的目标域名 (例如: example.com): ).strip() if not target_domain: print(域名不能为空。) return print(f[*] 正在查询 {target_domain} 的CT日志记录...) data query_crtsh(target_domain) if data: subdomains extract_subdomains_from_json(data, target_domain) print(f[] 发现 {len(subdomains)} 个子域名:) for sub in sorted(subdomains): print(f - {sub}) # 可选保存到文件 filename f{target_domain}_subdomains.txt with open(filename, w) as f: for sub in sorted(subdomains): f.write(sub \n) print(f[*] 结果已保存至 {filename}) else: print([-] 未获取到数据。) if __name__ __main__: main() # 礼貌性延迟避免对服务器造成压力 time.sleep(1)脚本解析与操作意图query_crtsh函数这是我们的数据获取层。我们向crt.sh发送一个GET请求。关键参数是q我们设置为%.example.com这利用了crt.sh后端数据库的LIKE语法意味着匹配所有以.example.com结尾的域名记录自然就包含了所有子域名。添加User-Agent头是为了避免被某些服务器拒绝无头的请求。extract_subdomains_from_json函数这是我们的数据解析层。crt.sh返回的JSON数组中每个条目代表一个证书记录。我们关心的name_value字段包含了该证书所有认证的域名用换行符\n分隔。我们将其分割、清洗转小写、去空格、然后通过判断是否以目标域名结尾来过滤出相关的子域名。使用set()集合来自动去重。main函数串联整个流程并添加了简单的文件保存功能。运行一下在终端里执行python miner.py输入你想查询的域名比如github.com。稍等片刻你就能看到控制台打印出通过CT日志发现的一系列子域名。实操心得1crt.shAPI的局限性上面这个脚本虽然简单有效但它依赖的是crt.sh聚合和预处理后的数据。crt.sh的name_value字段有时可能不完整或者更新有延迟通常几分钟到几小时。对于最全面、最实时的数据需要直接查询CT日志服务器的get-entriesAPI但那就需要处理分页、Merkle Tree叶子节点的编码通常是Base64编码的X.509证书等更复杂的问题。作为入门crt.sh完全够用。4. 进阶直接解析原始证书与大规模处理当我们不满足于crt.sh的预处理数据或者需要处理海量目标时就需要更进阶的方案。核心思路是直接获取CT日志的原始条目 - 解码出证书 - 使用密码学库解析证书 - 提取SAN字段。4.1 使用certstream库进行实时监听对于实时性要求极高的场景比如监控新颁发的证书我们可以使用certstream库。它是一个Python客户端可以连接到由CaliDog团队运营的certstream.calidog.io服务这个服务实时推送来自多个CT日志的新证书信息。安装和基础用法pip install certstreamimport certstream import re def on_message(message, context): # 监听证书流中的消息 if message[message_type] heartbeat: return if message[message_type] certificate_update: all_domains message[data][leaf_cert][all_domains] # 在这里处理所有域名过滤出你关心的目标 target example.com for domain in all_domains: if domain.endswith(target) or domain target: print(f[实时发现] {domain}) # 开始监听 certstream.listen_for_events(on_message)这种方式是“订阅-推送”模型非常适合做实时监控告警。但注意它推送的是全量数据你需要自己写过滤逻辑并且网络连接需要稳定。4.2 构建本地CT日志爬虫与解析器对于需要深度、批量分析历史数据的场景我们需要一个更自主的方案。思路如下获取日志列表从已知地址获取所有活跃CT日志的元信息。获取条目范围查询某个日志获取其当前的总条目数tree size。分批获取条目使用get-entriesAPI按批次例如每次1000条下载指定索引范围的条目。解析条目每个条目包含一个“叶子证书”或“预证书”。需要先进行Base64解码然后使用ASN.1解析器如cryptography库解析出X.509证书对象。提取域名从证书对象的extensions中获取SubjectAlternativeName并提取其中的DNS名称。这是一个简化的架构示例使用cryptography库import requests import base64 from cryptography import x509 from cryptography.hazmat.backends import default_backend def fetch_and_parse_entries(log_url, start, end): 从指定CT日志获取并解析一批条目 entries_url f{log_url}/ct/v1/get-entries?start{start}end{end} resp requests.get(entries_url) data resp.json() domains set() for entry in data[entries]: # 条目有两种类型x509_entry 和 precert_entry我们主要处理x509 leaf_input entry[leaf_input] # 这里需要根据CT的编码规则提取出证书数据略过Merkle Tree相关结构 # 假设我们已提取到证书的DER编码数据cert_der # cert_der base64.b64decode(leaf_input_parsed[cert_data]) # 使用 cryptography 库解析证书 # cert x509.load_der_x509_certificate(cert_der, default_backend()) # 提取SAN # try: # san_ext cert.extensions.get_extension_for_oid(x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME) # san_names san_ext.value.get_values_for_type(x509.DNSName) # domains.update(san_names) # except x509.ExtensionNotFound: # pass # 实际代码需要处理CT特定的编码结构MerkleTreeLeaf pass return domains为什么这么复杂因为直接与CT日志服务器交互你需要遵循RFC 6962规范正确处理“Merkle Tree Leaf”结构它包含了时间戳、日志扩展、证书数据等。这超出了入门教程的范围但知道这个方向很重要。社区有成熟工具如ct-tools(Google官方) 或certificate-transparency-go库可以帮助处理这些底层细节。4.3 大规模目标处理与去重优化当你需要监控成百上千个域名时效率至关重要。并发请求使用asyncioaiohttp或concurrent.futures.ThreadPoolExecutor来并发查询多个目标域名或者并发获取CT日志的不同条目块可以极大缩短时间。本地数据库将获取到的子域名、发现时间、关联的证书序列号、日志ID等信息存入本地SQLite或小型数据库。这样便于增量更新记录上次查询的日志位置下次只拉取新条目。历史关联分析某个子域名首次出现的时间、证书的变更历史。去重基于证书序列号或域名进行高效去重。结果聚合与分类对发现的子域名进行简单分类例如通过正则匹配api.、admin.、test.、dev.、staging.等前缀快速识别出可能更有价值的目标。5. 常见问题、排查技巧与避坑指南在实际操作中你肯定会遇到各种问题。下面是我踩过坑后总结的一些经验。5.1 数据不完整或遗漏子域名现象自己知道存在的某个子域名在CT日志里没查到。排查思路证书类型该子域名可能使用了内部CA签发的证书如企业自建PKI或使用了私有信任的证书。这类证书没有义务提交到公开CT日志。证书有效期CT日志服务器通常只保留证书一段时间例如几年。非常古老的证书可能已被修剪Log Pruning。日志源覆盖你使用的API如crt.sh可能没有聚合所有日志服务器的数据。尝试换用其他工具交叉验证如subfinder带CT模块、amass或tls.bufferover.run等在线服务。查询语法确保查询语法正确。在crt.sh中%.domain.com和%domain.com结果不同前者要求以.domain.com结尾后者是模糊匹配包含domain.com。对于根域名直接查询domain.com。5.2 查询被限制或封禁现象请求频繁返回429Too Many Requests或其他错误。解决方案降低频率在请求间添加随机延迟如time.sleep(random.uniform(1, 3))。对于crt.sh的公开接口建议至少1秒一次。使用代理池如果需要极高频率的查询考虑使用轮换代理IP。遵守规则查阅目标服务的Robots.txt或使用条款。crt.sh的数据库接口明确不鼓励自动化大规模扫描。考虑付费API如果需要生产级、稳定的服务可以考虑类似SecurityTrails、Censys或Spyse的付费API它们通常整合了CT数据并提供更友好的接口和更高的限额。5.3 结果中包含大量无关或无效域名现象结果里出现了很多明显不属于目标公司的域名或者像*.example.com这样的通配符条目甚至是证书错误配置导致的乱码。处理技巧严格的后缀匹配在过滤时确保使用domain.endswith(‘.example.com’) or domain ‘example.com’而不是‘example.com’ in domain后者会匹配到example.com.attacker.com这种域名。处理通配符通配符证书*.example.com本身是一个有效记录代表所有一级子域名。你可以选择保留它作为资产记录也可以在展示时特殊标记或展开为常见子域名列表如www, mail, api, blog等。清洗数据对提取的域名进行有效性检查例如移除开头结尾的特殊字符。检查域名格式是否符合RFC标准可以使用tldextract库辅助。过滤掉明显是IP地址或内部地址如.local,.internal的条目虽然它们在SAN里出现的情况较少。5.4 性能瓶颈与优化问题扫描一个大型组织拥有数万历史证书时脚本运行缓慢甚至内存溢出。优化方向流式处理对于直接爬取日志条目的场景不要一次性把所有数据加载到内存再解析。应该边下载、边解析、边过滤、边存储到文件或数据库。选择性解析如果只关心域名可以只解析证书的SAN扩展部分而不是整个证书结构。这需要对证书的ASN.1结构有深入了解或者使用能够部分解析的库。分布式处理将目标列表或日志索引范围拆分在多台机器或多个进程上并行处理。5.5 与其他工具的联动CT日志挖掘很少单独使用它通常是资产发现“武器库”中的一件利器。一个典型的流程是被动收集使用CT日志、DNS历史记录如SecurityTrails、搜索引擎语法如site:、网络空间测绘如Shodan、Fofa进行初步、广泛的资产发现。主动验证对发现的子域名进行HTTP/HTTPS请求获取标题、状态码、指纹如Wappalyzer确认其存活性和服务类型。端口扫描对存活的IP进行快速端口扫描如用masscan或nmap发现非Web服务。漏洞扫描针对特定的服务或应用进行深度扫描。你可以将我们写的CT挖掘脚本集成到这样的自动化流程中作为资产发现流水线的第一个环节。最后我想分享一个我自己的体会技术本身是中立的CT日志的公开本意是为了提升Web安全。我们在利用它进行资产发现时务必只针对自己有授权测试的目标或者自己所属的组织。未经授权扫描他人资产不仅是非法的也违背了安全社区互助互信的原则。把这项技术用在正确的地方比如帮助企业完善自身的资产清单、发现被遗忘的“影子IT”这才是它最大的价值所在。