0%
毅种循环

返回

邮件钓鱼攻防工程·4:参数多态与响应伪装Blur image

字数 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.goextractRIDFromRequest 的真实逻辑(精简后):

关键变化:RID 的识别从”按参数名查”变成”按参数值查 + 库里存在性确认”。参数名可以任意生成,甚至每个目标用不同参数名。

蓝队没法再用 SecRule ARGS:id 拦了,因为参数名成了不可预测的动态量。要检测就得穷举所有参数名、对所有参数值做熵分析,这在多数邮件安全网关上是性能上扛不住的。

2.2 路由层#

Gophish 在 route.go 里注册固定路由:

r.HandleFunc("/landing", landingHandler) // 固定路由 + 固定参数名
bash

taigong 用通配路由,路径匹配和参数解析完全解耦,所有非静态路径走同一个 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

参数名在发信时从词库随机选:tokensidauth_statesession_keycodeticketnoncesign。词库可扩展,能针对目标公司的内部命名习惯定制,比如用阿里云系的企业给 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&*(Df
bash

&*(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=ZqW8kPm
bash

这条 URL 里真正被解析的只有 sid=ZqW8kPm,其余全是合法业务里高频出现的参数名和值。蓝队做参数熵分析时,单个高熵值(ZqW8kPm 约 6.0 比特/字符)被一堆低熵值(zh-CNAsia/Shanghaimobile )稀释,整体统计特征向合法流量靠拢。


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: nginxContent-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 404Server: Microsoft-IIS/10.0、含 .aspx 路径提示、特定 HTML 结构

  • CDN 伪装:加 CF-RayCF-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路径结构 + 域名生命周期组合
SPAHash 路由(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。


系列导航