theHarvester 精确作用域边界:如何以操作者提供的域名(含 www)作为唯一授权范围
theHarvester 精确作用域边界如何以操作者提供的域名含 www作为唯一授权范围【免费下载链接】theHarvesterE-mails, subdomains and names Harvester - OSINT项目地址: https://gitcode.com/GitHub_Trending/th/theHarvester导读theHarvester 是经典的 OSINT 情报收集工具用于从公开源枚举子域名、邮箱、IP 等信息。本文基于仓库中的架构决策记录ADR0007-keep-operator-hostname-as-exact-scope.md状态accepted讲解 theHarvester 如何把操作者提供的 DNS 名称视为精确授权边界并配套给出源码级的实现印证。读完本文你将理解为什么www.example.com不再被当作example.com的别名、统一的主机名规范化流程如何同时处理大小写、尾点与 IDNA 国际化域名、被动 API 的apex 查询为何不会扩大结果范围以及旧式www.剥离解析器为何被移除。决策概述将操作者主机名视为精确授权边界ADR 0007 的核心决策可概括为一句话把操作者提供的 DNS 名称经小写化、去除尾点、IDNA 规范化之后作为精确的授权边界前导的www.标签属于该名称的一部分永远不被当作根域apex domain的别名而移除。被接受的主机名结果必须等于该边界或者是它的后代descendant。这意味着在 theHarvester 中作用域与查询词是两个被刻意分离的概念作用域scope由操作者输入决定是证据能否被接受的唯一判据查询词query由各数据源适配器按 API 契约构造可以也仅能在 API 强制要求时派生 apex 查询词但查询词本身不扩大结果的授权范围。该决策被标记为Status: accepted即已通过评审、成为当前代码库的既定行为。你可以在 docs/adr/0007-keep-operator-hostname-as-exact-scope.md 查看原始记录全文。为什么www.不能被静默剥离ADR 记录给出了做出这一决策的根本理由可以拆解为两点DNS 语义上不存在等价关系DNS 协议并不定义www.example.test与example.test等价。若工具静默剥掉www.等于把对一个 DNS 子树的请求悄悄扩大成对更宽泛的可注册域registrable domain的请求这是一种未经操作者决策的作用域扩张。例如操作者输入www.example.com他可能只关心这个 Web 主机剥掉www.后却会枚举整个example.com的全部子域收集范围远超预期。授权边界的严肃性在 OSINT 工具中枚举范围直接关系到数据合法性与合规边界。扩张作用域等同于在没有新授权的情况下收集了更多资产信息因此必须把是否覆盖 apex的决定权留给操作者本人而不是由工具内部规则替用户做主。第二个理由则对应了查询构造与结果边界分离的设计部分被动 API如证书透明度类服务要求以 apex 域名作为查询参数这类受约束的被动 API仍可被使用但 API 响应的形状不允许重新定义授权边界——无论查询词是什么返回的主机名都必须先经过共享的精确边界规范化才有资格成为证据。精确作用域边界的源码实现ADR 中描述的统一规范化在代码库中由 theHarvester/lib/hostnames.py 集中实现包含两个关键函数。1.normalize_hostname无作用域的规范化该函数对任意主机名执行集中的、与作用域无关的规范化hostnames.py#L4-L28处理顺序如下def normalize_hostname(value: str) - str: hostname value.strip().rstrip(.).lower() # 1. 去首尾空白、去尾点、小写 if not hostname: raise ValueError(hostname must not be empty) try: hostname hostname.encode(idna).decode(ascii) # 2. IDNA 编码国际化域名 → punycode except UnicodeError as error: raise ValueError(hostname must be valid) from error try: ipaddress.ip_address(hostname) # 3. 拒绝 IP 地址 except ValueError: pass else: raise ValueError(hostname must not be an IP address) labels hostname.split(.) # 4. 逐标签校验 if len(hostname) 253 or any( not label or len(label) 63 or label.startswith(-) or label.endswith(-) or not all(character.isalnum() or character - for character in label) for label in labels ): raise ValueError(hostname must be valid) return hostname关键点在于它没有删除www.标签。www.example.test规范化后仍是www.example.test——前导www.被完整保留。同时规范化还集中处理了三类输入形态大小写与尾点strip()rstrip(.)lower()因此WWW.Example.COM.会被规范为www.example.comIDNA/Unicode通过encode(idna).decode(ascii)把 Unicode 域名转为 punycode如münchen.example.test→xn--mnchen-3ya.example.testIP 值通过ipaddress.ip_address()检测并拒绝把 IP 当主机名传入。2.normalize_scoped_hostname作用域内的过滤真正实现精确边界的是normalize_scoped_hostnamehostnames.py#L31-L42def normalize_scoped_hostname(value: object, target: str) - str | None: Return a canonical hostname when value is inside the target boundary. if not isinstance(value, str): return None try: hostname normalize_hostname(value) normalized_target normalize_hostname(target) except ValueError: return None if hostname normalized_target or hostname.endswith(f.{normalized_target}): return hostname return None它的判定规则只有一个规范化后的主机名要么恰好等于规范化后的目标边界本身要么以.规范化目标结尾边界内的后代。二者都不满足的兄弟节点sibling与 apex 名称一律返回None被拒绝。例如目标为www.example.com时dev.www.example.com→ 接受边界的后代www.example.com→ 接受等于边界admin.example.com→拒绝既不是www.example.com本身也不以其为后缀而是它的兄弟。测试印证边界语义的自动化保障仓库测试 tests/lib/test_hostnames.py 为这条决策提供了可执行的契约验证恰好对应 ADR 的三个后果def test_normalize_scoped_hostname_keeps_www_as_the_boundary() - None: assert normalize_scoped_hostname(dev.www.example.com, WWW.Example.COM.) dev.www.example.com assert normalize_scoped_hostname(www.example.com, WWW.Example.COM.) www.example.com assert normalize_scoped_hostname(admin.example.com, WWW.Example.COM.) is None def test_normalize_scoped_hostname_idna_encodes_value_and_target() - None: assert normalize_scoped_hostname(API.München.Example.TEST., münchen.example.test) api.xn--mnchen-3ya.example.test def test_normalize_scoped_hostname_rejects_invalid_or_unscoped_values() - None: assert normalize_scoped_hostname(bad_label.example.com, example.com) is None assert normalize_scoped_hostname(192.0.2.1, example.com) is None assert normalize_scoped_hostname(api.example.com, 192.0.2.1) is None assert normalize_scoped_hostname(123, example.com) is None注意测试名keeps_www_as_the_boundary它把www.example.com作为边界验证admin.example.com会被拒绝——这正是前导www.标签属于名称的一部分、不是 apex 别名的直接自动化体现。IDNA 测试则证明边界和目标都做同一套 IDNA 规范化后再比较münchen.example.test与它的 punycode 形式互为等价而不是简单字符串前缀匹配。无效值非法标签、IP、非字符串则一律返回None保证边界比较不会把垃圾数据放进来。共享边界所有数据源结果必经的统一闸门ADR 强调共享的精确边界规范化必须贯穿所有数据源。这一点在结果汇聚层与 CLI 入口层均有体现。结果汇聚层source_runner的_normalize_values在 theHarvester/lib/source_runner.py#L201-L225 中所有子域名类证据在成为观测记录ResultObservation之前都要经过_normalize_valuesdef _normalize_values(request: SourceRequest, kind: ResultKind, values: Iterable[object]) - set[ResultObservation]: observations: set[ResultObservation] set() canonical_target normalize_scoped_hostname(request.target, request.target) for item in values: if kind hostname: value normalize_scoped_hostname(item, request.target) if value is None or value canonical_target: continue ... observations.add(ResultObservation(request.source, kind, value)) return observations两个细节与 ADR 精确对应canonical_target normalize_scoped_hostname(request.target, request.target)先把目标自身规范化一次得到规范形态的边界之后任何主机名都要通过normalize_scoped_hostname(item, request.target)过滤边界自身value canonical_target会被排除出发现结果——因为 apex/边界本身是查询起点而非新发现避免把目标自己当作发现项上报。而_collect_observationssource_runner.py#L240-L276为每个数据源按路由subdomains、emails、ips 等拉取结果并统一送入_normalize_values无论是普通主机名、rapiddns 的 host-IP 对、还是 shodan 的扩展证据全部走同一个闸门从机制上保证任何适配器都无法绕过共享结果规范化。CLI 入口层__main__的存储前过滤在 theHarvester/main.py#L93-L99 中_normalize_hosts_for_storage在把发现的主机写入存储之前再做一次同样的过滤def _normalize_hosts_for_storage(discovered_hosts: Iterable[object], target: str) - set[str]: canonical_target normalize_scoped_hostname(target, target) return { normalized for host in discovered_hosts if (normalized : normalize_scoped_hostname(host, target)) and normalized ! canonical_target }同样先对目标求规范化边界再逐个过滤发现项并排除边界本身。这意味着即使某个适配器内部有私有的清洗逻辑最终落盘的数据也必然落在操作者指定的精确边界内。虚拟主机发现边界被规范化后复用在虚拟主机vhost发现流程中入口处同样先规范化作用域theHarvester/main.py#L432 执行vhost_scope normalize_hostname(args.domain)随后在main.py#L1535 用normalize_scoped_hostname(candidate, vhost_scope)逐个过滤候选 vhost。也就是说精确边界语义同样适用于虚拟主机扫描以www.example.com为域时vhost 候选也必须落在www.example.com或其子树内。被动 API 的 apex 查询不会扩大结果范围ADR 特别提到被动数据源适配器只有在 API 契约要求时才可以派生 apex 查询词。仓库中有两个典型示例印证了这一设计且它们都遵循同一模式——查询参数可能是 apex但返回结果仍必须通过normalize_scoped_hostname过滤示例一crtname以apex为查询参数theHarvester/discovery/crtname.py#L45-L82 中适配器向证书透明度类端点发起流式请求查询参数就是apexasync with AsyncFetcher.stream_records( self.ENDPOINT, framingndjson, params{apex: self.word}, ... ) as response: ... async for record in response: candidate record.strip() ... candidate candidate.lower().removeprefix(*.).removesuffix(.) if not self._valid_hostname(candidate): malformed True continue normalized normalize_scoped_hostname(candidate, self.word) if normalized is None: malformed True elif normalized ! self.word: self.hostnames.add(normalized)可见params{apex: self.word}只是为满足 API 契约而构造的查询词而每条记录在加入结果集前必须经过normalize_scoped_hostname(candidate, self.word)的精确边界校验等于边界的目标自身normalized ! self.word也会被剔除。查询词与结果边界在这里被严格分离——即使 API 因为 apex 查询返回了大量同名兄弟域记录也只有边界内的记录能成为证据。示例二securitytrailssearch把子域拼回目标再校验theHarvester/discovery/securitytrailssearch.py#L57-L69 展示了另一个方向的防护SecurityTrails 的 subdomains 接口返回的是相对标签如dev、www适配器把它们拼回目标域后再交给边界校验def _parse_subdomains(self, data: dict[str, Any]) - bool: values data.get(subdomains) ... for value in values: if not isinstance(value, str) or not value.strip(): malformed True continue if hostname : normalize_scoped_hostname(f{value}.{self.word}, self.word): hostnames.add(hostname) return malformedf{value}.{self.word}先重建完整主机名normalize_scoped_hostname(..., self.word)再验证它是否落在以self.word为边界的子树内。因为每个返回标签都被拼接后校验即使 API 返回了越界标签例如把 apex 自身的记录混入 subdomains 列表也会被边界过滤拦截。其余适配器同一个函数的广泛复用从源码结构看normalize_scoped_hostname是全仓库使用最广泛的主机名过滤入口。除上述两个适配器外它还出现在 virustotal.py、fullhuntsearch.py、shodansearch.py、dns_consensus.py、recursive_dns.py、takeover.py、myparser.py 等数十处调用点覆盖了证书透明度、搜索引擎、DNS 一致性汇总、递归 DNS、接管检测、通用解析器等多个环节——这正是 ADR 所述每个返回的主机名都经过共享的精确边界规范化的全仓库落地。旧式www.剥离规则无处可寻的遗留实现ADR 的后果部分提到实现旧式www.剥离规则、且没有活跃调用者的遗留解析器将被移除。从当前仓库源码检索情况看这一点已经落实在theHarvester/全部源码中搜索removeprefix(www、startswith(www等剥离模式均无命中所有www.出现点如 baidusearch.py、onyphe.py、mojeek.py、criminalip.py、virustotal.py都是普通字符串内容或正则匹配片段并非剥掉www.当作 apex的逻辑唯一把www作为前缀处理的 myparser.py 也只是urls()方法里对trello.comURL 的匹配与主机名规范化无关。可以推断历史版本中解析器剥离www.的行为已被统一收敛到normalize_hostname/normalize_scoped_hostname这套不剥离www.的集中逻辑中遗留规则在缺少活跃调用者后即被清除。实践要点与边界语义小结把 ADR、源码与测试串起来可以得到一套可直接用于理解乃至二次开发的精确边界操作规则输入 / 场景行为依据目标www.example.com结果admin.example.com拒绝兄弟节点不在子树内hostnames.py#L31-L42目标www.example.com结果dev.www.example.com接受边界后代test_hostnames.py#L4-L7目标münchen.example.test结果API.München.Example.TEST.接受规范化为api.xn--mnchen-3ya.example.testtest_hostnames.py#L10-L15结果为 IP 或非法标签一律拒绝hostnames.py#L12-L27API 以 apex 为查询参数允许但结果仍按原目标边界过滤crtname.py#L45-L82相对子域标签如 SecurityTrails拼接回目标后再校验securitytrailssearch.py#L57-L69边界自身出现在结果中被过滤不作为新发现source_runner.py#L201-L225给操作者的实践建议明确输入边界当你只想枚举某个 Web 主机时输入www.example.com需要整域资产时输入example.com。工具不再替你纠正www.作用域完全由你的输入决定——这也是安全合规场景下最稳妥的行为。理解 IDNA 域名的等价性Unicode 域名如münchen.example.test与 punycodexn--mnchen-3ya.example.test在边界判定中视为同一目标可任选其一作为输入。关注边界自身不参与结果规范化目标本身apex 或www边界被视为查询起点而非发现项不会出现在子域名结果集中。延伸阅读决策原文docs/adr/0007-keep-operator-hostname-as-exact-scope.md边界规范化实现theHarvester/lib/hostnames.py边界语义测试tests/lib/test_hostnames.py结果汇聚闸门theHarvester/lib/source_runner.py被动 API apex 查询示例theHarvester/discovery/crtname.py、theHarvester/discovery/securitytrailssearch.py通用解析器边界过滤theHarvester/parsers/myparser.py相关 ADR 背景docs/adr/0005-persist-asn-organization-attribution-as-sourced-evidence.md、docs/adr/0009-scope-proxy-mode-to-http-transport.md【免费下载链接】theHarvesterE-mails, subdomains and names Harvester - OSINT项目地址: https://gitcode.com/GitHub_Trending/th/theHarvester创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考