SNI 是 TLS 协议里一个很基础的机制,但在攻防里它的意义很特殊------因为它是明文的。
一、SNI 是什么
SNI(Server Name Indication,服务器名称指示) 是 TLS 握手的扩展字段。客户端在建立 HTTPS 连接时,会在 ClientHello 里明文 告诉服务器「我要访问哪个域名」,这样一台服务器(同一个 IP)才能返回对应的证书,实现一个 IP 托管多个 HTTPS 网站。
关键点:SNI 在加密建立之前发送,是明文的(除非用 ECH)。这正是它在攻防中重要的原因。
ClientHello
└── SNI: www.example.com ← 明文可见
二、攻击方视角的应用场景
| 场景 | 说明 |
|---|---|
| 流量识别与阻断 | 中间设备/防火墙读取 SNI 明文,判断目标域名并封禁。这是很多企业网关、运营商做域名过滤的常用手段 |
| 资产测绘 | 用 SNI 反查:给一个 IP,发不同 SNI,观察返回的证书。可以发现同一 IP 上隐藏的其他站点,用于资产梳理/攻击面发现 |
| 绕过 IP 封禁 | 目标封了 IP 但没做 SNI 校验时,用域名+SNI 指向被封 IP 仍可访问 |
| 域名前置(Domain Fronting) | 用合法大站域名做 SNI,实际流量转发到目标,隐藏真实通信目标,常用于绕过审查/C2 隐蔽通信 |
| CDN 回源与真实 IP 探测 | 结合 SNI 与证书信息,判断站点是否挂在 CDN 后面,寻找回源 IP |
| 证书信息搜集 | SNI 对应的证书里常含域名、组织名,用于情报收集 |
三、防守方视角的应用场景
| 场景 | 说明 |
|---|---|
| 流量审计/出入站控制 | 基于 SNI 做域名白名单/黑名单,管住内网出站访问 |
| 恶意 TLS 流量检测 | 检测异常 SNI(如 SNI 与证书不匹配、域名为 DGA 生成、SNI 为空或乱码) |
| CDN/负载均衡 | 依据 SNI 分流到不同后端(这是正常业务用法) |
| 威胁情报匹配 | 把 SNI 与情报库比对,识别 C2、钓鱼域名 |
四、值得注意的技术演进
| 技术 | 作用 | 对攻防的影响 |
|---|---|---|
| ESNI(已废弃) | 早期尝试加密 SNI | 被 ECH 取代 |
| ECH(Encrypted Client Hello) | 加密整个 ClientHello,包括 SNI | 让基于 SNI 的识别/阻断失效,是防守方检测能力的新挑战 |
| SNI 为空/伪装 | 客户端不发或发假 SNI | 常用于绕过 SNI 过滤,但会与证书不匹配,容易被识别 |
一句话总结
SNI 是 TLS 握手里明文的「目标域名标签」:
- 攻击方用它做资产测绘、流量混淆、绕封禁(域名前置最典型);
- 防守方用它做出站管控、流量审计、恶意域名识别;
- ECH 的出现正在让「靠 SNI 看流量」这套打法逐渐失效。
检测异常 SNI 有什么特征,如何在zeek、wirshark或者SOC 态势产品上发现或者验证而已SNI流量
这是个偏防守侧的好问题。我按「异常特征 → 各平台落地方法 → 验证思路」三层来讲,尽量给可操作的规则而不是泛泛而谈。
一、异常 SNI 的特征清单
1. 协议层异常(最容易落地检测)
| 特征 | 说明 |
|---|---|
| SNI 与证书 CN/SAN 不匹配 | 最经典的异常信号。请求 a.com 但服务器返回 b.com 的证书 |
| SNI 缺失(空) | 现代浏览器几乎必发 SNI;空 SNI 常见于恶意工具、扫描器、老旧/定制客户端 |
| SNI 是 IP 字面量 | 正常客户端很少把 IP 当 SNI |
| SNI 与 HTTP Host 不一致 | 同一连接里 TLS 层和 HTTP 层目标不一致 → 疑似隧道/代理 |
| SNI 后跟不同的 SNI(同连接) | 罕见,可能异常协商 |
2. 内容层异常
| 特征 | 说明 |
|---|---|
| 域名非标准格式 | 纯数字、随机字符串、超长域名、多级随机子域(DGA 特征) |
| 使用免费/可疑 TLD | .tk .ml .ga .cf .xyz .top 等高频出现在恶意外联 |
| 域名刚注册 | 结合 whois/情报:注册时间 < 30 天 |
| 命中威胁情报 | SNI 落在 C2、钓鱼、矿池等情报库中 |
| 高频轮换 | 同一源 IP 短时间内访问大量不同 SNI(扫描/DGA 行为) |
| 时序/长度异常 | 心跳式周期连接、固定大小包(C2 beacon 特征) |
3. 行为层异常
- 单主机 SNI 数量异常多;
- 非工作时间的固定周期外联;
- 内网主机访问与业务无关的外部域名;
- 同一 SNI 分辨率下的证书频繁变化。
二、Zeek 上怎么发现
Zeek 自带 ssl.log,里面有 server_name(SNI)、subject、issuer、validation_status 等字段,非常适合做检测。
关键字段:
server_name--- SNIsubject/issuer--- 证书主体/签发者validation_status--- 含certificate-unknown-authority、self signed certificate等
实用检测思路:
zeek
# 1. 无 SNI 的 TLS 连接(可疑客户端)
event ssl_client_hello(c: connection, version: count, record_version: count,
possible_ts: time, client_random: string,
session_id: string, ciphers: index_vec,
comp_methods: index_vec) {
if ( ! c$ssl?$server_name || c$ssl$server_name == "" )
print fmt("[No-SNI] %s -> %s", c$id$orig_h, c$id$resp_h);
}
更实用的做法是用 Zeek 输出日志 + 集中分析:
- 把
ssl.log喂给 SIEM,做字段关联与情报匹配; - 用
conn.log关联ssl.log,看「同 SNI 的证书是否稳定」。
验证方式 :zeek -r capture.pcap 重放流量,直接看 ssl.log 里的 server_name / validation_status。
三、Wireshark 上怎么发现
显示过滤器(快速定位)
# 只看含 SNI 的 ClientHello
tls.handshake.extensions_server_name
# 按具体 SNI 过滤
tls.handshake.extensions_server_name == "example.com"
# 无 SNI 的 ClientHello
tls.handshake.type == 1 && !tls.handshake.extensions_server_name
# 找证书里的 CN(配合 SNI 对比)
tls.handshake.type == 11
排查手法
tls.handshake.type == 1抓 ClientHello → 展开 Extension: server_name 看明文 SNI;- 再找同连接的 Certificate 消息 → 看 CN/SAN;
- 两者不一致,即为强异常信号。
小技巧:
Statistics → Conversations看 TLS 会话,再逐条跟到 SNI。
四、SOC / 态势感知怎么落地
| 能力 | 实现方式 |
|---|---|
| 日志接入 | 采集 Zeek ssl.log、防火墙/代理日志、DNS 日志 |
| 关联分析 | SNI ↔ DNS 查询 ↔ 证书 ↔ 源主机 多源关联(能大幅降误报) |
| 情报匹配 | SNI 与威胁情报(C2/钓鱼/矿池域名)实时比对 |
| 基线建模 | 建立「主机---SNI」正常基线,检测从未出现过的新 SNI 、异常时段外联 |
| 规则告警 | 无 SNI、SNI/证书不匹配、DGA 域名、可疑 TLD、高频轮换 |
| 可视化 | SNI 频次 TOP、新出现域名趋势、主机外联拓扑 |
推荐的降误报组合(单看 SNI 误报很高,要叠加):
异常SNI AND (新域名 OR 命中情报 OR 无SNI OR 证书不匹配) AND 内网主机发起
五、验证用的实验方法(授权前提下)
要验证检测规则是否有效,可以在自己环境里做:
- 构造异常流量 :
- 用
curl --resolve example.com:443:1.2.3.4 https://example.com/验证「SNI 与证书不匹配」; - 用
openssl s_client -servername <SNI> -connect <ip>:443指定任意 SNI,观察返回证书; - 用
curl --no-alpn或自定义客户端构造空 SNI。
- 用
- 抓包验证:Wireshark 确认 SNI 字段值,标记 pcap 作为测试样本。
- 回放检测 :
zeek -r test.pcap,确认ssl.log抓出预期字段;再喂 SIEM 看规则是否触发。 - 误报评估:用正常业务流量做基线,测量误报率后再调阈值。
⚠️ 边界提醒:以上验证方法只应在你自有或明确授权的环境中进行。「指定任意 SNI 探测某 IP 返回证书」这类操作若指向未授权目标,属于越界探测。
六、必须知道的现实局限
ECH(Encrypted Client Hello)正在让 SNI 检测逐渐失效------启用 ECH 后 SNI 被加密,网关只能看到外层 SNI(通常是 CDN 域名)。
应对方向:
- 从 SNI 检测 转向证书/指纹/行为检测(JA3/JA4 指纹、连接时序、流量大小分布);
- 结合 DNS 层(DoH/DoT 会让 DNS 也难见,需终端侧遥测);
- 加强**端点侧(EDR)**的可见性,弥补网络层盲区。
一句话总结
异常 SNI 的核心信号 是「无 SNI、SNI 与证书不匹配、域名字符串可疑、行为高频轮换」;Zeek 看 ssl.log、Wireshark 用 tls.handshake.extensions_server_name 过滤器 即可快速发现,SOC 上要叠加情报与基线才能把误报压下来 ;而 ECH 正让这条检测路径失效,需要转向指纹与端点侧遥测。
异常 SNI 检测:Zeek 脚本 + Wireshark 过滤器清单
用途:在自有或已授权 环境中,检测 TLS 握手阶段的异常 SNI 行为
适用:Zeek 5.x / 6.x;Wireshark 3.6+
⚠️ 仅限自有系统或明确授权的安全测试场景使用
一、Zeek 脚本
1.1 部署方式
bash
# 放到 Zeek 本地站点目录
sudo cp snicheck.zeek /usr/local/zeek/share/zeek/site/
在 local.zeek 中加载:
zeek
# /usr/local/zeek/share/zeek/site/local.zeek
@load ./snicheck
# 务必定义本地网段,否则脚本不检测内网流量
redef Site::local_nets += { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 };
或命令行直接跑:
bash
zeek -r capture.pcap site/snicheck.zeek Site::local_nets=10.0.0.0/8
1.2 主脚本 snicheck.zeek
zeek
##! 异常 SNI 检测脚本
##! 检测:无 SNI、可疑 TLD、DGA 特征、SNI/证书不匹配、高频轮换
##! 依赖:本机所抓 TLS 流量 + Site::local_nets 定义
module SNICheck;
export {
redef enum Notice::Type += {
No_SNI,
SNI_Cert_Mismatch,
Suspicious_TLD,
DGA_Like_SNI,
SNI_High_Frequency,
};
## ---- 可调参数 ----
# 可疑/高频滥用 TLD
const suspicious_tlds: set[string] = {
"tk", "ml", "ga", "cf", "gq", "xyz", "top", "buzz",
"click", "work", "loan", "review", "country", "stream"
} &redef;
# 同一源主机在窗口内访问的不同 SNI 数阈值(扫描/轮换告警)
const sni_freq_threshold: count = 50 &redef;
const sni_freq_window: interval = 5min &redef;
# 无 SNI 白名单(内部服务、已知合法客户端)
const no_sni_whitelist: set[subnet] = {} &redef;
}
# ---------- 辅助:DGA 特征判断 ----------
function dga_like(name: string): bool
{
local parts = split_string(name, /\./);
if ( |parts| < 2 )
return F;
local label = parts[|parts| - 2]; # 二级域名标签
# 条件1:标签过长
if ( |label| >= 15 )
return T;
# 条件2:数字占比 >= 40%
local digits = 0;
local letters = 0;
local i = 0;
local ch = "";
for ( i in label )
{
ch = label[i];
if ( ch in "0123456789" )
digits += 1;
else if ( ch in "abcdefghijklmnopqrstuvwxyz" )
letters += 1;
}
if ( digits + letters == 0 )
return F;
if ( digits * 100 / (digits + letters) >= 40 )
return T;
# 条件3:连续 5 个及以上辅音(随机串特征)
local cons = 0;
local vowels = "aeiou";
for ( i in label )
{
ch = label[i];
if ( ch !in vowels && ch !in "0123456789" )
{
cons += 1;
if ( cons >= 5 )
return T;
}
else
cons = 0;
}
return F;
}
# ---------- 状态表:统计每源主机的 SNI 频次 ----------
global sni_tracker: table[addr] of set[string] &create_expire=sni_freq_window;
# ---------- 检测 1:无 SNI ----------
# 说明:ssl_client_hello 的签名在 Zeek 各版本间略有差异,
# 这里使用最通用的 (c, version, record_version) 前三个参数写法。
event ssl_client_hello(c: connection, version: count, record_version: count)
{
local orig = c$id$orig_h;
local resp = c$id$resp_h;
# 仅关注内网发起的连接
if ( orig !in Site::local_nets )
return;
if ( orig in no_sni_whitelist )
return;
if ( ! c$ssl?$server_name || c$ssl$server_name == "" )
{
NOTICE([$note=No_SNI,
$conn=c,
$msg=fmt("TLS connection without SNI: %s -> %s:%s", orig, resp, c$id$resp_p),
$identifier=cat(orig, resp)]);
}
}
# ---------- 检测 2/3/4:可疑 TLD、DGA、SNI/证书不匹配 ----------
event ssl_established(c: connection)
{
if ( ! c$ssl?$server_name || c$ssl$server_name == "" )
return;
local sni = c$ssl$server_name;
local sni_lower = to_lower(sni);
# 2a. 可疑 TLD
local s_parts = split_string(sni_lower, /\./);
local tld = "";
if ( |s_parts| >= 2 )
{
tld = s_parts[|s_parts| - 1];
if ( tld in suspicious_tlds )
NOTICE([$note=Suspicious_TLD,
$conn=c,
$msg=fmt("Suspicious TLD: SNI=%s", sni),
$identifier=cat(c$id$orig_h, sni)]);
}
# 2b. DGA 特征
if ( dga_like(sni_lower) )
NOTICE([$note=DGA_Like_SNI,
$conn=c,
$msg=fmt("DGA-like SNI: %s", sni),
$identifier=cat(c$id$orig_h, sni)]);
# 2c. SNI 与证书主体不匹配
if ( c$ssl?$certificate && c$ssl$certificate$subject != "" &&
sni_lower !in to_lower(c$ssl$certificate$subject) )
{
NOTICE([$note=SNI_Cert_Mismatch,
$conn=c,
$msg=fmt("SNI/cert mismatch: SNI=%s cert=%s",
sni, c$ssl$certificate$subject),
$identifier=cat(c$id$orig_h, sni)]);
}
}
# ---------- 检测 5:高频 SNI(扫描 / 轮换) ----------
event ssl_established(c: connection)
{
if ( ! c$ssl?$server_name || c$ssl$server_name == "" )
return;
local orig = c$id$orig_h;
if ( orig !in Site::local_nets )
return;
if ( orig !in sni_tracker )
sni_tracker[orig] = set();
add sni_tracker[orig][c$ssl$server_name];
if ( |sni_tracker[orig]| == sni_freq_threshold )
{
NOTICE([$note=SNI_High_Frequency,
$conn=c,
$msg=fmt("Source %s hit %d distinct SNIs within %s (possible scan/rotation)",
orig, sni_freq_threshold, sni_freq_window),
$identifier=cat(orig)]);
}
}
1.3 部署后验证
bash
# 1) 语法检查(-a 表示仅解析不执行)
zeek -a site/snicheck.zeek
# 2) 回放 pcap(记得指定本地网段)
zeek -r test.pcap site/snicheck.zeek Site::local_nets=10.0.0.0/8
# 3) 查看告警
cat notice.log | zeek-cut note msg id.orig_h id.resp_h
# 4) 查看 SNI 明细(ssl.log 自带)
cat ssl.log | zeek-cut id.orig_h id.resp_h server_name validation_status
1.4 脚本要点说明(部署前必读)
| 要点 | 说明 |
|---|---|
必须定义 Site::local_nets |
否则 orig !in Site::local_nets 恒真,脚本不会报任何告警 |
ssl_client_hello 签名 |
各 Zeek 版本参数不同,脚本只取前三个参数(Zeek 支持按前缀匹配 event 签名)。若你的版本编译报参数错误,改为 event ssl_client_hello(c: connection, version: count, record_version: count, possible_ts: time, client_random: string, session_id: string, ciphers: index_vec, comp_methods: index_vec) 补齐 |
c$ssl?$certificate |
证书字段并非所有连接都有,必须先判断存在性再访问,脚本已处理 |
&create_expire |
5 分钟窗口自动清理,避免状态表无限增长 |
两个 ssl_established |
Zeek 允许多个同名 event handler,会依次执行;如需合并可自行整理为一个 |
二、Wireshark 过滤器清单
2.1 基础定位(含 SNI 的握手)
# 只显示含 SNI 的 ClientHello
tls.handshake.type == 1 && tls.handshake.extensions_server_name
# 所有 ClientHello(含无 SNI 的)
tls.handshake.type == 1
# 按具体 SNI 过滤
tls.handshake.extensions_server_name == "example.com"
tls.handshake.extensions_server_name contains "example"
# 无 SNI 的 ClientHello(可疑客户端/扫描器)
tls.handshake.type == 1 && !tls.handshake.extensions_server_name
2.2 证书与 SNI 对比排查
# 显示证书消息(看 CN/SAN)
tls.handshake.type == 11
# 证书里包含某个域名(与 SNI 对比)
tls.handshake.certificate contains "example.com"
# 只看 ServerHello + Certificate(快速看服务端返回)
tls.handshake.type == 2 || tls.handshake.type == 11
2.3 按主机 / IP 过滤
# 某 IP 的所有 TLS
ip.addr == 10.0.0.5 && tls
# 某内网主机发起的 ClientHello
ip.src == 10.0.0.5 && tls.handshake.type == 1
# 排除某个已知合法的 SNI
tls.handshake.type == 1 && tls.handshake.extensions_server_name != "internal.corp.com"
2.4 结合时间 / 流
# 指定时间段的 ClientHello
frame.time >= "2026-09-10 09:00:00" && frame.time <= "2026-09-10 18:00:00" && tls.handshake.type == 1
# 按 TCP 流排查
tcp.stream == 12 && tls
2.5 快捷排查动作
| 目的 | 操作 |
|---|---|
| 看 SNI 明文 | 抓 ClientHello → 展开 Extension: server_name |
| 看证书 CN/SAN | 同连接 Certificate 消息 → 展开 signedCertificate |
| SNI 与证书比对 | 两条消息对照,域名不一致即异常 |
| 找无 SNI 连接 | 用 2.1 最后一条过滤器 |
| 批量看会话 | Statistics → Conversations → TCP |
2.6 导出可疑样本供 Zeek 回放
# 只保留无 SNI 的流,导出为 no_sni.pcap
tls.handshake.type == 1 && !tls.handshake.extensions_server_name
# File → Export Specified Packets
三、验证流程(仅限授权环境)
① 构造测试流量
- 无 SNI: 用可关闭 SNI 的客户端/自写 TLS 客户端发起连接
- SNI 不匹配: curl --resolve example.com:443:<目标IP> https://example.com/
- 自定义 SNI: openssl s_client -servername <任意SNI> -connect <ip>:443
② 抓包确认
- Wireshark 用 2.1 / 2.2 过滤器确认字段值
③ 检测回放
- zeek -r test.pcap site/snicheck.zeek Site::local_nets=10.0.0.0/8
- 检查 notice.log 是否产生对应告警
④ 误报评估
- 用正常业务 pcap 回放,统计误报
- 调整 sni_freq_threshold、suspicious_tlds、no_sni_whitelist 后复测
四、参数调优建议
| 参数 | 默认 | 调整建议 |
|---|---|---|
sni_freq_threshold |
50 / 5min | 办公网调高至 100+;服务器区可调低 |
suspicious_tlds |
常见滥用 TLD | 按贵司业务裁剪,避免误伤 |
no_sni_whitelist |
空 | 加入内网服务网段、已知无 SNI 的合法客户端 |
| SNI/证书不匹配 | 全量告警 | CDN 场景误报高,建议加白名单域 |
五、局限与演进(务必知悉)
- ECH(Encrypted Client Hello) 生效后 SNI 被加密,本套检测基本失效------需转向 JA3/JA4 指纹、连接时序、流量分布、端点遥测(EDR)。
- DoH/DoT 会让 DNS 侧关联变难,需终端侧数据补充。
- CDN 场景下 SNI 与证书不匹配属正常(证书由 CDN 签发),需白名单降误报。
- 单看 SNI 误报较高,务必叠加情报、基线、行为多维关联再告警。
- 本脚本未做 pcap 实测验证 (编写环境无 Zeek 运行时),首次部署请在测试环境用
zeek -a校验语法并回放样本确认。