利用证书透明度日志挖掘子域名:原理、工具链与实战指南

利用证书透明度日志挖掘子域名:原理、工具链与实战指南

1. 项目概述:为什么证书透明度日志是子域名发现的“金矿”?

如果你负责过企业安全、做过渗透测试,或者只是单纯好奇自家公司到底有多少个对外暴露的网站入口,那你一定对“子域名枚举”这件事不陌生。传统的做法,要么是暴力穷举字典,要么是爬取搜索引擎结果,再高级点就是利用DNS记录、网络空间测绘引擎。这些方法各有各的痛点:暴力破解效率低、噪音大;搜索引擎结果不全且有频率限制;DNS记录可能不完整;测绘引擎的API要钱,数据也有延迟。

今天我要聊的,是一种被严重低估,但数据源极其权威、免费且实时性相当不错的方法:挖掘证书透明度(Certificate Transparency, 简称CT)日志。我第一次接触到这个思路,是在一次内部资产梳理的焦头烂额之际,传统的扫描器跑了一晚上,报告里还是那几十个已知域名。抱着试试看的心态,我写了个脚本去拉CT日志,结果在半小时内,发现了十几个连运维团队自己都忘了的、用于临时测试的子域名,其中两个甚至还在跑着带默认密码的老旧应用。那一刻我就意识到,这玩意儿是个宝藏。

简单来说,CT是一个由行业推动的公开审计框架。为了应对过去证书误签发或恶意签发的问题,它要求所有公开信任的证书颁发机构(CA),比如Let‘s Encrypt、DigiCert、Sectigo等,必须将它们签发的每一张SSL/TLS证书都提交到全球多个公开的、不可篡改的CT日志服务器上。这意味着,每当有人为一个域名(比如*.api.internal.yourcompany.com)申请了一张证书,这个域名信息就会几乎实时地出现在公开日志里。我们的目标,就是从这片数据的海洋中,捞出所有属于我们目标域名的“珍珠”——也就是子域名。

这个方法有几个无可替代的优势:

  1. 被动且隐蔽:你只是在读取公开日志,没有向目标服务器发送任何探测包,完全不会被对方的WAF或IDS察觉。
  2. 数据权威:数据来自全球各大CA的一手签发记录,准确性极高。
  3. 覆盖广泛:很多内部系统、临时环境、开发测试站点为了图方便也会申请免费证书(尤其是Let‘s Encrypt),这些都会留下记录。
  4. 免费且无限制:CT日志是公开的,有稳定的API可以查询,没有调用次数限制(当然要遵守合理的频率)。

接下来,我就手把手带你从零开始,搭建一套属于自己的CT日志挖掘工具链。我们不止于调用现成工具,更要理解背后的原理、自己动手处理数据,并解决实操中一定会遇到的那些坑。

2. 核心原理与数据源解析:CT日志里到底有什么?

在动手写代码之前,我们必须搞清楚我们要挖的“矿脉”究竟长什么样。否则,面对返回的原始数据,你只会是一头雾水。

2.1 证书透明度(CT)的运作机制

你可以把CT日志想象成一个全球分布的、只允许追加的公共账本。这个账本由多个独立的“日志服务器”(Log Server)维护,比如Google的“Argon2023”、Cloudflare的“Nimbus2023”等。运作流程是这样的:

  1. 证书申请:网站管理员向CA(如Let‘s Encrypt)申请一张证书,例如用于blog.example.com
  2. 提交日志:CA在签发证书后,必须将完整的证书(或至少是证书的“承诺”,即Precertificate)提交到一个或多个CT日志服务器。
  3. 返回SCT:日志服务器接收到证书后,会对其进行签名,并生成一个名为“签名证书时间戳”(Signed Certificate Timestamp, SCT)的凭据,返回给CA。
  4. 交付证书:CA将SCT和证书一起交付给申请者。现代浏览器(如Chrome)在验证网站证书时,会同时检查是否存在有效的SCT,以确保证书已被公开记录。
  5. 公开查询:所有提交的证书信息都被编码后存入日志,并通过公开的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选择

并不是所有日志服务器都提供相同便利的查询接口。对于我们的子域名挖掘场景,主要推荐以下两种方式:

  1. Cert Spotter API (由SSLMate运营):这可能是对开发者最友好的入口。它提供了一个RESTful API,可以直接根据域名查询证书。它背后聚合了多个日志源的数据。优点是简单直接,有免费额度(对于个人和小规模使用足够)。缺点是免费版有速率限制,且无法获取非常历史的数据。
  2. Google的Certificate Transparency Log (CRT.sh 底层使用)crt.sh是一个广为人知的CT日志查询网站,它背后直接对接Google等日志服务器。它本身也提供公开的PostgreSQL数据库接口,允许执行SQL查询!功能强大且数据最全,但查询方式更底层、更复杂。
  3. 其他公开日志列表:你可以从类似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 requests

3.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, params=params, headers=headers, timeout=30) 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)

脚本解析与操作意图:

  1. query_crtsh函数:这是我们的数据获取层。我们向crt.sh发送一个GET请求。关键参数是q,我们设置为%.example.com,这利用了crt.sh后端数据库的LIKE语法,意味着匹配所有以.example.com结尾的域名记录,自然就包含了所有子域名。添加User-Agent头是为了避免被某些服务器拒绝无头的请求。
  2. extract_subdomains_from_json函数:这是我们的数据解析层。crt.sh返回的JSON数组中,每个条目代表一个证书记录。我们关心的name_value字段包含了该证书所有认证的域名,用换行符\n分隔。我们将其分割、清洗(转小写、去空格)、然后通过判断是否以目标域名结尾来过滤出相关的子域名。使用set()集合来自动去重。
  3. main函数:串联整个流程,并添加了简单的文件保存功能。

运行一下:在终端里执行python miner.py,输入你想查询的域名,比如github.com。稍等片刻,你就能看到控制台打印出通过CT日志发现的一系列子域名。

实操心得1:crt.shAPI的局限性上面这个脚本虽然简单有效,但它依赖的是crt.sh聚合和预处理后的数据。crt.shname_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 certstream
import 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日志爬虫与解析器

对于需要深度、批量分析历史数据的场景,我们需要一个更自主的方案。思路如下:

  1. 获取日志列表:从已知地址获取所有活跃CT日志的元信息。
  2. 获取条目范围:查询某个日志,获取其当前的总条目数(tree size)。
  3. 分批获取条目:使用get-entriesAPI,按批次(例如每次1000条)下载指定索引范围的条目。
  4. 解析条目:每个条目包含一个“叶子证书”或“预证书”。需要先进行Base64解码,然后使用ASN.1解析器(如cryptography库)解析出X.509证书对象。
  5. 提取域名:从证书对象的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 大规模目标处理与去重优化

当你需要监控成百上千个域名时,效率至关重要。

  • 并发请求:使用asyncio+aiohttpconcurrent.futures.ThreadPoolExecutor来并发查询多个目标域名,或者并发获取CT日志的不同条目块,可以极大缩短时间。
  • 本地数据库:将获取到的子域名、发现时间、关联的证书序列号、日志ID等信息存入本地SQLite或小型数据库。这样便于:
    • 增量更新:记录上次查询的日志位置,下次只拉取新条目。
    • 历史关联:分析某个子域名首次出现的时间、证书的变更历史。
    • 去重:基于证书序列号或域名进行高效去重。
  • 结果聚合与分类:对发现的子域名进行简单分类,例如通过正则匹配api.admin.test.dev.staging.等前缀,快速识别出可能更有价值的目标。

5. 常见问题、排查技巧与避坑指南

在实际操作中,你肯定会遇到各种问题。下面是我踩过坑后总结的一些经验。

5.1 数据不完整或遗漏子域名

  • 现象:自己知道存在的某个子域名,在CT日志里没查到。
  • 排查思路
    1. 证书类型:该子域名可能使用了内部CA签发的证书(如企业自建PKI),或使用了私有信任的证书。这类证书没有义务提交到公开CT日志。
    2. 证书有效期:CT日志服务器通常只保留证书一段时间(例如几年)。非常古老的证书可能已被修剪(Log Pruning)。
    3. 日志源覆盖:你使用的API(如crt.sh)可能没有聚合所有日志服务器的数据。尝试换用其他工具交叉验证,如subfinder(带CT模块)、amasstls.bufferover.run等在线服务。
    4. 查询语法:确保查询语法正确。在crt.sh中,%.domain.com%domain.com结果不同,前者要求以.domain.com结尾,后者是模糊匹配包含domain.com。对于根域名,直接查询domain.com

5.2 查询被限制或封禁

  • 现象:请求频繁返回429(Too Many Requests)或其他错误。
  • 解决方案
    • 降低频率:在请求间添加随机延迟(如time.sleep(random.uniform(1, 3)))。对于crt.sh的公开接口,建议至少1秒一次。
    • 使用代理池:如果需要极高频率的查询,考虑使用轮换代理IP。
    • 遵守规则:查阅目标服务的Robots.txt或使用条款。crt.sh的数据库接口明确不鼓励自动化大规模扫描。
    • 考虑付费API:如果需要生产级、稳定的服务,可以考虑类似SecurityTrailsCensysSpyse的付费API,它们通常整合了CT数据并提供更友好的接口和更高的限额。

5.3 结果中包含大量无关或无效域名

  • 现象:结果里出现了很多明显不属于目标公司的域名,或者像*.example.com这样的通配符条目,甚至是证书错误配置导致的乱码。
  • 处理技巧
    1. 严格的后缀匹配:在过滤时,确保使用domain.endswith(‘.example.com’) or domain == ‘example.com’,而不是‘example.com’ in domain,后者会匹配到example.com.attacker.com这种域名。
    2. 处理通配符:通配符证书*.example.com本身是一个有效记录,代表所有一级子域名。你可以选择保留它作为资产记录,也可以在展示时特殊标记或展开为常见子域名列表(如www, mail, api, blog等)。
    3. 清洗数据:对提取的域名进行有效性检查,例如:
      • 移除开头结尾的特殊字符。
      • 检查域名格式是否符合RFC标准(可以使用tldextract库辅助)。
      • 过滤掉明显是IP地址或内部地址(如.local,.internal)的条目,虽然它们在SAN里出现的情况较少。

5.4 性能瓶颈与优化

  • 问题:扫描一个大型组织(拥有数万历史证书)时,脚本运行缓慢甚至内存溢出。
  • 优化方向
    • 流式处理:对于直接爬取日志条目的场景,不要一次性把所有数据加载到内存再解析。应该边下载、边解析、边过滤、边存储(到文件或数据库)。
    • 选择性解析:如果只关心域名,可以只解析证书的SAN扩展部分,而不是整个证书结构。这需要对证书的ASN.1结构有深入了解,或者使用能够部分解析的库。
    • 分布式处理:将目标列表或日志索引范围拆分,在多台机器或多个进程上并行处理。

5.5 与其他工具的联动

CT日志挖掘很少单独使用,它通常是资产发现“武器库”中的一件利器。一个典型的流程是:

  1. 被动收集:使用CT日志、DNS历史记录(如SecurityTrails)、搜索引擎语法(如site:)、网络空间测绘(如Shodan、Fofa)进行初步、广泛的资产发现。
  2. 主动验证:对发现的子域名进行HTTP/HTTPS请求,获取标题、状态码、指纹(如Wappalyzer),确认其存活性和服务类型。
  3. 端口扫描:对存活的IP进行快速端口扫描(如用masscannmap),发现非Web服务。
  4. 漏洞扫描:针对特定的服务或应用进行深度扫描。

你可以将我们写的CT挖掘脚本集成到这样的自动化流程中,作为资产发现流水线的第一个环节。

最后,我想分享一个我自己的体会:技术本身是中立的,CT日志的公开本意是为了提升Web安全。我们在利用它进行资产发现时,务必只针对自己有授权测试的目标,或者自己所属的组织。未经授权扫描他人资产不仅是非法的,也违背了安全社区互助互信的原则。把这项技术用在正确的地方,比如帮助企业完善自身的资产清单、发现被遗忘的“影子IT”,这才是它最大的价值所在。