截至2026年10月7日,Shifter拥有规模最大的美国实时IP池,超过Oxylabs和Bright Data,而成本只是其一小部分。Shifter 现在拥有数量最多的美国活跃 IP 资源池

查看基准测试

被动威胁情报:不接触目标也能收集情报

如何在不向目标发送任何数据包的情况下收集域名威胁情报:证书日志、RDAP、存档和被动DNS,附经过测试的Python代码及其局限性。

工程 · 更新于2026年11月1日 · 3分钟阅读
CT logsRDAPWaybackpDNS扫描数据你目标
数据源早已看到了目标。琥珀色线代表你永远不会发送的请求。
知识
本页内容
  1. 被动的含义
  2. 值得检查的来源
  3. 一个小型被动采集工具
  4. 被动不再是被动的边界
  5. 如何处理这些结果
  6. 结论
  7. 来源与参考资料

在威胁情报团队扫描任何目标之前,它可以从其他方已经保存的记录中了解到一个域名的大量信息。证书日志列出了它的主机名,注册数据给出了它的日期和域名服务器,而网络存档则显示了它过去发布过的内容。对安全团队来说,这一被动阶段是整个工作中成本最低、最不引人注意的部分,而且它所展示的内容任何攻击者同样能够看到。

本指南涵盖了主要的被动来源、一个经过测试的小型采集工具,以及”被动”不再成立的边界所在。请在你自己的组织内使用它,或者在经过授权的范围内使用。

被动的含义

被动收集意味着你的请求发往已经持有相关数据的第三方,而从不发往目标自身的服务器。MITRE ATT&CK 在”侦察”(Reconnaissance)战术下描述了攻击者的这一做法,包括搜索”可自由获取的技术数据库”(T1596),其中列出了 DNS 与被动 DNS、WHOIS、数字证书、CDN 以及扫描数据库等条目。

MITRE 也解释了防御者为何应该关注这一点。它指出,这类活动可以发生在目标组织的可见范围之外,因此难以检测,也无法通过预防性控制措施轻易缓解。你无法阻止有人去读取一份公开的日志。你能做的,是先一步去查看,并减少这些日志所暴露的信息。

值得检查的来源

来源能提供什么局限性
证书透明度日志已签发 TLS 证书上的主机名只显示名称,不表明是否在用;通配符会隐藏子域名
RDAP注册商、日期、域名服务器个人数据通常会被遮蔽
网络存档旧页面、URL 和主机仅限于被抓取过的内容,往往已是多年前的
被动 DNS一个名称曾解析到哪些 IP,以及解析时间多为商业服务,覆盖范围各不相同
互联网扫描数据库其他人观测到的开放端口与服务标语属于快照,可能已经过时

证书透明度是最丰富的免费来源。RFC 9162 描述了一种协议,用于”公开记录”TLS 证书的存在,以便任何人都能审计证书颁发机构,而且日志是只能追加、不可删改的。这也意味着,即便 staging.internal-tool.example.com 这样的主机只存在了一周,它的证书记录依然会保持可见。

RDAP 是 WHOIS 的结构化替代方案。ICANN 表示,自 2025 年 1 月 28 日起,通用顶级域的注册局和注册商必须提供 RDAP,而不再被要求提供 WHOIS,因此新的工具应当从这里开始。

我们关于什么是 OSINT 以及其法律边界所在的指南涵盖了这一更广泛的学科,包括与人相关的来源,而本指南不涉及这些内容。

一个小型被动采集工具

下面的脚本会向三个第三方服务询问有关某个域名的信息,并且从不连接该域名本身。它运行于 Python 3.9 及以上版本,只需要 requests 库。当某个来源不可用时它会继续运行,因为免费的公共服务经常会出问题:在测试本文内容期间,crt.sh 和 Wayback Machine 都曾连续数分钟返回错误和超时。

from __future__ import annotations

import sys
import time

import requests

TIMEOUT = (10, 60)
HEADERS = {"User-Agent": "passive-recon-demo/1.0"}


def get_json(url, params=None):
    """Every request goes to a third-party source, never to the target itself."""
    for attempt in range(3):
        try:
            r = requests.get(url, params=params, headers=HEADERS, timeout=TIMEOUT)
            r.raise_for_status()
            return r.json()
        except requests.RequestException:
            if attempt == 2:
                raise
            time.sleep(10 * (attempt + 1))  # free public services go down often


def in_scope(host, domain):
    """The domain or a real subdomain of it; CT names also hold labels and lookalikes."""
    return " " not in host and (host == domain or host.endswith("." + domain))


def certificate_names(domain):
    """Hostnames from Certificate Transparency logs, through crt.sh."""
    rows = get_json("https://crt.sh/", {"q": f"%.{domain}", "output": "json"})
    names = {n.strip().lower() for row in rows for n in row["name_value"].splitlines()}
    return sorted(n for n in names if "*" not in n and in_scope(n, domain))


def archived_hosts(domain, limit=500):
    """Hostnames the Wayback Machine has captured, without loading any live page."""
    rows = get_json("https://web.archive.org/cdx/search/cdx", {
        "url": domain, "matchType": "domain", "output": "json",
        "fl": "original", "limit": limit,
    })
    urls = [row[0] for row in rows[1:]]  # the first row is the header
    hosts = {u.split("//")[-1].split("/")[0].split(":")[0].lower() for u in urls}
    return sorted(h for h in hosts if in_scope(h, domain))


def registration(domain):
    """Registrar and key dates from RDAP, the structured successor to WHOIS."""
    data = get_json(f"https://rdap.org/domain/{domain}")
    dates = {e["eventAction"]: e["eventDate"][:10] for e in data.get("events", [])}
    return {"registered": dates.get("registration"), "expires": dates.get("expiration"),
            "nameservers": sorted(n["ldhName"].lower() for n in data.get("nameservers", []))}


def collect(domain):
    sources = {"certificates": certificate_names, "archive": archived_hosts,
               "rdap": registration}
    found, failed = {}, {}
    for name, fetch in sources.items():
        try:
            found[name] = fetch(domain)
        except (requests.RequestException, ValueError, KeyError) as exc:
            failed[name] = type(exc).__name__  # a source being down is a result too
    return found, failed


if __name__ == "__main__":
    domain = sys.argv[1] if len(sys.argv) > 1 else "example.com"
    found, failed = collect(domain)
    hosts = set(found.get("certificates", [])) | set(found.get("archive", []))
    only_ct = set(found.get("certificates", [])) - set(found.get("archive", []))
    only_archive = set(found.get("archive", [])) - set(found.get("certificates", []))
    print(f"{domain}: {len(hosts)} hostnames seen by third parties")
    print(f"  in certificates only: {sorted(only_ct)[:6]}")
    print(f"  in the archive only:  {sorted(only_archive)[:6]}")
    print(f"  registration: {found.get('rdap')}")
    print(f"  sources unavailable: {failed or 'none'}")

对 example.com 的输出结果:

example.com: 7 hostnames seen by third parties
  in certificates only: ['dev.example.com', 'emudhra-lab.example.com', 'm.example.com', 'products.example.com', 'support.example.com']
  in the archive only:  []
  registration: {'registered': '1995-08-14', 'expires': '2027-08-13', 'nameservers': ['elliott.ns.cloudflare.com', 'hera.ns.cloudflare.com']}
  sources unavailable: none

这段代码中有两处细节来自测试过程。证书日志中包含的不仅仅是主机名:早期版本使用了简单的后缀检查,结果放过了一个带空格的证书标签,以及一个形似的名称 m.testexample.com。这就是为什么 in_scope 要匹配整个子域名。另一个细节是,每个来源看到的内容各不相同,因此应当相互比对,而不是只信任其中一个。一个只出现在旧存档中的主机名可能已经不存在了,而一个只出现在近期证书中的主机名可能是新建的。

被动不再是被动的边界

这条边界比看上去更容易越过。

  • 打开你发现的某个 URL。 访问结果中的一个活跃主机名,会从你的 IP 向目标发送请求。
  • 带有实时资源的存档页面。 一个被保存下来的页面仍可能尝试从原网站加载图片或脚本。请禁用外部加载,或者直接读取原始响应内容。
  • DNS 查询。 通过解析器发出的查询可能会到达目标的权威域名服务器。而历史性的被动 DNS 数据则不会。
  • 你的查询对数据来源可见。 你所询问的服务会知道你在查看什么。对于敏感的调查工作,要考虑这会暴露什么信息,并阅读其服务条款和速率限制。

把结果当作线索来对待。记录会过时,证书的存在时间可能比主机本身更长,而共享主机环境会让 IP 历史记录产生误导。

如何处理这些结果

对防御者而言,这一结果呈现的是外部世界已经能够看到的内容清单。一个合理的流程如下:

  1. 与你的资产清单进行比对。 一个你无法与所有者对应起来的主机名,本身就是一个发现。
  2. 检查你的命名会暴露什么。 公开日志中像 vpn-old 或 jenkins-test 这样的名称会帮到攻击者。可以考虑在适合的场景下为内部命名使用通配符证书,同时权衡其自身的风险。
  3. 重复执行,并对新出现的内容发出告警。 我们关于持续性外部攻击面监控的指南展示了如何将这一流程转化为一个带有责任人的差异对比机制。
  4. 有计划地转向主动检查。 直接验证你自己的资产。对于组织以外的任何对象,先取得授权。当威胁研究需要查看某个可疑网站向特定国家的真实访客展示了什么内容时,那属于主动工作,有其自身的规则,相关内容在我们关于用于威胁情报的住宅代理的文章中有所介绍。

结论

被动收集能以几乎为零的成本、且不惊动目标的方式,为你提供一个域名的有用概况。从证书日志、RDAP 和存档入手,相互比对,把发现的内容当作线索,并且对工作不再是被动的那一刻保持诚实。先在自己身上运行一遍,因为其他任何人也能这么做。

来源与参考资料

常见问题

什么是被动威胁情报?

从第三方已经掌握的数据中收集有关某个域名或组织的信息,例如证书日志、注册记录、网页存档和被动DNS,而不向目标自身的系统发送任何流量。

被动侦察真的不可见吗?

对目标来说,大多如此。目标的日志不会显示你的查询,但第三方服务会看到,任何能看到你搜索记录的人都能了解你感兴趣的内容。诸如加载你找到的实际URL,或是触及目标域名服务器的DNS查询等操作,就不再严格属于被动行为了。

被动收集是合法的吗?

阅读公开记录通常比主动扫描风险更低,但这并不能解决所有问题。每个数据源都有自己的使用条款,涉及个人的数据可能受隐私法约束,而你如何使用结果仍需获得授权。请与你的法务团队核实,并确保针对其他组织的工作始终保持在约定的范围内。

什么时候需要从被动收集转向主动收集?

当你需要了解当前真实情况时。被动来源的数据可能已有数月甚至数年之久。一旦你列出了候选对象清单,防御方可以直接核查自己的资产,而对第三方采取这一步则需要明确的授权。

准备好开始了吗?

试用 Shifter 住宅代理,205M+ 个 IP,195+ 个国家,低至 $0.10/GB。

立即开始