ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

theHarvester 虚拟主机发现(Virtual Host Discovery)完整实战指南:基于 Host 头与 SNI 的有界安全扫描

2026/9/13 22:15:14 拓冰建站 浏览量
theHarvester 虚拟主机发现(Virtual Host Discovery)完整实战指南:基于 Host 头与 SNI 的有界安全扫描 theHarvester 虚拟主机发现Virtual Host Discovery完整实战指南基于 Host 头与 SNI 的有界安全扫描【免费下载链接】theHarvesterE-mails, subdomains and names Harvester - OSINT项目地址: https://gitcode.com/GitHub_Trending/th/theHarvester导读虚拟主机发现Virtual Host Discovery是 theHarvester 中的一项 P2 级主动探测能力一台 Web 服务器可以在同一个 IP 地址上托管多个站点服务器根据 HTTPHost头HTTPS 下还依据 TLS 的 Server Name Indication即 SNI决定返回哪个站点。本指南讲解 theHarvester 如何将已授权的目标主机名作为候选candidate对收割到的字面 IP 端点发起探测通过与默认/通配响应做差异分类来发现真实存在的虚拟主机并完整覆盖 CLI 用法、请求限额、分类器原理、JSONL/SQLite 证据模型与常见排障。读完本文你将能够安全、有界地运行虚拟主机发现扫描并正确解读其结果与证据。本文对应仓库文档 docs/wiki/Virtual-Host-Discovery.md。⚠️ 授权边界虚拟主机发现会向收割到的 IP 地址或操作者指定的单个字面 IP 端点直接发送 HTTP/HTTPS 请求属于 P2 直接交互。仅在目标、地址、端口与探测技术均处于评估授权范围内时使用。使用前请阅读 Responsible Use and Scope 与 Operator Workflows。虚拟主机发现原理与探测边界当多个站点共享一个 IP 时服务器依据请求中的Host头以及 HTTPS 下的 TLS SNI选择返回哪个虚拟主机。theHarvester 的虚拟主机发现不做 DNS 解析、不基于字典暴力枚举而是基于已收割候选的差异测试harvested-candidate testing它检查一个范围内的主机名是否会产生与该端点默认default或通配wildcard响应不同的响应。从实现上看整个功能封装在 theHarvester/lib/virtual_host.py 中其模块 docstring 开宗明义Probe authorized IP endpoints for virtual hosts without resolving candidate names——候选名称永远不会被解析每个候选必须是目标主机名本身或其子域。范围边界是精确的对www.example.com的授权并不自动覆盖admin.example.com。一次扫描分为两个阶段先由收割源provider收集证据再对目标端点发起探测如下面两幅架构图所示运行一次有界扫描最小命令收割后直接扫描网络活动先是面向 provider 的发现请求随后是对收割到的 IP 端点发起的目标侧请求。AUTHORIZED_DOMAINreplace-with-a-domain-you-control uv run theHarvester \ -d $AUTHORIZED_DOMAIN \ -b rapiddns \ --vhost \ -f report这里选择rapiddns是因为它能同时返回主机名与字面 IP 地址。如果你选用的源只返回主机名就需要启用一个能贡献 IP 证据的 DNS 动作或显式提供--vhost-endpoint。补充已授权的候选名重复使用--vhost-candidate可以追加那些已授权、但所选源没有返回的名称uv run theHarvester \ -d $AUTHORIZED_DOMAIN \ -b rapiddns \ --vhost \ --vhost-candidate admin.${AUTHORIZED_DOMAIN} \ --vhost-candidate preview.${AUTHORIZED_DOMAIN}传输与 TLS 行为虚拟主机发现使用直连传输明确拒绝--proxies。在 theHarvester/main.py 中可以看到只要虚拟主机功能被启用而同时又传入了代理参数会直接抛出ValueError(virtual-host discovery supports direct transport only; do not use --proxies)。HTTPS 证书校验默认开启--vhost-insecure会关闭校验并在证据中记录tls_verified: false。仅在评估确实需要未校验 TLS 且端点已授权时才使用它。探测对象什么会被纳入扫描--vhost使用本次运行已收集的证据字面 IP 地址成为端点扫描先在整组地址上尝试 HTTPS 443 端口之后才尝试 HTTP 80 端口。在源码 virtual_host.py 的_harvested_endpoints()中地址会先按(address.version, int(address))排序去重再生成https://ip:443/优先、http://ip:80/次之的端点序列。精确落在目标边界内的已收割主机名成为候选。DNS 暴力枚举得到的 IP 与名称也会被纳入因为虚拟主机发现在 DNS 暴力枚举之后运行见 Operator Workflows。反向 DNSreverse DNS发现的范围内名称成为候选但反向 DNS 的/24网段不会成为一组新的虚拟主机端点。这是“对已收割候选做差异测试”不是基于字典的虚拟主机暴力枚举。重复--vhost-candidate只是补充一份小的已授权列表弥补所选源未返回的名称。对一次常规收割运行来说--vhost是唯一需要的虚拟主机选项。请求数、运行时、超时与并发等选项属于高级安全覆盖项省略时自动套用有界默认值见下文“请求与运行时限制”。共享的请求上限可能在扫描覆盖全部端点之前就将其停止。此时vhost动作状态为partial停止原因为request-limit扫描不会声称覆盖完整。显式端点替换收割端点网络活动仅对该字面 IP 端点发起目标侧请求。AUTHORIZED_DOMAINreplace-with-a-domain-you-control AUTHORIZED_IPreplace-with-an-authorized-ip uv run theHarvester \ -d $AUTHORIZED_DOMAIN \ --vhost-endpoint https://${AUTHORIZED_IP}:443/ \ --vhost-candidate admin.${AUTHORIZED_DOMAIN} \ -f report端点规范非常严格。源码 normalize_virtual_host_endpoint() 的校验规则如下约束说明scheme必须是http或https地址形式必须是字面 IPv4 或 IPv6 地址ipaddress.ip_address可解析端口可选范围为 1–65535省略时 https 默认 443、http 默认 80路径/查询/片段只允许空路径或/不允许 query 与 fragment用户信息不允许包含用户名/密码userinfo主机名/CIDR一律拒绝主机名端点与 CIDR 网段不合法规范化后的端点为scheme://[压缩地址]:port/形式IPv6 用方括号包裹。候选名的范围校验在 normalize_virtual_host_candidates()每个候选要么等于范围scope要么以其为后缀.scope否则抛出ValueError(virtual-host candidate is outside authorized scope: ...)。同时候选名永远不会被本功能解析——它们只作为 HTTPHost头与 HTTPS SNI 发送。分类器如何工作对每个端点theHarvester 会发送1 次字面 IP 上下文请求context不带 Host 覆盖直接访问端点至少 3 次合成的未知主机控制请求controls且控制名的标签形状与所测候选一致。形状匹配很关键源码中的_candidate_shape()会计算候选相对 scope 的各标签长度元组如admin.example.com相对example.com为(5,)a.b.example.com为(1,1)而_new_control_names()用a-z0-9的 base36 字母表、以secrets.randbelow随机起点生成同形状的未知名称如x7.example.com。这样做的原因是有些服务器的通配行为会随标签深度变化只有同形状的未知主机控制请求才是有效的基线对照。源码还强制要求每个形状至少保留 3 个可用未知控制名36 ** sum(shape) - candidate_count VHOST_CONTROL_COUNT时直接拒绝并在请求数不足以覆盖“1 次上下文 每形状 3 次控制”时抛错。请求行为见 virtual_host.py 的_probe()HTTPS 候选与控制请求在TLS SNI 与 HTTPHost头中发送相同的主机名HTTP 请求只在Host头中发送主机名。使用aiohttpGET 请求allow_redirectsFalse——重定向会被记录为证据但不会跟随。单次请求由timeout_seconds默认 5 秒限制响应体最多读取VHOST_BODY_LIMIT 1 MiB1024 × 1024 字节超出即标记body_truncated。探测阶段phase分为connect、tls、headers、body四级记录请求在哪个阶段失败或成功。扫描首先把收割到的证据与操作者的覆盖项合并成一个有界的可比请求集合对应上文的vhost-sweep-overview.svg随后响应集合进入一个独立分类器对应vhost-classifier.svg。反射归一化Reflection Normalization在比较之前分类器会把响应体或Location头中对当前 authority 的精确反射替换为{authority}占位符。源码_replace_authority()用正则匹配独立的 authority 串前后不允许再有主机名字符并做大小写不敏感替换随后基于归一化后的内容计算body_sha256与body_size。这样做的目的是一个仅仅重复了所请求主机名的通用错误页不应被误判为发现。是否发生替换会记录在证据的reflection_normalized字段中。可比较的信号归一化之后比较可以使用以下信号对应_FINGERPRINT_SIGNALS信号含义statusHTTP 状态码与两条基线均不同location归一化后的重定向位置不同body_size有界响应体大小不同仅当双方都未被截断时才比较body_sha256有界响应体内容不同仅当双方都未被截断时才比较注意_fingerprint_signals()只有在候选与基线的响应体都未截断时才会比较body_size/body_sha256一旦任一侧被截断正文信号就不参与判定。三种分类结果分类器最终返回三种状态之一见 virtual_host.py 的_classify_fingerprints()分类含义default候选与字面 IP 上下文响应或某个稳定的未知主机控制响应一致distinct候选与两条基线都不同纯响应体差异需要第二次匹配的候选响应确认请求indeterminate基线不可用、控制请求彼此不一致、候选只匹配一条基线、正文证据不完整或所需确认未重复出现分类器要求 3 次控制响应完全一致且可用len(set(controls)) 1否则基线不稳定直接判为indeterminate。若候选同时不等于控制与上下文基线则取差集信号作为distinct_signals当差异只体现在正文信号body_size/body_sha256时需要发起确认请求用同一候选名再次探测并得到一致正文后才升级为distinct否则保持indeterminate并标记needs_confirmation。另外若候选恰好是 scope 本身apex 域_classify_candidate()会强制判为indeterminate——apex 作为边界名称不参与发现。只有确认的distinct观察才会被保留在完成结果中。default与indeterminate响应同样消耗预算但不会被报告为发现。请求与运行时限制默认限额如下与源码DEFAULT_VHOST_*常量一致见 virtual_host.py 与main.py 的 argparse 定义控制项CLI 选项默认值整个扫描的请求总数--vhost-request-limit100整个扫描的运行时--vhost-runtime-seconds30 秒单次请求超时--vhost-timeout-seconds5 秒并发候选请求数--vhost-concurrency5请求上限覆盖上下文请求、未知控制请求、候选请求以及任何确认请求。未使用的请求与某个较早端点省下的时间会顺延给后续端点。较低的上限偏向广度在开始 HTTP 之前先在所有收割 IP 上尝试 HTTPS。限额的校验也相当严格VirtualHostLimits.__post_init__request_limit必须为整数且大于基线请求数VHOST_BASELINE_REQUEST_COUNT 1 3 4否则抛错runtime_seconds/timeout_seconds必须为正且有限math.isfiniteconcurrency必须为正整数。预算分配逻辑discover_harvested_virtual_hosts()会按剩余端点数均分剩余请求与剩余时间endpoint_request_limit remaining_requests // endpoints_left、endpoint_runtime remaining_runtime / endpoints_left并用_candidates_for_budget()为每个端点挑选能在预算内完成的候选子集放不下的候选即被截断并标记。此外maximum_endpoint_count request_limit // (VHOST_BASELINE_REQUEST_COUNT 1)端点过多时也会被截断——这些都会导向request-limit停止原因。行动状态action_executions 中的 vhost 条目vhost条目在action_executions中记录以下状态之一对应main.py 的状态映射行动状态含义completed每个选定端点与候选都在限额内完成partial请求或运行时限制停止了覆盖或确认了主机名后发生了请求/扫描/取消错误skipped没有可用的候选名或字面 IP 端点failed请求、扫描或取消错误在保留确认主机名之前就结束了行动停止原因stop_reasonstop_reason更细致地说明结果停止原因含义completed每个选定端点与候选都在限额内完成no-candidates运行中没有范围内主机名可测试no-endpoints运行中没有收割到的字面 IP也没有端点覆盖项request-limit上限导致省略了端点或候选或在确认完成前停止request-errors每个候选都尝试过但一个或多个请求失败runtime-limit共享的墙钟截止时间到期已完成的部分证据被保留scan-error意外的探测错误停止了行动cancelled操作者取消了运行停止原因的优先级在源码中一目了然runtime-limitscan-errorrequest-limitrequest-errors 其余子结果取非completed者。操作者发起的取消与运行时限额不同取消会通过 worker 生命周期传播并关闭活动连接同时在 virtual_host.py 的VirtualHostDiscoveryCancelled中携带已完成的部分证据_preserve_partial_on_cancelTrue以便保留已确认的发现。阅读终端输出扫描结束后汇总行报告确认的端点观察与覆盖情况[*] Virtual hosts: confirmed1; candidate-endpoints18/48; endpoints4/8; requests100 [!] Coverage stopped at the request limit; raise --vhost-request-limit or narrow the scan. admin.authorized.example at https://192.0.2.10:443/: status401; signalsstatuscandidate-endpoint是“一个主机名对一个 IP 端点”的测试对数量。用配对计数可以避免“某个主机名在一个 IP 上测过”被误读为“在其它所有 IP 上都测过”。当覆盖提前停止时[!]行会说明到达的是哪个限额以及由哪个选项控制对应main.py 的stop_hint映射。每个确认观察单独一行主机名 at 端点: status...; signals...其中signals为该发现命中的差异信号列表。HarvestView 与 REST APIHarvestView Web 界面HarvestView 暴露一个“虚拟主机发现”复选框以及可选的端点与候选输入框。高级安全控制项包含请求数、运行时、超时、并发与证书校验的覆盖字段。端点留空则使用收割到的 IP。相关实现见 theHarvester/lib/api/static/harvestview/app.js 与 theHarvester/lib/api/harvestview.py。REST API同一个运行也可以提交到POST /api/v1/runs{ target: authorized.example, sources: [rapiddns], vhost: true }网络活动API 请求本身是本地调用但入队的运行会执行面向 provider 的发现以及面向目标的虚拟主机请求。仅当收割证据无法提供端点或候选时才需要追加vhost_endpoint或vhost_candidates{ target: authorized.example, sources: [], vhost_endpoint: https://192.0.2.10:443/, vhost_candidates: [admin.authorized.example] }请求数、运行时、超时、并发与不安全 TLS 字段覆盖上文相同的默认限额字段名为vhost_request_limit、vhost_runtime_seconds、vhost_timeout_seconds、vhost_concurrency、vhost_insecure见 tests/e2e/test_harvestview.py。行为要点提供vhost_endpoint或vhost_candidates之一即可启用行动即使省略了vhost字段这与 CLI 的vhost_enabled args.vhost or bool(args.vhost_endpoint) or bool(args.vhost_candidates)逻辑一致main.py。sources: []的运行必须同时提供端点和至少一个候选否则缺失的一侧必须来自收割结果。API 在入队前拒绝代理使用、主机名端点、超出范围的候选、以及 IP 目标。完整 API 约定见 REST API。一个发现对应多个端点观察JSONL 输出JSONL 是未版本化的。汇总行之后它为每个确认的主机名存储一条规范发现常规value字段存放主机名actions记录vhost来源而observations是原生数组而不是藏在字符串里的 JSON。候选名是作为 HTTPHost值与 HTTPS SNI 发送的此功能从不解析它们。如果某个主机名在两个端点上都是distinct的两条端点记录都会留在同一条主机名发现下完整 JSONL 发现示例{ type: hostname, value: admin.authorized.example, sources: [], actions: [vhost], observations: [ { endpoint: https://192.0.2.10:443/, http_host: admin.authorized.example, tls_server_name: admin.authorized.example, classification: distinct, phase: body, status: 401, location: null, body_sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, body_size: 123, body_truncated: false, context_phase: body, context_status: 200, context_location: null, context_body_sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, context_body_size: 123, context_body_truncated: false, control_phase: body, control_status: 200, control_location: null, control_body_sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, control_body_size: 123, control_body_truncated: false, confirmation_body_sha256: null, tls_verified: true, distinct_signals: [status], reflection_normalized: false }, { endpoint: https://192.0.2.11:443/, http_host: admin.authorized.example, tls_server_name: admin.authorized.example, classification: distinct, phase: body, status: 403, location: null, body_sha256: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, body_size: 87, body_truncated: false, context_phase: body, context_status: 404, context_location: null, context_body_sha256: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, context_body_size: 87, context_body_truncated: false, control_phase: body, control_status: 404, control_location: null, control_body_sha256: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, control_body_size: 87, control_body_truncated: false, confirmation_body_sha256: null, tls_verified: true, distinct_signals: [status], reflection_normalized: false } ] }字段含义context_*字段是字面 IP 上下文响应control_*是稳定的未知主机控制响应。一个 distinct 候选必须与两者都不同。confirmation_body_sha256仅在重复响应确认了纯正文差异时才出现。这些字段让审查者无需保留每一次合成控制请求即可核验记录的distinct_signals。sources列表为空因为虚拟主机发现是行动action不是被动的信息源source。SQLite 存储SQLite 使用相同的形状而不引入新的证据概念results表存一条hostname行result_origins将其关联到vhost执行记录该结果的 details 中存放端点观察数组。JSONL 导出与 API 运行详情把数组作为原生 JSON 暴露。vhost是行动名不是结果类型result kind。兼容报告的限制JSON 与 XML 兼容报告只包含虚拟主机名列表不含端点或响应证据。自动化需要结构化观察时请使用 JSONL 或GET /api/v1/runs/{run_id}。更完整的本地数据说明见 Results and Local Data。排障运行提示no-candidates选择能返回主机名的源或用--vhost-candidate添加一个已授权名称。如果目标本身是www.example.com这样的主机名请检查精确的目标边界——此时example.com的其它子域并不在候选范围内。运行提示no-endpoints选择能返回 IP 地址的源、启用贡献字面 IP 证据的 DNS 动作或提供--vhost-endpoint。注意反向 DNS 的/24范围不会自动成为端点集合。运行提示request-limit上限对全部端点、候选形状和确认请求来说太小了。在提高上限之前先收窄源集合或候选列表。终端汇总中的attempted/total会显示已尝试的端点比例帮助判断收窄方向。某个候选没有出现在结果中只有确认的distinct观察会被保留。候选可能匹配了默认响应、产生了不稳定的控制响应、传输失败、返回了被截断的正文、或未通过纯正文差异的确认。HTTPS 结果全是indeterminate证书校验或 TLS 协商可能在服务器返回 HTTP 证据之前就失败了。请检查证书对该候选 SNI 是否有效。如果评估允许未校验 TLS可以用--vhost-insecure重跑这个有界测试并把记录的校验状态与结果一起保留。相关文档Responsible Use and ScopeOperator WorkflowsResults and Local DataREST APITroubleshooting【免费下载链接】theHarvesterE-mails, subdomains and names Harvester - OSINT项目地址: https://gitcode.com/GitHub_Trending/th/theHarvester创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考