使用 DNSTwist 分析域名搶註 是做什麼的?
概述
DNSTwist 是一個域名排列引擎,能產生外觀相似的域名,用於偵測域名搶註(typosquatting)、同形異義字釣魚攻擊及品牌仿冒。它透過字元替換、換位、插入、刪除、同形異義字取代等技術生成數千個域名排列,接著檢查 DNS 記錄(A、AAAA、NS、MX),並使用模糊哈希(ssdeep)與感知哈希(pHash)計算網頁相似度,識別出可能已註冊的惡意域名。
使用時機
- 需要分析域名搶註的安全事件調查時
- 為此類網域建立偵測規則或威脅狩獵查詢時
- SOC 分析師需要此分析類型的結構化程序時
- 驗證相關攻擊技術的安全監控覆蓋範圍時
前置需求
- Python 3.9+ 且已安裝
dnstwist(pip install dnstwist[full]) - 選用:用於 IP 地理位置的 GeoIP 資料庫
- 選用:用於資料增強的 Shodan API 金鑰
- 具備執行 DNS 查詢的網路存取權限
- 了解 DNS 記錄類型與域名註冊
關鍵概念
域名排列技術
DNSTwist 透過以下方式生成排列:追加(附加字元)、位元翻轉(位元錯誤)、同形異義字(視覺相似 Unicode 字元,如 rn 與 m)、連字符(加入連字號)、插入(插入字元)、刪除(移除字元)、重複(重複字元)、替換(用鄰近鍵盤按鍵取代)、子網域(插入點號)、換位(交換相鄰字元)、母音交換(交換母音),以及字典式(附加常見單字)。
模糊哈希與視覺相似度
DNSTwist 使用 ssdeep(局部敏感哈希)比較 HTML 內容,並以 pHash(感知哈希)比對網頁截圖。這有助於識別視覺上模仿合法網站的克隆釣魚頁面。高相似度分數代表極可能為釣魚頁面。
偵測工作流程
典型工作流程為:生成域名排列 -> 解析 DNS 記錄 -> 檢查已註冊域名 -> 比較網頁相似度 -> 標記可疑域名 -> 通知安全團隊 -> 要求下架。對於一般企業網域,dnstwist 約會生成 5,000 至 10,000 個排列。
工作流程
步驟 1:基本域名排列掃描
import subprocess
import json
import csv
from datetime import datetime
def run_dnstwist_scan(domain, output_file=None):
"""對目標網域執行 dnstwist 掃描。"""
cmd = [
"dnstwist",
"--registered", # 僅顯示已註冊網域
"--format", "json", # 輸出為 JSON 格式
"--nameservers", "8.8.8.8,1.1.1.1",
"--threads", "50",
"--mxcheck", # 檢查 MX 記錄
"--ssdeep", # 模糊哈希比較
"--geoip", # GeoIP 查詢
domain,
]
print(f"[*] 正在掃描 {domain} 的排列域名")
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
if result.returncode == 0:
results = json.loads(result.stdout)
registered = [r for r in results if r.get("dns_a") or r.get("dns_aaaa")]
print(f"[+] 發現 {len(registered)} 個已註冊的相似域名")
if output_file:
with open(output_file, "w") as f:
json.dump(registered, f, indent=2)
print(f"[+] 結果已儲存至 {output_file}")
return registered
else:
print(f"[-] dnstwist 錯誤:{result.stderr}")
return []
results = run_dnstwist_scan("example.com", "typosquat_results.json")
步驟 2:分析與排序結果
def analyze_results(results, legitimate_ips=None):
"""分析 dnstwist 結果並排序威脅。"""
legitimate_ips = legitimate_ips or set()
high_risk = []
medium_risk = []
low_risk = []
for entry in results:
domain = entry.get("domain", "")
fuzzer = entry.get("fuzzer", "")
dns_a = entry.get("dns_a", [])
dns_mx = entry.get("dns_mx", [])
ssdeep_score = entry.get("ssdeep_score", 0)
risk_score = 0
risk_factors = []
# 與合法網站高度相似
if ssdeep_score and ssdeep_score > 50:
risk_score += 40
risk_factors.append(f"網頁高度相似 ({ssdeep_score}%)")
# 設有 MX 記錄(可收發郵件/釣魚)
if dns_mx:
risk_score += 20
risk_factors.append("有 MX 記錄(具備郵件功能)")
# 近期註冊(若有 whois 資料)
whois_created = entry.get("whois_created", "")
if whois_created:
try:
created = datetime.fromisoformat(whois_created.replace("Z", "+00:00"))
age_days = (datetime.now(created.tzinfo) - created).days
if age_days < 30:
risk_score += 30
risk_factors.append(f"近期註冊 ({age_days} 天前)")
elif age_days < 90:
risk_score += 15
risk_factors.append(f"註冊於 {age_days} 天前")
except (ValueError, TypeError):
pass
# 同形異義字攻擊風險最高
if fuzzer == "homoglyph":
risk_score += 25
risk_factors.append("同形異義字(視覺上完全相同)")
elif fuzzer in ("addition", "replacement", "transposition"):
risk_score += 10
risk_factors.append(f"排列類型:{fuzzer}")
# 未指向合法基礎設施
if dns_a and not set(dns_a).intersection(legitimate_ips):
risk_score += 10
risk_factors.append("與合法網站的 IP 不同")
entry["risk_score"] = risk_score
entry["risk_factors"] = risk_factors
if risk_score >= 50:
high_risk.append(entry)
elif risk_score >= 25:
medium_risk.append(entry)
else:
low_risk.append(entry)
high_risk.sort(key=lambda x: x["risk_score"], reverse=True)
medium_risk.sort(key=lambda x: x["risk_score"], reverse=True)
print(f"\n=== 域名搶註分析 ===")
print(f"高風險:{len(high_risk)}")
print(f"中風險:{len(medium_risk)}")
print(f"低風險:{len(low_risk)}")
if high_risk:
print(f"\n--- 高風險域名 ---")
for entry in high_risk[:10]:
print(f" {entry['domain']}(分數:{entry['risk_score']})")
for factor in entry['risk_factors']:
print(f" - {factor}")
return {"high": high_risk, "medium": medium_risk, "low": low_risk}
analysis = analyze_results(results, legitimate_ips={"93.184.216.34"})
步驟 3:持續監控管道
import time
import hashlib
class TyposquatMonitor:
def __init__(self, domains, known_domains_file="known_typosquats.json"):
self.domains = domains
self.known_file = known_domains_file
self.known_domains = self._load_known()
def _load_known(self):
try:
with open(self.known_file, "r") as f:
return json.load(f)
except FileNotFoundError:
return {}
def _save_known(self):
with open(self.known_file, "w") as f:
json.dump(self.known_domains, f, indent=2)
def scan_all_domains(self):
"""掃描所有受監控網域,尋找新的搶註域名。"""
new_findings = []
for domain in self.domains:
results = run_dnstwist_scan(domain)
for entry in results:
domain_key = entry.get("domain", "")
if domain_key not in self.known_domains:
entry["first_seen"] = datetime.now().isoformat()
entry["monitored_domain"] = domain
self.known_domains[domain_key] = entry
new_findings.append(entry)
print(f" [NEW] {domain_key} ({entry.get('fuzzer', '')})")
self._save_known()
print(f"\n[+] 新發現的搶註域名數量:{len(new_findings)}")
return new_findings
def generate_alert(self, findings):
"""為新發現的高風險搶註域名產生警示。"""
analysis = analyze_results(findings)
alerts = []
for entry in analysis["high"]:
alerts.append({
"severity": "HIGH",
"domain": entry["domain"],
"target": entry.get("monitored_domain", ""),
"risk_score": entry["risk_score"],
"risk_factors": entry["risk_factors"],
"dns_a": entry.get("dns_a", []),
"dns_mx": entry.get("dns_mx", []),
"timestamp": datetime.now().isoformat(),
})
return alerts
monitor = TyposquatMonitor(["mycompany.com", "mycompany.org"])
new_findings = monitor.scan_all_domains()
alerts = monitor.generate_alert(new_findings)
步驟 4:匯出封鎖清單與下架請求
def export_blocklist(analysis, output_file="blocklist.txt"):
"""將高風險域名匯出為防火牆/代理伺服器的封鎖清單。"""
domains = []
for entry in analysis["high"] + analysis["medium"]:
domain = entry.get("domain", "")
if domain:
domains.append(domain)
with open(output_file, "w") as f:
f.write(f"# 搶註域名封鎖清單,產生於 {datetime.now().isoformat()}\n")
for d in sorted(set(domains)):
f.write(f"{d}\n")
print(f"[+] 封鎖清單已儲存:{len(domains)} 個域名 -> {output_file}")
return domains
def generate_takedown_report(high_risk_domains):
"""產生下架請求報告。"""
report = f"""# 域名下架請求
產生日期:{datetime.now().isoformat()}
## 摘要
共識別出 {len(high_risk_domains)} 個可能為搶註/釣魚的域名。
## 需下架之域名
"""
for entry in high_risk_domains:
report += f"""
### {entry['domain']}
- **排列類型**:{entry.get('fuzzer', '未知')}
- **IP 位址**:{', '.join(entry.get('dns_a', ['無']))}
- **MX 記錄**:{', '.join(entry.get('dns_mx', ['無']))}
- **風險分數**:{entry.get('risk_score', 0)}
- **風險因素**:{';'.join(entry.get('risk_factors', []))}
- **網頁相似度**:{entry.get('ssdeep_score', '無')}%
"""
with open("takedown_report.md", "w") as f:
f.write(report)
print("[+] 下架報告已產生:takedown_report.md")
export_blocklist(analysis)
generate_takedown_report(analysis["high"])
驗證標準
- DNSTwist 能為目標網域生成域名排列
- DNS 解析能識別出已註冊的相似域名
- 網頁相似度評分能偵測到克隆釣魚頁面
- 風險評分能依威脅等級排序域名
- 持續監控能偵測到新註冊的搶註域名
- 能正確產生封鎖清單與下架報告