피싱 탐지를 위한 인증서 투명성 분석은(는) 무엇을 하나요?
개요
인증서 투명성(CT)은 발급된 모든 SSL/TLS 인증서의 공개적이고 추가 전용 로그를 생성하는 인터넷 보안 표준입니다. CT 로그를 모니터링하면 정식 브랜드를 사칭하는 피싱 도메인, 소유 도메인에 대한 무단 인증서 발급, 인증서 기반 공격 인프라를 조기에 탐지할 수 있습니다. 이 스킬은 crt.sh를 통한 CT 로그 질의, Certstream을 사용한 실시간 모니터링, 의심스러운 인증서에 대한 자동 경고 구축, 그리고 위협 인텔리전스 워크플로우로 결과 통합을 다룹니다.
사용 시기
- 보안 사고 조사 시 인증서 투명성 분석이 필요한 경우
- 이 도메인에 대한 탐지 규칙 또는 위협 헌팅 쿼리를 구축할 때
- SOC 분석가가 이 분석 유형에 대한 구조화된 절차가 필요할 때
- 관련 공격 기술에 대한 보안 모니터링 범위를 검증할 때
전제 조건
- Python 3.9+ 및
requests,certstream,tldextract,Levenshtein라이브러리 - 과거 CT 로그 질의를 위한 crt.sh 접근 권한 (https://crt.sh/)
- 실시간 모니터링을 위한 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 응답을 지원합니다. 주요 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 인텔리전스 보고서 생성