· liyu · network-security · 27 min read · — 次浏览
合作伙伴的图片服务器沦陷记:一次 MinIO 挖矿攻击真实案例与完整攻击链还原
合作伙伴的图片资源服务器遭挖矿攻击:改完密码后秒级又被自动改回去、ls 看不到的文件却真实存在、伪装成 mysqld 的挖矿进程在磁盘上早已"消失"——本文以这次真实事件为样本,完整还原攻击者从 MinIO 弱口令进入、Root 提权、系统命令替换到 Watchdog 守护与后门持久化的七阶段攻击链,分析应急过程中的正确操作与三处失误,并给出从隔离、取证、格盘重装到凭据轮换的标准处置 SOP 与长效防范思路。
一、开篇:一个”闹鬼”的图片资源服务器
事情的起点,是合作伙伴的一台图片资源服务器——上面跑着 MinIO 对象存储,承载着业务的图片素材。某天它开始出现几个诡异的现象:
- 服务器 CPU 长时间满载,load average 一度冲到 23+;
- Docker 服务莫名其妙停了,
docker ps报Cannot connect to the Docker daemon; - 我把 SSH 用户密码改掉,几分钟后密码又”自动恢复”成了旧的;
- 更邪门的是,明明怀疑
/tmp里有问题文件,ls /tmp看起来却一切正常。
如果你只遇到第一条,那可能只是业务高峰;但四条同时出现,尤其是在改完密码又能被改回去之后,基本可以下一个结论了:
这不是”中了挖矿病毒”,这是一次 Root 权限级别的完整主机失陷(Full Compromise)。
这篇文章会完整还原这次事件的攻击链和背后的技术原理,复盘我应急处置时做对了什么、踩了哪些坑,最后给出标准的处置 SOP 和以后的防范思路。希望它既能当一篇攻击链分析,也能当一份可以照着做的应急手册。
二、先定性:为什么”杀掉挖矿进程”远远不够
在动手之前,先看一张现场证据与风险等级的对照表:
| 风险 | 现场证据 | 严重程度 |
|---|---|---|
| Root 权限失陷 | 可篡改 /etc/shadow、系统二进制 | 极高 |
| 挖矿载荷 | /tmp/mysqld (deleted) 进程 | 高 |
| Watchdog 守护 | /61ab68db (deleted) 进程 | 极高 |
| 系统命令替换 | ls / cat / rm 被替换为 1.3MB 恶意二进制 | 极高 |
| 认证文件篡改 | 修改密码后被自动恢复 | 极高 |
| 远程控制后门 | 伪装成 httpd 的 Perl 进程 + 异常 SSH 外联 | 极高 |
| 业务干扰 | Docker 服务被停止 | 高 |
| 数据泄露可能 | Root 权限理论上可读取全盘数据 | 极高 |
这张表直接决定了处置策略:任何”找到恶意文件删掉就好”的思路都是错的。当攻击者拥有 Root、替换过系统工具、部署了守护进程和后门时,唯一可信的终点是:备份业务数据 → 保全证据 → 格盘重装。
三、攻击链全景:七阶段还原
根据现场证据,可以拼出攻击者的完整攻击路径:
互联网暴露的 MinIO 服务(弱口令 admin/admin123)
│
▼
① 获取初始执行权限
│
▼
② 提权至宿主机 Root(疑似经 Docker 攻击面)
│
▼
③ 停止 Docker 服务 —— 独占 CPU 资源
│
▼
④ 替换系统命令 ls / cat / rm —— Rootkit 隐藏
│
├────────────────────────┐
▼ ▼
⑤ 挖矿载荷 ⑥ 持久化后门
/tmp/mysqld (deleted) Perl 后门伪装 httpd
│ SSH 异常会话 + C2 外联
▼ │
Watchdog 守护 ◄──────────────────┘
/61ab68db (deleted)
│
├── 监控挖矿进程:被杀就重启
└── 监控 /etc/shadow:改密码就写回
│
▼
⑦ 连接矿池 / C2,长期稳定挖矿需要严谨说明:“初始入侵是否一定通过 MinIO 进入”、“是否发生了 Docker 逃逸”属于基于现场证据的高可信推断,受感染系统的日志可信度本身存疑,无法百分之百回溯确认。但攻击链后段的每一个环节(工具替换、隐藏进程、Watchdog、后门、shadow 篡改)都有确凿的现场证据支撑。
下面逐阶段拆解技术原理。
四、阶段一:入口——一个 admin/admin123 引发的事故
事后检查 Docker 容器元数据(config.v2.json / hostconfig.json)时,发现了这样一个配置:
MINIO_ROOT_USER=admin
MINIO_ROOT_PASSWORD=admin123同时这个 MinIO 容器还把宿主机目录直接挂了进去:
/mnt/minio-data:/data这三行配置,每一行都是教科书级的反面教材:
- 弱口令:
admin/admin123出现在全球攻防团队的字典第一页,暴露在公网的 MinIO 控制台会被自动化工具在几分钟内爆破成功; - 暴露面:管理端口直接对公网开放,等于把大门拆了;
- 宿主机目录挂载:一旦容器被攻破,攻击者顺手就能摸到宿主机文件系统,容器与宿主机之间的隔离形同虚设。
更重要的是,拿到 MinIO 管理权限通常意味着可以上传对象、触发服务端处理逻辑,在历史漏洞的加持下很容易过渡到”任意文件写入/命令执行”。虽然本事件中初始入侵路径无法 100% 锁定,但一个弱口令加暴露端口的组合,足以解释一切。
教训:公有云时代的攻击是全自动的。 攻击者不需要知道你是谁,扫描器 7×24 小时在互联网上转,
admin123被试中的时间以分钟计。
五、阶段二:为什么攻击者要停掉 Docker?
排查中发现 docker.service 处于异常停止状态。结合挖矿场景分析,这是攻击者的主动选择,目的有两个:
其一,独占 CPU。 一台物理机的算力是零和博弈:
停 Docker 前:MinIO + 业务容器 + 挖矿进程 → 三方抢 CPU,挖矿效率低
停 Docker 后:挖矿进程 独占全部核心 → 算力收益最大化其二,干扰视线。 业务突然挂掉,管理员的注意力会立刻被”服务为什么停了”吸引,忙着重启 Docker、排查业务,反而没有精力去看真正的挖矿进程。这是一种典型的资源独占 + 业务干扰组合拳。
六、阶段三:Rootkit 的核心——替换系统命令
这是整个事件里最有意思、也最危险的部分。
6.1 发现:ls 怎么这么胖?
正常 Linux 中 ls 只有 100KB 出头,但现场发现:
/bin/ls、/usr/bin/ls → 约 1.3 MB
/usr/bin/opentr → 约 146 KB,strings 可见 GNU Coreutils 字符串真相一目了然:攻击者把原版 ls 改名成了 opentr 藏起来,然后把一个约 1.3MB 的恶意 Wrapper 放到 /usr/bin/ls 的位置。而且修改时间 2026-04-15 16:45:03 在 opentr、ls、zipcat、ziprm 多个文件上完全一致——这是同一个批量脚本一次性完成的。
6.2 恶意 ls 能做什么?
管理员执行: ls /tmp
恶意 ls: 读目录 → 过滤黑名单文件名(mysqld / xmrig / 61ab68db ...)→ 输出
管理员看到: 干干净净,什么都没有
实际磁盘: /tmp/mysqld 好端端躺在那里这就是典型的用户态 Rootkit:不修改内核,只劫持你观察世界的眼睛。同样的思路可以作用于 ps(隐藏进程)、netstat(隐藏连接)、cat(读文件时篡改内容)、rm(删除恶意文件时假装成功)。
6.3 由此推出应急响应的第一原则
一旦怀疑 Rootkit,默认这台机器上所有用户态工具都不可信:
ls、ps、top、netstat、ss、cat、find、rm,一个都别信。
那还能信什么?后面会讲,/proc 是这次事件中的破局点。
七、阶段四:落地即删——(deleted) 背后的进程隐藏术
通过 /proc 排查时,发现了本次事件最关键的两个进程:
PID 2269 → /tmp/mysqld (deleted)
PID 2116 → /61ab68db (deleted)(deleted) 是什么意思?
Linux 中进程启动后,可执行文件会被加载进内存并保持打开。此时即使执行 unlink() 把磁盘文件删掉——目录项消失,但进程依然正常运行:
execve("/tmp/mysqld")
↓
文件被加载、保持打开
↓
unlink("/tmp/mysqld") ← 磁盘上文件消失
↓
进程照常运行,内存中代码完整这套”落地即删(Unlink Evasion)“的组合效果极佳:
- 伪装名
mysqld——看起来像 MySQL 数据库进程,谁会对mysqld产生怀疑? - 磁盘文件已删除——
ls /tmp找不到任何可疑文件,文件扫描器扑空; - 只有
/proc/<PID>/exe会诚实地显示/tmp/mysqld (deleted)。
从恶意二进制中提取到的 transparent_hugepage、hpage_pmd_size 等字符串,与 XMRig 类门罗币挖矿程序利用 Linux 大页内存做性能优化的特征高度吻合,可以基本判定这是一个 CPU 挖矿载荷。
八、阶段五:Watchdog——为什么密码改了会”自己变回去”
/61ab68db (deleted) 这个进程,才是整条攻击链里真正的”钉子”。它是一个守护程序(Watchdog),干两件事:
第一件,守护挖矿进程:
循环检测: /tmp/mysqld 还活着吗?
├── 活着 → sleep 继续监控
└── 被杀 → 重新拉起第二件,监控认证文件——这解释了开头那个”闹鬼”现象:
管理员执行 passwd 修改密码
↓
/etc/shadow 内容变化
↓
Watchdog 检测到 shadow 变更
↓
重新写入攻击者已掌握哈希的条目
↓
几分钟后,密码"自己"变了回去这属于认证文件持久化篡改。很多人排查时会反复尝试”再改一次密码”,但只要 Watchdog 还活着,改多少次都是徒劳——你根本没在跟”密码”作战,你是在跟一个 Root 权限的守护进程作战。
九、阶段六:Perl 后门与异常 SSH 会话
排查 /proc 时还发现了一个伪装高手:
PID 1403 → /usr/bin/perl,进程名伪装成 usr/sbin/httpd,工作目录 /一个”长得很像 Web 服务器”的 Perl 进程,高度疑似反弹 Shell / 交互式后门。它的战略意义远超挖矿本身:
攻击者 → C2 服务器 → Perl 后门 → Root Shell也就是说,就算管理员发现并杀掉了挖矿进程、改掉了所有密码,只要这个后门还在,攻击者随时可以卷土重来。挖矿载荷只是”赚钱工具”,后门才是攻击者的”房产证”。
网络侧还通过 /proc/net/tcp 发现了两条 SSH 相关外联:
49.93.125.47—— 后来确认是管理员自己的公网出口 IP;122.96.37.92—— 高度可疑的攻击者 SSH 会话。
这里埋了一个我后来踩的大坑,应急篇细说。
十、七层防御体系:攻击者为什么设计得这么复杂?
把上面所有环节叠起来看,攻击者构建了一个接近完整的 Linux 主机入侵工具链:
| 层级 | 手段 | 目的 |
|---|---|---|
| 第一层:伪装 | 挖矿程序命名 mysqld | 混淆视听 |
| 第二层:隐藏 | 替换 ls/cat/rm + 落地即删 | 文件与进程不可见 |
| 第三层:自恢复 | Watchdog | 杀了就重启 |
| 第四层:权限维持 | Perl 后门 + SSH 后门 | 随时回来 |
| 第五层:反管理 | 监控 /etc/shadow 写回密码 | 改密码无效 |
| 第六层:反分析 | 磁盘文件全部删除 | 取证困难 |
| 第七层:资源独占 | 停止 Docker | 算力收益最大化 |
单纯跑一个 xmrig 太容易被发现。攻击者真正的目标是:把这台服务器变成一个长期、稳定、无人察觉的矿机。每一层设计都在对抗管理员的一种处置手段。
十一、应急处置复盘:做对了什么
11.1 先保业务数据,没有上来就格盘
第一时间完成了 MinIO 数据(/mnt/minio-data)和 Nginx 配置(/etc/nginx)的备份,并从 Docker 元数据中恢复出了完整的容器重建信息:
镜像: minio/minio:RELEASE.2025-04-22T22-12-26Z-cpuv1
挂载: /mnt/minio-data:/data
端口: 宿主机 90 → 9000,9001 → 9001应急处置不能以牺牲业务数据为代价,这一点上的顺序是对的。
11.2 用 /proc 绕过 Rootkit 的欺骗
这是本次处置最关键的一步。既然 ls 和 ps 都可能说谎,那就直接问内核——/proc 文件系统是内核真实状态的投影:
# 遍历所有进程,查看真实可执行文件路径
for pid in /proc/[0-9]*; do
exe=$(readlink -f "$pid/exe" 2>/dev/null)
case "$exe" in
*deleted*) echo "可疑: PID $(basename $pid) → $exe" ;;
esac
done输出直接锁定了两个目标:
[x] PID 2116 → /61ab68db (deleted) ← Watchdog
[x] PID 2269 → /tmp/mysqld (deleted) ← 挖矿主进程11.3 标准的两步击杀:先冻结,再击杀
kill -STOP 2116 2269 # 先冻结,防止 Watchdog 在间隙拉起矿机
kill -9 2116 2269 # 确认冻结后彻底终止先 STOP 后 KILL 的顺序很重要:如果直接 kill -9 挖矿进程,Watchdog 可能抢在你杀它之前把矿机拉起来;先冻结两者,再逐个击杀,才能避免”按下葫芦浮起瓢”。执行完毕后系统负载开始回落。
11.4 但——杀掉进程 ≠ 服务器安全了
这是本次事件最重要的结论。攻击者已经完成 Root 权限下的持久化布局,即使两大核心进程被清掉,仍可能存在未被发现的后门:
SSH authorized_keys / Cron / systemd 单元 / PAM 模块
LD_PRELOAD / 内核模块 / /etc/rc.local / .bashrc
隐藏 ELF / WebShell / 其他后门用户 ...“CPU 降到 0”只能证明矿机停了,不能证明房子里没有别的贼。
十二、踩过的坑:三次失误与正确做法
应急过程不可能完美,把错误也写下来,比只写成功更有价值。
坑一:用了攻击者留下的”原版备份工具”
现场发现 zipcat、ziprm 两个文件,我最初判断它们是攻击者替换工具时顺手备份的原版 cat/rm,于是直接复制回去恢复系统命令。
结果它们同样是约 1.3MB 的恶意二进制。 执行后立刻出现 Too many open files、Shell level 1000,CPU 负载再次暴涨——恶意程序在递归 fork:
cat → 恶意 Wrapper → bash → bash → bash → ... → 1000 层文件描述符和 pipe 资源瞬间耗尽。这是一个非常典型的教训:
永远不要使用攻击者留在受害机上的”备份”来恢复系统。 正确做法是从可信来源重装软件包(Ubuntu 下
apt-get install --reinstall coreutils,且最好在隔离环境下进行),或直接使用可信 Live CD / 救援系统操作。
坑二:没确认自己的 IP 就执行了 ss -K
发现可疑外联后,我直接执行了:
ss -K dst 49.93.125.47下一秒,自己的 SSH 会话被掐断了——因为 49.93.125.47 后来确认是管理员自己的公网出口 IP。ss -K 成功地杀掉了……我自己的 TCP 连接。
正确流程应该是:
① 先确认自己的公网出口 IP(curl ifconfig.me 之类)
② 再区分攻击者 IP 与自身 IP
③ 确认 SSH 会话对应的 PID
④ 最后再 kill / 封禁坑三:iptables 执行失败却没有验证结果
尝试添加封禁规则时实际返回了 iptables: command not found——规则从未写入,但后续处置逻辑仍按”22 端口已封禁”继续推进。正确原则:
任何安全控制动作,必须检查命令返回结果。 “执行了封禁命令”和”封禁生效”是两回事。Ubuntu 上还要先搞清楚系统用的是
iptables、nft还是ufw。
坑四(方法论层面):取证顺序颠倒了
更理想的顺序是”先取证、后处置”:
发现入侵 → 隔离网络 → 保存内存/进程样本 → 保存日志
→ 记录网络连接 → 保存恶意文件 → 最后才杀进程本次是”发现即处置、边处置边调查”,作为业务优先的止血策略可以理解,但如果事件需要溯源定责或司法取证,这个顺序会丢失大量关键证据(进程被杀、内存消失后,(deleted) 背后的样本就再也拿不回来了)。
十三、正确的完整处置 SOP:从隔离到恢复公网
结合本次经验,整理出一套可复用的标准流程:
P0 判定 ──── CPU 异常 + 密码自动恢复 + 可疑文件 → 直接按"Root 失陷"处置
P1 隔离 ──── 优先物理断网 / 交换机 ACL / 安全组(在受害机之外操作)
P2 可信调查 ─ 不信任受害机工具,使用 IPMI / iLO / Live CD / 救援系统
P3 保全证据 ─ /proc 样本、/var/log、crontab、systemd、SSH、网络连接、恶意 ELF
P4 备份业务 ─ 数据库、MinIO、静态资源、源码、配置、证书(备份后需在干净机器复查)
P5 彻底重装 ─ 格盘 → 全新安装 OS → 更新全部软件包 → 重建 Docker / Nginx
P6 凭据轮换 ─ 见下节清单,全部换掉
P7 恢复业务 ─ MinIO → 数据库 → Docker → Nginx → Web/API
P8 加固收尾 ─ 防火墙 + SSH 加固 + fail2ban + 自动更新 + 监控,最后才开放公网13.1 为什么必须格盘重装?
因为**“清除了已发现的文件”无法证明”系统干净”**。你永远不知道攻击者是否还留了别的后门:一个 PAM 模块、一个 LD_PRELOAD、一个内核模块、一个被改过的 sshd。在这种确定性不对称面前:
重装的成本是确定的几小时,“相信自己已经清干净”的风险是不确定的无限大。
13.2 重装后的凭据轮换清单
Root 失陷意味着攻击者理论上读过全盘数据。以下凭据全部视为已泄露,必须轮换:
SSH 密钥对 → 重新生成,废弃旧 Key,清查 authorized_keys
Linux 用户密码 → 全部重置
MinIO Access/Secret → 重建(admin/admin123 永久作废)
MySQL / Redis 密码 → 全部更换
JWT Secret → 更换(会使所有已发 token 失效)
第三方 API Key / OSS Key / SMTP / Git Token → 逐个轮换
SSL 证书私钥 → 重新申请签发,不要原样恢复旧私钥十四、防范思路:下次别再给攻击者送分
复盘整条攻击链会发现,攻击者的每一步都利用了”低成本配置 + 缺失监控”。防范也只需要对症下药:
14.1 收缩攻击面(掐断阶段一)
- 消灭弱口令:所有暴露服务强制强密码/密钥认证,
admin123这类口令直接禁止; - 最小暴露面:MinIO 控制台、数据库、Redis 等管理端口一律不对公网开放,必须访问就走 VPN / 堡垒机 / IP 白名单;
- SSH 加固:禁用密码登录、仅密钥认证、禁用 root 直接登录、fail2ban 自动封禁爆破 IP。
14.2 容器安全(掐断阶段二)
- 容器不加
--privileged、不挂载/var/run/docker.sock; - 尽量避免把宿主机敏感目录直接挂进容器,确需挂载用只读(
:ro); - 保持 Docker 与内核及时更新,堵住已知逃逸漏洞。
14.3 纵深检测(让阶段三~六无处藏身)
- 文件完整性监控:AIDE / osquery 对
/usr/bin、/bin建立基线,系统命令被替换立刻告警; - 关键文件监控:用 auditd / inotify 监控
/etc/shadow、/etc/passwd、authorized_keys的变更——本次事件的”密码自动恢复”如果有 shadow 变更告警,第一时间就能暴露; - 异常进程监控:对”名字像系统服务、路径却在
/tmp”、/proc/<PID>/exe指向(deleted)的进程直接告警; - 外联监控:服务器通常不需要主动外连陌生 IP,对矿池协议端口(如 3333、4444、5555)出站封禁。
14.4 降低爆炸半径
- 业务容器与宿主机服务资源隔离,挖矿程序无法一句话停掉 Docker 独占资源;
- 最小权限原则:业务进程不要用 root 跑,被攻破时攻击者还要再做一次提权;
- 定期快照 + 异地备份,让”格盘重装”的底气更足。
十五、写在最后:十条值得记住的经验
- Root 权限失陷后,不要再相信这台机器上的任何东西。
ls能骗你,ps也能骗你;/proc是内核留下的诚实入口。(deleted)不代表消失——落地即删是挖矿木马的标配隐身术。/tmp/mysqld可以根本不是 MySQL。 进程名是最廉价的伪装。- 密码改完又自动变回去,是最强的持久化入侵信号,没有之一。
- 攻击者最值钱的不是挖矿程序,而是 Watchdog 和后门。
- 攻击者留下的”原版工具备份”,本身就是武器。
- 杀掉挖矿进程 ≠ 清除入侵;CPU 恢复正常 ≠ 服务器干净。
- 动手封 IP 之前,先确认哪个 IP 是你自己的。
- Root 失陷的生产服务器,格盘重装永远比”我觉得我清干净了”更安全。
这次事件的处置有亮点也有失误,但最大的收获是想明白了一个道理:安全攻防的本质不是”找到一个恶意文件然后删掉”,而是判断系统的信任边界是否已经被击穿。一旦 Root 沦陷,信任模型就崩塌了——此时最专业的选择不是恋战,而是快速保全数据与证据,然后在一片干净的地基上重建。
愿你的服务器永远用不上这篇文章。
回看感悟累计回看 — 次
加载中…