1. 项目概述:为什么我们需要“高级”子域名发现?
在网络安全评估、渗透测试,甚至是日常的资产梳理工作中,子域名发现都是最基础、也最关键的起点。你可能用过subfinder、amass这类工具,它们能快速拉出一堆子域名,但很多时候你会发现,结果要么不全,要么充斥着大量无效或过时的记录。这就是“基础”与“高级”的区别所在。基础方法依赖公开的DNS数据集和暴力枚举,而高级子域名发现,则更像一个侦探,它不满足于询问“谁在这里登记过”,而是去翻查“谁在这里留下过痕迹”。
这个项目标题——“高级子域名发现:证书透明度、爬虫与JS文件分析”——精准地指出了三个核心的、能挖掘出“隐藏资产”的线索来源。证书透明度(CT)日志记录了所有公开签发的SSL/TLS证书,里面可能包含从未在公开DNS中解析过的内部域名;网络爬虫能模拟真实用户访问,从网页的HTML、注释、甚至跳转逻辑中发现未被直接链接的子域名;而JS文件分析,则是从现代Web应用复杂的JavaScript代码中,提取出API端点、第三方服务域名等“动态资产”。把这些手段组合起来,你构建的就不再是一个简单的域名列表,而是一张目标组织的数字资产地图。这张地图的完整度,直接决定了后续安全测试的覆盖面和深度。无论是安全工程师、红队成员,还是负责企业资产管理的运维,掌握这套方法都意味着你能看到别人看不到的东西。
2. 核心思路与方案选型:构建一个立体的侦察体系
传统的子域名发现工具,其数据源相对单一,主要依靠:
- 被动数据源:如VirusTotal、SecurityTrails、Censys等平台聚合的DNS历史记录。
- 字典暴力枚举:使用庞大的子域名字典,向目标的权威DNS服务器发起查询。 这些方法有效,但存在明显短板:被动数据可能滞后或遗漏;暴力枚举则会产生大量噪音,且对没有泛解析的域名效果有限。
因此,我们的高级方案设计思路是“主动侦察 + 深度内容挖掘”,形成一个立体化的信息收集管道:
2.1 为什么选择证书透明度(CT)?当网站启用HTTPS时,证书颁发机构(CA)需要将签发的证书信息公开记录到CT日志中。这些日志是公开可查的宝藏。一个为dev.internal.example.com签发的证书,即使这个域名从未对外提供Web服务,它的记录也会留在CT日志里。这完美解决了“内部系统域名外部不可见”的问题。我们选择使用crt.sh的API或直接查询公共CT日志服务器(如Google的pilots)作为数据源。它的优势在于数据权威、实时性强,能发现大量在常规扫描中遗漏的测试、开发、预发布环境域名。
2.2 为什么需要定制化爬虫,而非通用爬虫?通用爬虫(如gospider,hakrawler)速度快,但“智商”不高。它们主要提取明显的链接(href,src)。而定制爬虫的核心任务是“理解上下文”。例如:
- 从JavaScript代码中提取字符串:正则匹配类似
api.、ws.、internal.开头的域名模式。 - 解析HTML注释:开发人员经常在注释里留下测试地址,如
<!-- 测试环境: https://staging.app.example.com -->。 - 处理JavaScript重定向:有些子域名只在特定条件下通过JS跳转,需要执行JS才能发现。 我们选择用Python的
requests/aiohttp库配合BeautifulSoup/lxml进行静态分析,并结合Playwright或Selenium进行动态渲染,以应对现代前端框架(如React, Vue)构建的单页应用。这样能确保我们抓取到页面完全加载后的所有内容。
2.3 为什么聚焦JS文件分析?在现代Web开发中,前后端分离是主流。前端JavaScript文件里包含了大量的硬编码URL、API路径、第三方服务域名(如CDN、统计、客服系统)。这些信息往往不会出现在HTML中,却是应用逻辑的重要组成部分。分析JS文件,就是直接“翻阅”应用的通信手册。我们不仅要从爬虫下载的JS文件中提取,还应主动从类似https://example.com/static/js/main.abc123.js这样的常见路径中寻找,或从页面的<script>标签中收集JS URL进行深度下载和分析。
方案整合架构:整个流程将设计为一个管道。首先,从CT日志获取一批“种子”域名;然后,用定制爬虫对这些域名的Web服务进行深度抓取,从中提取更多子域名和JS文件路径;最后,对收集到的所有JS文件进行静态分析,提取出隐藏的域名和端点。这三个环节相互反馈,形成循环,不断扩充资产列表。
3. 核心模块详解与实操要点
3.1 证书透明度(CT)日志挖掘实战
CT日志查询的核心是理解其数据结构和访问方式。最常用的入口是crt.sh网站及其提供的PostgreSQL接口。不过,直接调用公共API更便于自动化。
实操步骤:
基础查询:使用
crt.sh的API,构造一个简单的查询来获取为目标域名签发的所有证书。# 使用curl示例 curl -s "https://crt.sh/?q=%.example.com&output=json" | jq -r '.[].name_value' | sed 's/\*\.//g' | sort -u这个命令会查询所有包含
.example.com的证书,并提取name_value字段(即证书中的域名),去除通配符*.前缀,最后去重排序。处理通配符与SAN字段:一个证书可能包含多个域名(主题备用名称,SAN)。
crt.sh的JSON输出中,name_value字段通常以\n分隔多个域名。我们需要妥善分割。此外,通配符证书(如*.internal.example.com)提示存在一个庞大的子域名空间,可以将其作为暴力枚举的字典来源。构建自动化脚本:为了提高效率,我们可以用Python编写一个模块。
import requests import json def query_ct_logs(domain): url = f"https://crt.sh/?q=%.{domain}&output=json" try: resp = requests.get(url, timeout=30) resp.raise_for_status() data = resp.json() subdomains = set() for entry in data: # 处理name_value字段,可能是字符串或列表 name_value = entry.get('name_value', '') if isinstance(name_value, str): names = name_value.split('\\n') else: names = name_value for name in names: name = name.strip().lower() if name.startswith('*.'): name = name[2:] # 移除通配符,保留根域 # 可以将根域加入列表,或根据它生成字典 if domain in name: # 简单过滤,确保相关性 subdomains.add(name) return list(subdomains) except Exception as e: print(f"查询CT日志失败: {e}") return [] if __name__ == '__main__': domains = query_ct_logs('example.com') for d in domains: print(d)
注意事项与心得:
注意:
crt.sh的API有速率限制,过于频繁的请求会导致IP被临时屏蔽。在脚本中务必加入延时(如time.sleep(1))和错误重试机制。心得一:数据清洗是关键。CT日志数据很“脏”,包含大量过期证书、重复条目、甚至是不相关的域名。提取后一定要进行严格的去重和有效性过滤(例如,只保留以目标域名结尾的项)。心得二:关注“关联域名”。证书里可能包含邮箱域名(@company.com)、或其他关联企业的域名。这些虽然不是直接的子域名,但对于绘制攻击面非常有价值,应单独归类记录。
3.2 智能爬虫的设计与避坑指南
写一个能用于资产发现的爬虫,和写一个数据抓取爬虫,侧重点完全不同。我们的目标是“发现”,而非“爬取全部内容”,因此需要精心设计策略。
核心设计要点:
- 广度优先与深度限制:采用广度优先搜索(BFS)来最大化覆盖不同路径。同时,必须设置爬取深度(例如3-4层),避免陷入无穷无尽的链接中,或者爬取到完全无关的站外链接。
- 遵守
robots.txt与伦理:在发起请求前,先获取并解析目标域的robots.txt文件,尊重Disallow规则。这是法律和伦理要求,也能避免你的IP被迅速封禁。 - 请求头伪装与速率控制:使用常见的浏览器User-Agent(如Chrome),并添加
Referer、Accept-Language等头,让请求看起来更像真人。在两个请求之间设置随机延时(如1-3秒),大幅降低对目标服务器的压力。 - 动态内容渲染:对于疑似SPA(单页应用)的网站,静态HTML解析一无所获。此时需要启动无头浏览器(如Playwright)。
动态渲染资源消耗大、速度慢,应仅作为静态爬虫的补充,针对性地用于重要或可疑的URL。from playwright.sync_api import sync_playwright def dynamic_crawl(url): with sync_playwright() as p: browser = p.chromium.launch(headless=True) # 无头模式 page = browser.new_page() page.goto(url, wait_until='networkidle') # 等待网络空闲 # 获取渲染后的HTML content = page.content() # 也可以执行JS来触发某些事件 # page.click('button#loadMore') browser.close() return content
从内容中提取子域名的技巧:
- 正则表达式:使用如
r'[a-zA-Z0-9][a-zA-Z0-9-]*\\.example\\.com'的模式进行匹配。注意转义点号,并考虑域名中可能包含下划线(虽然不符合标准,但确实存在)。 - HTML标签属性:不仅扫描
href和src,还要关注action(表单提交地址)、>import esprima import re def extract_from_js_with_ast(js_code): endpoints = set() try: tree = esprima.parseScript(js_code, tolerant=True) # 简化示例:遍历AST寻找字面量字符串 for node in esprima.walk(tree): if node.type == 'Literal' and isinstance(node.value, str): if re.match(r'^/api/[\w/-]+', node.value): endpoints.add(node.value) # 也可以检查是否包含域名 except Exception as e: print(f"AST解析失败,降级为正则匹配: {e}") # 降级方案:使用正则表达式 endpoints.update(re.findall(r'["\'](/api/[\w/-]+)["\']', js_code)) return endpoints - 输入:一个根域名,例如
example.com。 - 阶段一:初始发现:
- 调用CT日志查询模块,获取第一批子域名列表
A。 - 使用一个基础的子域名字典进行快速DNS枚举,获取列表
B。 - 合并
A和B,去重,得到初始资产集S。
- 调用CT日志查询模块,获取第一批子域名列表
- 阶段二:内容增强:
- 对
S中所有支持HTTP/HTTPS的域名,启动智能爬虫模块。 - 爬虫从页面中提取新的子域名和JS文件URL,分别加入集合
S_new和JS_urls。 - 将
S_new合并回S。
- 对
- 阶段三:深度挖掘:
- 下载
JS_urls中的所有JavaScript文件。 - 使用JS文件分析模块,从代码中提取隐藏的域名和API端点,得到集合
S_js和Endpoints。 - 将
S_js合并回S。
- 下载
- 阶段四:验证与输出:
- 对最终集合
S中的所有域名进行简单的存活验证(如HTTP状态码200/403/500等,或TCP端口扫描)。 - 输出结构化的报告,包括:有效子域名列表、对应的IP地址、开放的服务(Web、非Web)、发现的API端点、以及每个资产的来源(CT/爬虫/JS)。
- 对最终集合
实操心得:
心得一:分层处理。不要对所有JS文件一视同仁。优先分析来自主域名、且文件名类似
app、main、vendor的大型JS文件,它们包含核心逻辑的概率更高。来自第三方CDN(如cdn.bootcss.com)的库文件,信息价值相对较低。心得二:注意误报。JS中可能包含示例代码、注释掉的URL或用于单元测试的假域名。提取到的所有信息都需要经过一步验证,比如尝试解析DNS或发送一个简单的HTTP HEAD请求,确认其真实存在。心得三:保存上下文。仅仅记录一个域名api.internal.com是不够的。最好能记录它是在哪个主域名的哪个JS文件中被发现的,以及其前后的几行代码。这能为后续的漏洞利用或深入测试提供宝贵线索。
4. 系统整合与自动化流程实现
将三个模块串联起来,形成一个自动化的工作流,是提升效率的关键。这里我设计一个基于Python的简单框架流程。
4.1 整体架构与数据流整个系统可以看作一个“发现-增强-验证”的循环管道。
4.2 核心代码结构示例
# 这是一个简化的主流程示例 import asyncio from modules.ct_scanner import CTScanner frommodules.intelligent_crawler import Crawler frommodules.js_analyzer import JSAnalyzer frommodules.validator import Validator class AdvancedSubdomainDiscoverer: def __init__(self, root_domain): self.root_domain = root_domain self.all_subdomains = set() self.js_urls = set() self.endpoints = set() self.ct_scanner = CTScanner() self.crawler = Crawler(rate_limit=1) # 1秒延迟 self.js_analyzer = JSAnalyzer() self.validator = Validator() async def run(self): print(f"[*] 开始对 {self.root_domain} 进行高级子域名发现") # 1. CT日志查询 print("[*] 阶段1: 查询证书透明度日志...") ct_subs = await self.ct_scanner.query(self.root_domain) self.all_subdomains.update(ct_subs) print(f" 从CT日志发现 {len(ct_subs)} 个子域名。") # 2. 初始爬取与发现 print("[*] 阶段2: 启动智能爬虫进行内容抓取...") # 这里简化:只爬取初始集合中的前N个,实际应做任务队列 initial_targets = list(self.all_subdomains)[:10] for target in initial_targets: new_subs, new_js_urls = await self.crawler.crawl(f"http://{target}") self.all_subdomains.update(new_subs) self.js_urls.update(new_js_urls) # 3. JS文件深度分析 print("[*] 阶段3: 分析收集到的JavaScript文件...") for js_url in list(self.js_urls)[:20]: # 限制分析数量 hidden_subs, found_endpoints = await self.js_analyzer.analyze(js_url) self.all_subdomains.update(hidden_subs) self.endpoints.update(found_endpoints) # 4. 验证与输出 print("[*] 阶段4: 验证资产存活状态...") validated_assets = await self.validator.validate_async(list(self.all_subdomains)) # 生成报告 self.generate_report(validated_assets) def generate_report(self, assets): # 输出报告到文件和控制台 with open(f"{self.root_domain}_report.txt", 'w') as f: f.write(f"目标根域名: {self.root_domain}\\n") f.write(f"发现子域名总数: {len(self.all_subdomains)}\\n") f.write(f"发现API端点总数: {len(self.endpoints)}\\n\\n") f.write("=== 已验证的存活资产 ===\\n") for asset in assets: f.write(f"{asset['domain']} - IP: {asset['ip']} - 状态: {asset['status']}\\n") print(f"[+] 报告已生成: {self.root_domain}_report.txt") # 异步主函数 async def main(): discoverer = AdvancedSubdomainDiscoverer("example.com") await discoverer.run() if __name__ == '__main__': asyncio.run(main())4.3 性能优化与工程化考虑
- 异步并发:爬虫和网络验证是I/O密集型任务,使用
asyncio和aiohttp可以极大提升效率,同时控制并发连接数,避免对目标造成过大压力。 - 任务队列与去重:使用
Redis或内存中的队列管理待爬取URL,并在入队时进行严格去重(基于URL规范化后的结果)。 - 结果持久化:使用SQLite或MySQL存储发现结果,记录发现时间、来源、验证状态等,便于后续跟踪和增量扫描。
- 配置化管理:将目标域名、爬虫深度、请求头、代理设置、关键词字典等参数外置到配置文件(如
config.yaml)中,提高灵活性。
5. 常见问题、排查技巧与实战心得
在实际运行这套系统的过程中,你会遇到各种各样的问题。下面是我踩过坑后总结的一些典型场景和解决方法。
5.1 数据源失效或受限
- 问题:
crt.shAPI无法访问或返回空数据。 - 排查:首先检查网络连通性。其次,直接访问
https://crt.sh/?q=example.com看网页是否正常。如果网页正常但API失败,可能是API端点临时变更或增加了防护(如Cloudflare WAF)。 - 解决:
- 添加更完整的请求头模拟浏览器。
- 使用其他CT日志查询接口作为备用,如
censys.io或transparencyreport.google.com提供的视图(虽然交互方式不同)。 - 在脚本中实现简单的重试机制和退避策略(如首次失败等待5秒再试)。
5.2 爬虫被封锁或收到非预期响应
- 问题:大量请求返回403/429状态码,或收到验证码页面。
- 排查:
- 检查请求头是否完整(特别是
User-Agent,Accept)。 - 检查请求频率是否过高。
- 查看响应内容,是否包含“Access Denied”、“Rate Limited”或验证码HTML。
- 检查请求头是否完整(特别是
- 解决:
- 降低频率:这是最有效的方法。将请求间隔从固定值改为随机范围(如
random.uniform(2, 5)秒)。 - 使用代理池:如果目标防护严密,需要轮换多个IP地址。可以使用一些免费的代理API(注意稳定性和安全性),或搭建自己的代理服务器。
- 模拟浏览器行为:对于需要登录或复杂交互的网站,考虑使用
Playwright等工具完全模拟浏览器会话,包括处理Cookie。 - 设置超时与重试:对网络超时设置合理的阈值,并对非致命的5xx错误进行有限次重试。
- 降低频率:这是最有效的方法。将请求间隔从固定值改为随机范围(如
5.3 JS分析误报率过高
- 问题:提取出大量明显无效的域名,如本地地址(
localhost)、示例域名(example.com)、或其他完全不相关的商业域名。 - 排查:检查正则表达式或AST遍历规则是否过于宽松。查看提取到的字符串上下文。
- 解决:
- 强化过滤规则:建立强大的黑名单,过滤掉
localhost、127.0.0.1、example.com、test.com等。同时,使用白名单模式,只保留以目标根域名或其父域结尾的字符串。 - 上下文关联:不仅仅提取字符串,还尝试分析其所在的代码逻辑。例如,一个字符串如果被用在
fetch()、XMLHttpRequest或window.location赋值中,它是真实URL的可能性就大大增加。 - 人工审核样本:定期对提取结果进行抽样检查,根据误报案例调整分析规则。
- 强化过滤规则:建立强大的黑名单,过滤掉
5.4 结果庞杂,难以聚焦
- 问题:最终发现了成百上千个子域名,不知道哪些是重点。
- 解决:引入“资产优先级评分”机制。
- 来源权重:从CT日志发现的、特别是包含
api、admin、dev、staging、internal等关键词的域名,权重调高。 - 响应特征:返回状态码为200、301、302的域名,权重高于返回404的。具有独特Title或特殊HTTP头的(如
X-Powered-By: ASP.NET),权重调高。 - 端口与服务:除了80/443,还开放了其他端口(如8080, 8443, 22, 3306)的资产,需要重点关注。
- 关联性:在多个独立来源(如CT和JS分析)中都出现的域名,可信度和重要性更高。
- 来源权重:从CT日志发现的、特别是包含
最后一点个人体会:高级子域名发现不是一个一劳永逸的工具,而是一个需要持续调优的过程。每个目标都有其独特性。最好的策略是让工具跑起来,拿到初步结果,然后人工去分析这些结果:为什么这个域名会出现?它属于哪个系统?通过这种反馈,反过来优化你的爬虫规则、JS分析关键词和过滤逻辑。这套方法的真正威力,在于将自动化工具的广度与安全研究员的分析深度结合起来,最终让你对目标的攻击面有一个远超常人的清晰认知。