

字数 4101,阅读大约需 21 分钟
本文是「邮件钓鱼攻防工程」系列第4篇。这篇解决”访问的是什么”:URL 参数结构本身就是最显眼的特征。Gophish 十年没变的
?id=Ab7Xk2Q在 WAF 面前就是一条完美的正则签名。这篇从 RID 的代码细节讲到盲搜算法的工程实现,再到 404 伪装的细节。
作者: 可惜夜
首发wx Yofune安全研究
0x01 参数名即指纹#
Gophish 的追踪 URL,在当下的攻防格局里,很难度过完整的周期。
很明显的一个原因是:URL 参数结构本身就是指纹。Gophish 十年来的 ?id=Ab7Xk2Q 模式,在 ModSecurity Core Rule Set、FortiWeb、AWS WAF、Cloudflare WAF 里,一条正则就能做到接近零误报的拦截:
SecRule ARGS:id "^[a-zA-Z0-9_-]{7}$" "id:100001,deny"bash这条规则之所以致命,不在它是”已知攻击签名”,而在它天然排除合法业务。没有任何正常网站的 URL 会在 id 参数里塞一个恰好 7 位的随机串。Gophish 的 RID 用 64 字符集([a-zA-Z0-9_-])、7 位、从检测角度看,这本身就是就是个完美的静态特征。
至于防火墙和waf匹配这种”短随机串参数”的规则,对于攻击者而言,是现成的套路。任何开了默认防护的企业网关,拦 Gophish 默认格式的钓鱼链接基本不用额外配置。
Gophish 的架构把钓鱼 Landing Page 和管理后端绑在同一个 HTTP Server 上,这在 OpSec 上几乎是不可能的事情。访问根路由 /,如果红队没配 Landing Page,直接暴露 Gophish 管理端登录页。国内测绘平台(FOFA、ZoomEye、鹰图)早就把 Gophish 管理界面的 HTML 结构、X-Gophish-Contact 响应头、固定 favicon 哈希收进了规则库。
这三个缺陷(固定参数名、管理界面暴露、无 OpSec 设计)本身就是让我对Gophish进行重构的原因之一。
0x02 盲搜 RID:把参数名这个约定干掉#
核心思路很直接:取消参数名的固定约定。发信端和收信端不预先约定任何参数名,收信端靠遍历 + 校验来”盲搜” RID。
2.1 算法#
这是 taigong controllers/phish.go 里 extractRIDFromRequest 的真实逻辑(精简后):
HTTP 请求到达
│
▼
先试标准参数名(RecipientParameter):取值 → 去掉 TransparencySuffix(+)
│ └─ ridExistsInStorage(id)?存在 → 认领,结束
▼
否则:遍历所有 query key(按 key 排序,保证确定性)
├── 对每个 value:normalizeRID → isRIDCandidate(正则)
│ 不匹配 → 跳过
│ 匹配 → 去重(seen map) → ridExistsInStorage(id)
│ 存在 → 加入候选集
│
▼
候选集判定:
├── 恰好 1 个 → 认领为本次 RID
├── > 1 个 → 歧义,直接拒绝(记 warn,返回伪装 404)
└── 0 个 → 走 POST body 兜底;仍无 → 判定扫描探测,返回伪装 404bash关键变化:RID 的识别从”按参数名查”变成”按参数值查 + 库里存在性确认”。参数名可以任意生成,甚至每个目标用不同参数名。
蓝队没法再用 SecRule ARGS:id 拦了,因为参数名成了不可预测的动态量。要检测就得穷举所有参数名、对所有参数值做熵分析,这在多数邮件安全网关上是性能上扛不住的。
2.2 路由层#
Gophish 在 route.go 里注册固定路由:
r.HandleFunc("/landing", landingHandler) // 固定路由 + 固定参数名bashtaigong 用通配路由,路径匹配和参数解析完全解耦,所有非静态路径走同一个 handler,RID 在 handler 内盲搜提取。这样红队能构造完全合法的业务 URL:
-
https://phish-domain.com/salary/detail?token=XyZ123A -
https://phish-domain.com/auth/callback?code=XyZ123A&state=abc -
https://phish-domain.com/notice?session_id=XyZ123A
参数名在发信时从词库随机选:token、sid、auth_state、session_key、code、ticket、nonce、sign。词库可扩展,能针对目标公司的内部命名习惯定制,比如用阿里云系的企业给 ticket,微软系给 session_state。
2.3 工程边界#
盲搜有几个边界要处理,下面举例几个代码实现:
歧义即拒绝。如果一次请求里有多个参数值都通过了正则、且都在库里存在,extractRIDFromRequest 不猜,直接记一条 RID 解析歧义,命中多个候选值,已拒绝请求 然后返回 404。这是比”截断到前 N 个参数”更稳的防注入思路:攻击者塞一堆候选 RID 想撞库,结果是一律拒绝。参数遍历本身的开销因此也被这层拒绝兜住了。
数据库索引是硬要求。r_id 列上必须有 UNIQUE INDEX。SQLite 里 WHERE r_id = ? 没索引就退化成全表扫描,百万级 result 记录下每次盲搜几百毫秒;有索引是 O(log n),微秒级。这是能不能上生产的分水岭。
正则 + 库双校验。isRIDCandidate 卡字符集和长度,但一个恰好 7 位的普通表单值(比如半截密码)统计上会出现,所以数据库存在性确认是正则之后的兜底:正则过但库里不存在,照样不算 RID。
盲搜消掉了参数名特征,但留了个新口子:参数值暴力枚举。构造大量含候选 RID 的请求,每个都触发一次库查询。有 UNIQUE INDEX 保护不至于拖垮库,但批量枚举本身是流量异常。对付这个,靠的是上一篇文章讲的那套 IP/频率/UA 评分,不是盲搜算法自己。
0x03 参数多态与路径伪装#
3.1 Gophish 的固定路径遗产#
Gophish 在 controllers/route.go 里注册了一批固定钓鱼路由:/(Landing)、/track(打开追踪)、/report(凭证提交)、/robots.txt、/{path}、/static/{filename}。这些路径是预先注册、不可变的。蓝队或 WAF 拦掉 /track、/report 这几条固定路径,数据回传就断了,几条规则的事情。
3.2 把整个路径空间交给盲搜#
taigong 的做法是把几乎所有路径都收进盲搜逻辑,除了少数静态资源(/static/)、追踪像素(pixel.png)、robots.txt、/report、/download 这类必要端点单独注册,其余统一进 phishHandler。
于是 URL 结构的空间一下子撑开了:
-
兼容格式:
/?id=XyZ123A(向后兼容 Gophish) -
token 路径:
/salary/detail?token=XyZ123A -
OAuth 风格:
/auth/callback?code=XyZ123A&state=abc -
RESTful:
/api/v1/notice/XyZ123A(RID 嵌进路径) -
Hash 路由:
/#/verify/XyZ123A(SPA,参数在 fragment)
最后一种对邮件网关的 URL 提取特别有效,大多数 URL 解析器不提取 fragment,而 React/Vue 这类 SPA 在客户端会正常解析 fragment 里的 RID。
3.3 畸形参数#
SANS ISC 的 Xavier Mertens 在 2026 年 2 月的 diary(《Broken Phishing URLs》)里记录过一个正在被大规模使用的参数逃避技术:故意构造不符合 HTTP 规范的畸形参数,比如:
https://phish-domain.com/?dC=handler@domain&*(Dfbash&*(Df 按 HTTP 标准是无效参数名,但所有主流浏览器静默忽略它,页面照常加载。WAF、正则检测引擎、IOC 提取管道解析不出 *(Df 这个 key=value,会把整条 URL 标”异常”然后跳过,这正是攻击者要的。
这招本质上是利用”浏览器容错 vs 检测引擎严格”的不对称。浏览器什么都忍,安全工具什么都拒,中间地带全是文章。taigong 的 r.ParseForm() 也会忽略非法参数(Go 标准库行为),所以畸形参数可以当”诱饵参数”,检测工具盯着这些无效参数,反而忽略了藏在合法参数里的真 RID。
3.4 业务参数噪声#
更进一步,往 URL 里塞一堆合法业务参数,稀释 RID 的统计学特征:
https://phish-domain.com/report/download
?lang=zh-CN
&tz=Asia%2FShanghai
&device=mobile
&version=2.4.1
&_t=1749283200000
&sid=ZqW8kPmbash这条 URL 里真正被解析的只有 sid=ZqW8kPm,其余全是合法业务里高频出现的参数名和值。蓝队做参数熵分析时,单个高熵值(ZqW8kPm 约 6.0 比特/字符)被一堆低熵值(zh-CN、Asia/Shanghai、mobile )稀释,整体统计特征向合法流量靠拢。
0x04 Nginx 404 伪装的细节#
4.1 为什么是 Nginx 404#
自动化扫描工具探测可疑 URL(直接访问根、访问不存在路径、提交无效参数)时,HTTP 响应是暴露身份的关键窗口。
Nginx 在全球 Web 服务器市场长期占三分之一上下(Netcraft Web Server Survey 历年数据),返回一个 Nginx 404 的钓鱼服务器,和大多数合法网站表现一致,不触发特征检测。Gophish 默认的 404 同时暴露两个特征:响应体里的 Gophish 页面结构,以及 Server: Gophish 响应头。
4.2 taigong 的真实实现#
taigong 里返回伪装 404 的函数是 controllers/phish.go 里的 renderUnknownResponse,全文就这么几行:
func (ps *PhishingServer) renderUnknownResponse(w http.ResponseWriter, r *http.Request) {
if redirectURL := ps.currentPhishConfig().UnknownRedirectURL; redirectURL != "" {
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, `<html>
<head><title>404 Not Found</title></head>
<body bgcolor="white">
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>`)
}bash几个要点:
-
它先看
UnknownRedirectURL,配了就 302 跳走,没配才返回 404 体。 -
模板就是标准 Nginx 404,连
<body bgcolor="white">这个老属性都原样保留,别”优化”掉它,任何改动都可能成为指纹。 -
<center>标签、全小写nginx,都是 Nginx 从 0.x 时代沿用至今的特征;Apache 返回Apache,IIS 返回Microsoft-IIS/10.0。
不过呢当前这个实现只复制了响应体,并没有显式设置 Server: nginx 或 Content-Type: text/html 这些响应头。Go 的 net/http 会通过内容嗅探自动判 Content-Type,所以体没问题;但 Server 头是空的,一个真正较真的检测如果比对响应头集合,这里是个可以再补强的点。
还有个 WAF 检测的小门道要知道:有些规则会比对 404 响应体大小是否和 Nginx 标准 404 完全一致。多一个空格、少一个换行都可能触发”近似匹配”。所以这个模板的字节数要保持和真 Nginx 一致,别手贱加注释或缩进。
4.3 Apache 与 IIS 的场景#
不同目标环境可能需要不同的伪服务器模板:
-
Apache 404:
<address>Apache/2.4.41 (Ubuntu) Server at ...</address>、特定的Server头格式、响应体偏大(带额外 CSS) -
IIS 404:
Server: Microsoft-IIS/10.0、含.aspx路径提示、特定 HTML 结构 -
CDN 伪装:加
CF-Ray、CF-Cache-Status这类 Cloudflare 特征头
服务器伪装的原则不是以假乱真,没必要去做假的东西,只需要把真 Nginx 的 404 抓下来当模板,不要自己”改进”它。
0x06 非目标访问的三种策略#
taigong 的 Campaign 配置里,NonTargetAction 字段控制非目标访问怎么处理,代码里就是一个 switch(controllers/phish.go):
switch ptx.NonTargetAction {
case "404":
http.NotFound(w, r)
return
case "redirect":
if ptx.NonTargetURL != "" {
http.Redirect(w, r, ptx.NonTargetURL, http.StatusFound)
return
}
http.NotFound(w, r) // 没配 URL 就退回 404
return
case "show":
// 继续显示正常页面
}bash三种策略:
-
show:对所有访问者显示原始 Landing Page,不分目标和非目标。隐蔽性最低,但 A/B 测试不同 Landing 转化率、内部培训让员工自助访问、低风险目标场景下有用。 -
404:对非目标访问返回 404。注意这里走的是 Go 标准http.NotFound(“404 page not found” 纯文本),和上面renderUnknownResponse的 Nginx 伪装 404 是两条路径。一个用于”平台不匹配”(比如 Windows 访客撞上 macOS 专属 页面),一个用于”没找到 RID”,这两条目前是分开的。 -
redirect:302 跳到合法站点(企业官网、百度首页之类)。实战里 redirect 往往比 404 更好使,看到 302 跳到正常站点,判误报的概率比看到 404 高。
这套非目标判定之外,还有一条更前置的、真正在跑的机制:Cloudflare 边缘注入 自定义加密字符串,后端校验失败就 302 跳 https://www.baidu.com(见 章节1 和 章节2)。这是部署层的第一道非目标处置,比应用层的 NonTargetAction 更早生效。
之前提过一个”梯度响应”:首次扫描 302、第二次 404、第三次 TCP RST 静默丢包。查代码,三级梯度(尤其是 TCP RST 那级)目前没有实现,多级梯度是个合理的演进方向。
0x06:从特征匹配到行为画像#
红队的技术演进倒逼蓝队往更宏观的维度走。参数免杀消灭了静态特征,但留下了统计学和基础设施层的可检维度。
6.1 参数值的统计指纹#
盲搜消掉了固定参数名,但参数值本身留下统计学特征。一个 7 位随机 RID(64 字符集),信息熵:
比特
单位字符熵:
比特字符
对比正常业务参数:
-
session_id=user123→ 约 2.1 比特/字符 -
page=2&limit=10→ 约 1.5 比特/字符 -
token=Ab7Xk2Q→ 约 6.0 比特/字符(接近 64 字符集理论上限)
正确做法是条件组合:
高熵值参数 (>5.5 比特/字符)
AND 域名注册天数 < 90
AND 证书 = Let's Encrypt
AND 域名首次 CT Log 出现 < 7 天前
→ 高优先级告警bash四个条件 AND 下来误报率大降,合法网站通常不会同时满足”高熵参数 + 新注册域名 + 免费证书 + 无 CT 历史”。
6.2 DNS 时序#
钓鱼链接被点之前的第一跳是 DNS。蓝队在内网 DNS 上做异常检测:
| 信号 | 含义 |
|---|---|
| 同一域名短时间内被大量不同内网 IP 请求 | 邮件群发后的集中点击 |
| DNS 查询 TTL=1 | 常见于 Let’s Encrypt 验证配置 |
| 域名泛解析指向 CDN 边缘 | 隐藏源站 IP |
| DNS 查询集中在非工作时间 | 非正常业务模式 |
6.3 追踪像素的反向利用#
Gophish 和 taigong 都用透明追踪像素(1×1 PNG)检测邮件打开。反制条件也很简单:SEG 入站时检测并剥离追踪像素;邮件客户端默认不加载远程图片(Outlook/Thunderbird);即使参数名动态化,追踪像素的路径模式本身仍是潜在特征。
0x07 攻防演进:参数免杀会停在哪#
参数免杀的演进有个清楚的规律:就是说一个特征的消失,必然会带来更多的特征出现,红蓝对抗永无止境。可以把它粗略归纳成几代:
| 代际 | 红队 | 蓝队 |
|---|---|---|
| 早期 | 固定参数名 ?id= | 正则匹配 id 参数 |
| 盲搜 | 盲搜 RID、随机参数名 | 参数值信息熵分析 |
| 路径 | RESTful 路径嵌入 RID | 路径结构 + 域名生命周期组合 |
| SPA | Hash 路由(fragment 里藏 RID) | 流量时序 + DNS 异常 |
| 噪声 | 畸形参数 + 业务噪声注入 | 多维关联(CA+ASN+注册商+行为) |
| 当前 | Aged Domain + AI 生成 URL | 行为基线与基础设施关联异常 |
| 趋势 | WebSocket/SSE 实时推送 | 协议层行为基线偏离 |
当前态势在”噪声”到”当前”之间。传统 WAF 签名在盲搜 RID 面前已经失效,未来要更关注基础设施和流量元数据之间的关联异常,而不是依靠单一的模板指纹进行规则命中。
6.4 AI 生成 URL 的那一面#
Unit 42 演示过 LLM 运行时组装攻击:客户端实时调 LLM API,在受害者浏览器里生成恶意 JS,每次语法不同、功能相同,不留静态载荷。把这思路迁到 URL 上,就是让 LLM 生成符合目标企业内部命名规范的 URL 路径和参数名,每个组成部分都按目标风格定制后,基于特征的检测基本失效。
但这路线有它自己的破绽:LLM 生成的 URL 太”完美”了。真实业务里全是随机和不规范,拼写错误、不一致命名、版本号遗漏。一个被 LLM 优化得过于工整的 URL,反而可能因为”太干净”被异常检测标出来。这正是 GREASE 精神(RFC 8701)在反检测领域的回声:你得故意留点看似 bug 的特征,才能融进环境。
6.5 结尾#
参数级检测全面失效后,焦点往两个方向迁。
基础设施聚类。 URL 怎么伪装,钓鱼服务器的底层基础设施(ASN、托管商、IP 段、证书签发模式、DNS 配置)会形成稳定的聚类特征。不同 campaign 共享的基础设施特征,参数伪装盖不住。这是最值得的检测维度,可以参考现在的PhaaS
行为基线。 一个被攻击者管理的域名,行为一定偏离它的历史基线。哪怕域名年龄 10 年,它的 DNS 解析模式、证书更新模式、页面响应模式也一定在某维度上偏离原始用途。
借用德勒兹的一句话:To become is to deterritorialize。
系列导航