Zeek ssl.log와 x509.log¶
한 줄 요약
암호화된 트래픽은 읽을 수 없지만 핸드셰이크는 평문입니다. ssl.log는 클라이언트가 요청한 서버 이름(SNI), TLS 버전과 암호 스위트, 클라이언트 소프트웨어의 JA3/JA4 지문, 그리고 x509.log의 인증서 체인을 가리키는 포인터를 줍니다 — 자체 서명되거나 기본값인 인증서로 C2를 식별하고, 지문으로 도구를 찾고, 실제 웹 브라우징과 스크립트 트래픽을 구분하기에 충분합니다.
ssl.log 필드¶
| 필드 | 의미 | 분석 메모 |
|---|---|---|
ts, uid, id.* |
conn.log와 같음 |
|
version |
TLSv10, TLSv12, TLSv13, SSLv3, DTLSv12 |
TLS 1.3은 서버 인증서를 암호화 → 해당 연결은 x509.log가 비어 있을 수 있음 |
cipher |
협상된 암호 스위트 | 최신 호스트에서 아주 오래된/약한 스위트 = 오래된 도구 |
curve |
키 교환 곡선 | |
server_name |
SNI — 클라이언트가 요청한 호스트명 | "어디에 연결했나" 필드. SNI가 비어 있으면 = IP로 직접 연결 또는 오래된 도구 |
resumed |
세션 재개 사용 여부 | 비콘은 재개하는 경우가 많음; 한 호스트로 전체 핸드셰이크가 많은 것도 이상 |
last_alert |
TLS 경고 (있다면) | handshake_failure, unknown_ca… |
next_protocol |
ALPN (h2, http/1.1) |
"브라우저"인데 ALPN이 없으면 = 브라우저가 아님 |
established |
핸드셰이크 완료 여부 | 시도가 많은데 F = 탐색 / 실패하는 C2 |
ssl_history (Zeek 5+) |
핸드셰이크의 글자 기록 | conn.log history의 TLS 버전 |
cert_chain_fuids / client_cert_chain_fuids |
서버 / 클라이언트 인증서 체인의 파일 ID → x509.log |
첫 번째 요소 = 리프 인증서 |
subject / issuer |
리프 인증서 주체 / 발급자 (보였다면) | 같으면 자체 서명 |
client_subject / client_issuer |
클라이언트 인증서 (상호 TLS) | 드묾 — VPN, 일부 C2 |
validation_status |
ok, self signed certificate, unable to get local issuer certificate, certificate has expired… |
validate-certs 정책 필요 |
ja3 / ja3s (정책) |
클라이언트 / 서버 TLS 지문 (MD5) | 클라이언트 소프트웨어 지문 |
ja4 / ja4s (정책, Zeek 6+) |
더 새롭고 정렬 가능한 지문 | 예: t13d1516h2_8daaf6152771_02713d6af862 |
sni_matches_cert (정책) |
SNI가 인증서의 이름과 일치하나? | F = 도메인 프론팅, 설정 오류, 가짜 인증서 |
x509.log 필드¶
| 필드 | 의미 | 분석 메모 |
|---|---|---|
ts, id / fingerprint |
인증서의 파일 ID (fuid); SHA-1/256 지문 |
ssl.log cert_chain_fuids와 결합 |
certificate.version |
보통 3 | |
certificate.serial |
일련번호 | 도구 기본 인증서는 알려진 일련번호를 가짐 |
certificate.subject |
CN=…,O=…,C=… |
비어 있음/쓰레기/CN=localhost → 수상 |
certificate.issuer |
서명한 주체 | = subject → 자체 서명 |
certificate.not_valid_before / not_valid_after |
유효 기간 | 처음 사용하기 몇 분 전에 만들어진 인증서, 또는 1년이나 100년 유효 → 도구가 생성 |
certificate.key_alg, sig_alg, key_type, key_length, exponent, curve |
키 상세 | 1024비트 RSA / sha1WithRSA = 오래됐거나 대충 만듦 |
san.dns, san.uri, san.email, san.ip |
주체 대체 이름 (SAN) | 실제 사이트는 여러 개를 나열; C2 인증서는 없거나 하나 |
basic_constraints.ca, path_len |
CA 플래그 | ca=T인 리프 인증서는 이상 |
host_cert, client_cert (Zeek 5+) |
인증서가 보인 위치 |
빠른 쿼리¶
# Top SNIs from internal hosts — long tail again
zeek-cut server_name < ssl.log | sort | uniq -c | sort -n | head -40
# TLS to raw IP (no SNI) — legit CDNs rarely, C2 often
zeek-cut -d ts id.orig_h id.resp_h id.resp_p server_name version ja3 < ssl.log | awk '$5=="-"'
# Self-signed / invalid certs
zeek-cut -d ts id.orig_h id.resp_h server_name validation_status subject issuer < ssl.log | grep -v -P '\tok\t'
# Rare JA3 hashes (few clients use them) and where they go
zeek-cut id.orig_h ja3 < ssl.log | sort -u | awk '{print $2}' | sort | uniq -c | sort -n | head
zeek-cut -d ts id.orig_h id.resp_h server_name < ssl.log | grep -F '<ja3 hash>'
# Certificates valid for suspiciously short/long periods, or created just before use
zeek-cut -d ts certificate.subject certificate.issuer certificate.not_valid_before certificate.not_valid_after < x509.log \
| awk -F'\t' '{print}' | sort -u | head
# Certs with no SAN (real sites almost always have one)
zeek-cut fingerprint certificate.subject san.dns < x509.log | awk -F'\t' '$3=="-"' | sort -u
# Pivot: one connection's cert chain
grep -F 'CHhAvVGS1DHFjwGM9' ssl.log | zeek-cut cert_chain_fuids | tr ',' '\n' | while read f; do grep -F "$f" x509.log; done | zeek-cut certificate.subject certificate.issuer certificate.not_valid_before
# Known-bad default certificates
# Cobalt Strike default: CN=Major Cobalt Strike, serial 146473198 ; Metasploit: random CNs but 'O=…' patterns; Sliver: random 'O=' words
grep -iE 'Major Cobalt Strike|146473198' x509.log
index=zeek sourcetype=zeek:ssl
| stats dc(id.orig_h) as clients dc(id.resp_h) as servers values(server_name) as sni count by ja3
| where clients <= 3
| sort - count
모든 TLS 핸드셰이크를 클라이언트 지문 ja3로 묶습니다. values(server_name)은 그 지문과 함께 본 모든 SNI를 (중복 없이) 나열합니다. 3대 이하의 머신이 쓰는 지문이 한두 서버와 수천 번 통신한다면 비콘입니다. 브라우저는 전사에서 공유하는 지문을 만듭니다.
index=zeek sourcetype=zeek:ssl validation_status!="ok" validation_status!="-"
| stats count min(_time) as first max(_time) as last by id.orig_h, id.resp_h, server_name, validation_status, subject
| convert ctime(first) ctime(last)
min(_time)/max(_time)은 각 내부 호스트가 신뢰할 수 없는 인증서에 처음과 마지막으로 접속한 시각을 주고, convert ctime()은 epoch 숫자를 읽을 수 있는 날짜로 바꿉니다. count로 정렬하면 어떤 자체 서명 인증서가 일정한 주기로 방문되는지 보입니다.
분석 팁¶
ssl.log 한 줄로 하는 저렴한 C2 체크리스트
SNI 없음 또는 SNI가 IP · validation_status ≠ ok · subject = issuer · 오늘부터 유효한 인증서 · SAN 없음 · next_protocol 비어 있음 · 이 호스트만 쓰는 JA3 · 맨 VPS IP의 id.resp_p 443 · 일정한 간격으로 반복되는 연결. 세 개 이상 해당 → 반증될 때까지 C2로 취급하세요.
- TLS 1.3은 서버 인증서를 숨깁니다. 1.3에서도 SNI, JA3/JA4, ALPN, 타이밍은 남지만
x509.log에 서버 인증서는 없습니다(복호화 프록시가 아니라면). 암호화된 SNI/ECH는 이름까지 없애므로 — 그러면 JA4, 목적지 IP 평판, 행동만 남습니다. - JA3 충돌: 많은 도구가 지문을 공유합니다(Go TLS 스택, .NET, Python
requests). JA3는 그 자체로 시그니처가 아니라 묶기와 희소 목록에 쓰세요. JA4는 더 잘 구분합니다(t13d…가 버전, ALPN, 암호 개수를 보여 줌). 서버도 지문을 찍으세요(ja3s/ja4s) — C2 프레임워크의 리스너는 모든 임플란트에 똑같이 응답합니다. - 도메인 프론팅: SNI는
a.legit-cdn.com이라고 하는데 안쪽Host(보이지 않음)는 다른 곳입니다. 징후:sni_matches_cert = F, 유난히 규칙적인 트래픽이 있는 CDN SNI, 정해진 주기로 단 한 호스트만 접속하는 CDN 대역의resp_h. - Let's Encrypt는 안전 신호가 아닙니다: 최신 C2(Sliver, Mythic, Havoc, 리다이렉터)는 유효한 LE 인증서를 씁니다. 그럴 땐 행동으로 돌아가세요: 간격, 바이트 크기, 목적지 나이, JA3 희소성.
- 정상적인 자체 서명: 프린터, IoT, 내부 관리 패널, 개발 장비. 먼저
local_resp = T를 걸러 내고 인터넷으로 나가는 나머지를 보세요. - 워크스테이션 아웃바운드 트래픽의 클라이언트 인증서(
client_subject) → VPN, 회사 프록시, 또는 상호 TLS를 쓰는 C2 (드물지만 Sliver mTLS 모드가 있음). - 버전/암호 이상치: 2026년에
TLS_RSA_WITH_RC4_128_SHA로TLSv10을 협상하는 호스트 하나는 아주 오래된 무언가 — 또는 일부러 낮춘 도구를 돌리고 있습니다. - IP 간 인증서 재사용: 같은
x509지문이 시간에 따라 여러id.resp_h에 나타나면 = 행위자가 인프라를 옮기면서 인증서는 유지한 것. 위협 인텔리전스와 과거 헌팅에 훌륭한 피벗입니다.
상관분석¶
| 질문 | 다음에 볼 곳 |
|---|---|
| 얼마나 많이, 얼마나 자주 오갔나? | conn.log uid → 바이트, 지속 시간; 간격 분석 (비코닝과 C2) |
| 이름은 어디서 해석됐나? | 직전의 query = server_name인 dns.log |
| 어떤 프로세스? | 호스트의 Sysmon 3 (DestinationPort 443); EDR 네트워크 텔레메트리 |
| 인증서가 다른 곳에서도 쓰였나? | 시간대별 x509.log fingerprint; 같은 지문이나 일련번호를 Censys / Shodan / crt.sh에서 |
| 경보? | Suricata tls.sni, JA3 규칙 (ja3.hash), notice.log SSL::Invalid_Server_Cert |
| 페이로드 | 복호화 없이는 볼 수 없음 — 호스트 아티팩트로 피벗 (Prefetch, SRUM, 메모리) |