分析憑證透明度以偵測釣魚攻擊 是做什麼的?
概述
憑證透明度(Certificate Transparency, CT)是一項網際網路安全標準,建立了一個公開、僅能附加的 SSL/TLS 憑證發行日誌。監控 CT 日誌能夠早期發現以下威脅:註冊偽造合法品牌的釣魚網域、未經授權的憑證發行(針對自己擁有的網域),以及基於憑證的攻擊基礎設施。本技能涵蓋透過 crt.sh 查詢 CT 日誌、使用 Certstream 進行即時監控、建立針對可疑憑證的自動警報機制,以及將發現結果整合到威脅情報工作流程中。
使用時機
- 調查需要分析憑證透明度以找出釣魚跡象的安全事件時
- 針對此領域建置偵測規則或威脅狩獵查詢時
- SOC 分析師需要此類分析的結構化流程時
- 驗證安全監控是否涵蓋相關攻擊技術時
先決條件
- Python 3.9 或以上版本,並安裝
requests、certstream、tldextract、Levenshtein函式庫 - 可存取 crt.sh (https://crt.sh/) 以查詢歷史 CT 日誌
- 可使用 Certstream (https://certstream.calidog.io/) 進行即時監控
- 準備要監控的組織網域與品牌關鍵字清單
- 了解 SSL/TLS 憑證結構及發行流程
關鍵概念
憑證透明度日誌
CT 日誌是經過加密保證、公開可審計、僅能附加的 TLS 憑證發行記錄。主要憑證授權單位(CA)(如 Let's Encrypt、DigiCert、Sectigo、Google Trust Services)會將所有已發行的憑證提交到多個 CT 日誌。截至 2025 年,Chrome 和 Safari 要求所有公開信任的憑證都必須經過 CT 記錄。
透過 CT 進行釣魚偵測
攻擊者註冊近似網域並取得免費憑證(通常來自 Let's Encrypt),以讓釣魚網站看起來具有 HTTPS 的可信外觀。CT 監控之所以能夠早期發現這些行為,是因為憑證會比釣魚活動開始前更早出現在日誌中,從而提供主動封鎖的窗口。
crt.sh 資料庫
crt.sh 是由 Sectigo 營運的免費網頁介面與 PostgreSQL 資料庫,用於索引 CT 日誌。它支援萬用字元搜尋(%.example.com)、直接 SQL 查詢以及 JSON API 回應。crt.sh 會追蹤所有主要 CT 日誌中的憑證發行、到期及撤銷狀態。
工作流程
步驟 1:查詢 crt.sh 以取得憑證歷史
import requests
import json
from datetime import datetime
import tldextract
class CTLogMonitor:
CRT_SH_URL = "https://crt.sh"
def __init__(self, monitored_domains, brand_keywords):
self.monitored_domains = monitored_domains
self.brand_keywords = [k.lower() for k in brand_keywords]
def query_crt_sh(self, domain, include_expired=False):
"""查詢 crt.sh 以取得符合網域的憑證。"""
params = {
"q": f"%.{domain}",
"output": "json",
}
if not include_expired:
params["exclude"] = "expired"
resp = requests.get(self.CRT_SH_URL, params=params, timeout=30)
if resp.status_code == 200:
certs = resp.json()
print(f"[+] crt.sh:*.{domain} 共有 {len(certs)} 張憑證")
return certs
return []
def find_suspicious_certs(self, domain):
"""找出可能是釣魚嘗試的憑證。"""
certs = self.query_crt_sh(domain)
suspicious = []
for cert in certs:
common_name = cert.get("common_name", "").lower()
name_value = cert.get("name_value", "").lower()
issuer = cert.get("issuer_name", "")
not_before = cert.get("not_before", "")
not_after = cert.get("not_after", "")
# 檢查是否為完全網域相符(合法憑證)
extracted = tldextract.extract(common_name)
cert_domain = f"{extracted.domain}.{extracted.suffix}"
if cert_domain == domain:
continue # 合法憑證
# 標記可疑模式
flags = []
if domain.replace(".", "") in common_name.replace(".", ""):
flags.append("包含目標網域字串")
if any(kw in common_name for kw in self.brand_keywords):
flags.append("包含品牌關鍵字")
if "let's encrypt" in issuer.lower():
flags.append("免費 CA (Let's Encrypt)")
if flags:
suspicious.append({
"common_name": cert.get("common_name", ""),
"name_value": cert.get("name_value", ""),
"issuer": issuer,
"not_before": not_before,
"not_after": not_after,
"serial": cert.get("serial_number", ""),
"flags": flags,
"crt_sh_id": cert.get("id", ""),
"crt_sh_url": f"https://crt.sh/?id={cert.get('id', '')}",
})
print(f"[+] 發現 {len(suspicious)} 張可疑憑證")
return suspicious
monitor = CTLogMonitor(
monitored_domains=["mycompany.com", "mycompany.org"],
brand_keywords=["mycompany", "mybrand", "myproduct"],
)
suspicious = monitor.find_suspicious_certs("mycompany.com")
for cert in suspicious[:5]:
print(f" [{cert['common_name']}] 標記:{cert['flags']}")
步驟 2:使用 Certstream 進行即時監控
import certstream
import Levenshtein
import re
from datetime import datetime
class CertstreamMonitor:
def __init__(self, watched_domains, brand_keywords, similarity_threshold=0.8):
self.watched_domains = [d.lower() for d in watched_domains]
self.brand_keywords = [k.lower() for k in brand_keywords]
self.threshold = similarity_threshold
self.alerts = []
def start_monitoring(self, max_alerts=100):
"""開始即時 CT 日誌監控。"""
print("[*] 啟動 Certstream 監控...")
print(f" 監控對象:{self.watched_domains}")
print(f" 關鍵字:{self.brand_keywords}")
def callback(message, context):
if message["message_type"] == "certificate_update":
data = message["data"]
leaf = data.get("leaf_cert", {})
all_domains = leaf.get("all_domains", [])
for domain in all_domains:
domain_lower = domain.lower().strip("*.")
if self._is_suspicious(domain_lower):
alert = {
"domain": domain,
"all_domains": all_domains,
"issuer": leaf.get("issuer", {}).get("O", ""),
"fingerprint": leaf.get("fingerprint", ""),
"not_before": leaf.get("not_before", ""),
"detected_at": datetime.now().isoformat(),
"reason": self._get_reason(domain_lower),
}
self.alerts.append(alert)
print(f" [警報] {domain} - {alert['reason']}")
if len(self.alerts) >= max_alerts:
raise KeyboardInterrupt
try:
certstream.listen_for_events(callback, url="wss://certstream.calidog.io/")
except KeyboardInterrupt:
print(f"\n[+] 監控已停止。共收集 {len(self.alerts)} 個警報。")
return self.alerts
def _is_suspicious(self, domain):
"""檢查網域相對於監控對象是否可疑。"""
for watched in self.watched_domains:
# 精確關鍵字比對
watched_base = watched.split(".")[0]
if watched_base in domain and domain != watched:
return True
# Levenshtein 距離(域名搶註偵測)
domain_base = tldextract.extract(domain).domain
similarity = Levenshtein.ratio(watched_base, domain_base)
if similarity >= self.threshold and domain_base != watched_base:
return True
# 品牌關鍵字比對
for keyword in self.brand_keywords:
if keyword in domain:
return True
return False
def _get_reason(self, domain):
"""判斷網域被標記的原因。"""
reasons = []
for watched in self.watched_domains:
watched_base = watched.split(".")[0]
if watched_base in domain:
reasons.append(f"包含 '{watched_base}'")
domain_base = tldextract.extract(domain).domain
similarity = Levenshtein.ratio(watched_base, domain_base)
if similarity >= self.threshold and domain_base != watched_base:
reasons.append(f"與 '{watched}' 相似 ({similarity:.0%})")
for kw in self.brand_keywords:
if kw in domain:
reasons.append(f"品牌關鍵字 '{kw}'")
return "; ".join(reasons) if reasons else "未知"
cs_monitor = CertstreamMonitor(
watched_domains=["mycompany.com"],
brand_keywords=["mycompany", "mybrand"],
similarity_threshold=0.75,
)
alerts = cs_monitor.start_monitoring(max_alerts=50)
步驟 3:從 CT 日誌枚舉子網域
def enumerate_subdomains_ct(domain):
"""從憑證透明度日誌中發現所有子網域。"""
params = {"q": f"%.{domain}", "output": "json"}
resp = requests.get("https://crt.sh", params=params, timeout=30)
if resp.status_code != 200:
return []
certs = resp.json()
subdomains = set()
for cert in certs:
name_value = cert.get("name_value", "")
for name in name_value.split("\n"):
name = name.strip().lower()
if name.endswith(f".{domain}") or name == domain:
name = name.lstrip("*.")
subdomains.add(name)
sorted_subs = sorted(subdomains)
print(f"[+] 針對 {domain} 的 CT 子網域枚舉:共 {len(sorted_subs)} 個子網域")
return sorted_subs
subdomains = enumerate_subdomains_ct("example.com")
for sub in subdomains[:20]:
print(f" {sub}")
步驟 4:產生 CT 情報報告
def generate_ct_report(suspicious_certs, certstream_alerts, domain):
report = f"""# 憑證透明度情報報告
## 目標網域:{domain}
## 產生時間:{datetime.now().isoformat()}
## 摘要
- 發現可疑憑證數量:{len(suspicious_certs)}
- 觸發即時警報數量:{len(certstream_alerts)}
## 可疑憑證(crt.sh)
| 通用名稱 | 發行者 | 標記 | crt.sh 連結 |
|------------|--------|-------|-------------|
"""
for cert in suspicious_certs[:20]:
flags = "; ".join(cert.get("flags", []))
report += (f"| {cert['common_name']} | {cert['issuer'][:30]} "
f"| {flags} | [檢視]({cert['crt_sh_url']}) |\n")
report += f"""
## 即時 Certstream 警報
| 網域 | 發行者 | 原因 | 偵測時間 |
|--------|--------|--------|----------|
"""
for alert in certstream_alerts[:20]:
report += (f"| {alert['domain']} | {alert['issuer']} "
f"| {alert['reason']} | {alert['detected_at'][:19]} |\n")
report += """
## 建議
1. 將被標記的網域加入 DNS 黑洞 / 網頁代理封鎖清單
2. 針對確認的釣魚網域提交下架請求
3. 持續監控 CT 日誌以發現新的憑證註冊
4. 部署 CAA DNS 記錄以限制你網域的憑證發行
5. 部署 DMARC 以防止來自近似網域的電子郵件詐騙
"""
with open(f"ct_report_{domain.replace('.','_')}.md", "w") as f:
f.write(report)
print(f"[+] CT 報告已儲存")
return report
generate_ct_report(suspicious, alerts if 'alerts' in dir() else [], "mycompany.com")
驗證標準
- crt.sh 查詢能成功回傳目標網域的憑證資料
- 能根據近似模式識別出可疑憑證
- Certstream 即時監控能偵測到新的釣魚憑證
- 子網域枚舉能從 CT 日誌中產生完整的清單
- 警報需附帶原因分類
- CT 情報報告包含可執行的建議