脅威インテリジェンスチームが何かをスキャンする前に、他者がすでに保持している記録からドメインについて驚くほど多くのことを学べる。証明書ログにはホスト名が記載され、登録データには日付やネームサーバーが記載され、ウェブアーカイブには過去に公開されていた内容が示される。セキュリティチームにとって、このパッシブな段階は作業の中で最も安価かつ最も静かな部分であり、どんな攻撃者でも見えるものを示している。
このガイドでは、主なパッシブ情報源、小さなテスト済みコレクター、そして「パッシブ」が成立しなくなる境界を扱う。自組織に対して、あるいは許可されたスコープ内で使用すること。
パッシブとは何を意味するか
パッシブな収集とは、リクエストがすでにデータを保持している第三者に送られ、対象のサーバーには決して送られないことを意味する。MITRE ATT&CKは攻撃者側の手法をReconnaissanceの下で説明しており、「自由に利用可能な技術データベース」を検索すること(T1596)が含まれ、DNSとパッシブDNS、WHOIS、デジタル証明書、CDN、スキャンデータベースについてのエントリがある。
MITREはまた、なぜ防御側がこれを気にかけるべきかも説明している。この活動は対象組織の可視性の外で行われる可能性があり、検知が困難であり、予防的な制御で容易に緩和することができないとしている。誰かが公開ログを読むのを止めることはできない。できることは、先に自分で確認し、ログが明らかにする情報を減らすことだ。
確認する価値のある情報源
| 情報源 | 得られるもの | 限界 |
|---|---|---|
| Certificate Transparencyログ | 発行されたTLS証明書上のホスト名 | 名前は示すが、稼働中かどうかは不明。ワイルドカードはサブドメインを隠す |
| RDAP | レジストラ、日付、ネームサーバー | 個人データは通常編集済み |
| ウェブアーカイブ | 古いページ、URL、ホスト | キャプチャされたものだけで、しばしば数年前のもの |
| パッシブDNS | ある名前が解決されていたIPとその時期 | ほとんどが商用で、カバレッジはさまざま |
| インターネットスキャンデータベース | 他者によって確認された開いているポートとバナー | スナップショットであり、古くなっている可能性がある |
Certificate Transparencyは最も豊富な無料情報源だ。RFC 9162は、誰もが認証局を監査できるようにTLS証明書の「存在を公に記録する」プロトコルを説明しており、ログは追記専用だ。つまり、staging.internal-tool.example.comに対する証明書は、そのホストがたとえ1週間しか存続しなかったとしても、可視のまま残り続けるということだ。
RDAPはWHOISに代わる構造化された後継プロトコルだ。ICANNによれば、2025年1月28日以降、ジェネリックトップレベルドメインのレジストリとレジストラはRDAPを提供しなければならず、WHOISを提供する義務はなくなった。そのため、新しいツールはRDAPから始めるべきだ。
OSINTとは何か、その法的境界がどこにあるかに関する我々のガイドでは、人物に関する情報源を含むより広い分野を扱っているが、本ガイドではそれを扱わない。
小さなパッシブコレクター
以下のスクリプトは、ドメインについて3つの第三者サービスに問い合わせるが、ドメイン自体には決して接続しない。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
このコードの2つの詳細はテストから得られたものだ。証明書ログにはホスト名以上のものが含まれている。初期バージョンでは単純な接尾辞チェックを使用しており、スペースを含む証明書ラベルと、なりすまし名m.testexample.comを通過させてしまった。これが、in_scopeがサブドメイン全体を照合する理由だ。もう一つの詳細は、各情報源が異なるものを見るため、一つを信頼するのではなく比較すべきだということだ。古いアーカイブにのみあるホスト名はすでに消えている可能性があり、最近の証明書にのみあるものは新しい可能性がある。
パッシブがパッシブでなくなる境界
その境界は見た目より越えやすい。
- 見つけたURLを開くこと。 結果の中の稼働中のホスト名を訪問すると、対象に自分のIPからのリクエストを送ることになる。
- ライブリソースを含むアーカイブされたページ。 保存されたページが、元のサイトから画像やスクリプトを読み込もうとすることがある。外部読み込みを無効にするか、生のレスポンスを読むこと。
- DNSルックアップ。 リゾルバを通じたクエリは、対象の権威ネームサーバーに届く可能性がある。過去のパッシブDNSデータはそうならない。
- 自分のクエリは情報源に見えている。 問い合わせたサービスは、自分が何を調べているかを知る。機密性の高い調査では、これが何を明らかにするかを考慮し、利用規約とレート制限を確認すること。
結果はリードとして扱うこと。記録は古くなり、証明書はホストより長く存続し、共有ホスティングはIP履歴を誤解させる。
結果をどう扱うか
防御側にとって、結果は外部の世界がすでに見ることができるもののリストだ。賢明な手順は次の通り:
- 自組織の資産インベントリと照合する。 所有者に紐付けられないホスト名こそが発見事項だ。
- 自分たちの名前が何を漏らしているか確認する。 公開ログにある
vpn-oldやjenkins-testのような名前は攻撃者を助ける。適切な場合には、内部命名にワイルドカード証明書を検討すること。ただし、それ自体のリスクも考慮すること。 - 繰り返し実行し、新しいものに警告する。 継続的な外部攻撃対象領域モニタリングに関する我々のガイドでは、これを所有者が設定された差分に変える方法を示している。
- 意図的にアクティブなチェックに移行する。 自分の資産は直接検証すること。自組織外の何かについては、まず許可を得ること。脅威リサーチが、特定の国にいる実際の訪問者に不審なサイトが何を表示しているかを見る必要がある場合、それは独自のルールを持つアクティブな作業であり、脅威インテリジェンスのためのレジデンシャルプロキシに関する我々の記事で扱っている。
結論
パッシブな収集は、ほぼコストをかけずに、かつ対象に気づかれることなく、ドメインについて有用な全体像を与えてくれる。証明書ログ、RDAP、アーカイブから始め、それらを比較し、見つけたものをリードとして扱い、自分の作業がパッシブでなくなる瞬間について正直であること。まず自分自身に対して実行すること。なぜなら、他の誰でもそれができるからだ。
情報源と参考文献
- MITRE ATT&CK, Reconnaissance (TA0043): attack.mitre.org
- MITRE ATT&CK, Search Open Technical Databases (T1596): attack.mitre.org
- RFC 9162, Certificate Transparency Version 2.0: rfc-editor.org
- Certificate Transparency, How CT works: certificate.transparency.dev
- RFC 9083, JSON Responses for RDAP: rfc-editor.org
- ICANN, Registration Data Access Protocol: icann.org
- Internet Archive, Wayback CDX Server API: github.com/internetarchive/wayback