☰
PHP8.0网站被攻击怎么排查防护
2026/10/2 4:14:20 网站建设 项目流程

前言

"网站被攻击了"这句话背后其实是好几类完全不同的事故。搜索引擎里被收录了几千个赌博关键词页面,是挂马和内容注入;服务器 CPU 长期 100%、带宽跑满,是资源滥用(多半在被挖矿);用户收到钓鱼邮件、数据库里出现陌生账号,是数据泄露;接口突然大面积超时,是DDoS 或 CC 攻击。这四类的排查重点和处置手段都不一样。

需要先说明的是:PHP 8.0 并没有为安全引入什么专门机制,这个话题和版本基本无关。但它有几个和安全相关的行为变化值得知道——allow_url_include指令被移除(远程文件包含从机制上不再可能)、assert()的字符串形式被移除、create_function()被移除、preg_replace的/e修饰符早在 7.0 就没了。这些都是历史上 webshell 最常用的执行原语,它们在 8.0 上消失,会直接改变扫描特征库的写法。处置流程的核心原则只有一条:先取证,再隔离,最后清理,顺序颠倒会毁掉判断入口和影响范围的唯一线索。

一、先分辨事故类型

先对号入座能省掉大量无效劳动:

事故类型典型症状首要排查方向
挂马 / WebShell页面出现陌生的跳转脚本、搜索结果被劫持新增或近期被改动的 PHP 文件
后门账号出现不认识的登录记录、权限被提升SSHauthorized_keys、系统账号、数据库用户
数据泄露用户收到精准钓鱼邮件、接口被批量拉取数据库慢查询日志、出站连接记录
资源滥用CPU / 带宽长期打满、进程列表异常挖矿进程、守护脚本、出站流量
DDoS / CC大量并发导致服务不可用访问日志的 Top IP、UA、URL 分布
内容注入检索到大量与业务无关的页面内容表、模板文件、缓存文件

有个快速分辨技巧:如果只在特定 URL 上出问题,多半是代码或数据被注入;如果整站不可用,多半是资源或流量层面的攻击。

二、应急取证:别急着删文件

被入侵后最常见的错误操作,就是立刻rm掉可疑文件然后重启服务。这么做会把判断攻击入口、入侵时间点、影响范围的证据一并销毁。

2.1 先做现场快照

在动任何东西之前,按顺序把下面这些信息固化下来:

# 1. 时间基准 date -u +'%F %T UTC'; uptime # 2. 进程树:找异常进程和它的父进程 ps auxf > /root/ir/ps-auxf.txt # 3. 网络连接:重点看 web 用户发起的出站连接(C2 回连、挖矿矿池) ss -antp | grep ESTAB | grep -v ':443\|:80' # 4. 列出 web 目录下最近改动过的文件,按时间排序 find /var/www -type f -mtime -7 -printf '%T+ %s %p\n' | sort -r > /root/ir/recent.txt # 5. 计划任务(详见 2.4) ls -l /etc/cron.d/ /etc/cron.hourly/ /etc/cron.daily/ > /root/ir/cron-dirs.txt # 6. 系统完整性校验(按发行版二选一) debsums -c # Debian / Ubuntu rpm -Va # RHEL / CentOS # 7. 代码如果纳入了 git,这是最快的一条线索 cd /var/www/app && git status --porcelain && git diff --stat

代码在 git 里的话,第 7 条是最高效的手段——git status会列出所有被改动的文件,git diff能看出改了哪几行。反过来,如果显示"修改日期全变了但内容 diff 为空",往往是有人用touch改了时间戳来伪装。

2.2 把文件时间和访问日志对齐

判断"攻击者从哪个入口进来",关键是建立时间线:某个 PHP 文件被写入的时间点,往前找最近的 POST 请求。

<?php // timeline.php —— 需要 PHP 8.0+,用法:php timeline.php /var/www/app /var/log/nginx/access.log declare(strict_types=1); $wwwRoot = $argv[1] ?? '/var/www/app'; $logFile = $argv[2] ?? '/var/log/nginx/access.log'; // 第一步:近 7 天被改动过的 PHP 文件,按修改时间正序排列 $changed = []; $it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($wwwRoot, FilesystemIterator::SKIP_DOTS) ); foreach ($it as $file) { if ($file->isFile() && in_array(strtolower($file->getExtension()), ['php', 'phtml', 'inc'], true) && $file->getMTime() >= time() - 7 * 86400) { $changed[$file->getPathname()] = $file->getMTime(); } } asort($changed); // 第二步:对每个被改文件,列出它被写入前 30 分钟内的 POST 请求。 // 被改文件很多时,先把日志读进内存或按秒建索引,避免重复读盘 foreach ($changed as $path => $mtime) { printf("=== %s\n 改动时间:%s\n", $path, date('Y-m-d H:i:s', $mtime)); $found = 0; foreach (file($logFile, FILE_IGNORE_NEW_LINES) ?: [] as $line) { $ts = strtotime(substr($line, (int) strpos($line, '[') + 1, 20)); if ($ts === false || $ts < $mtime - 1800 || $ts > $mtime) { continue; } if (str_contains($line, 'POST')) { echo ' ', $line, PHP_EOL; if (++$found >= 5) { break; } } } if ($found === 0) { echo " (该时段内没有 POST 请求)\n"; } }

输出会给出"某个文件在什么时间被写入,之前的半小时里有哪些 POST 请求"。那个 POST 请求的 URL 和来源 IP,通常就是攻击入口——多半是一个文件上传接口、一个没做过滤的包含点,或者一个存在反序列化漏洞的接口。

2.3 扫描 WebShell

WebShell 的特征是"把用户输入当作代码执行",在源码里表现为高危函数和动态调用的组合。下面是一个只读扫描器:

<?php // webshell-scan.php —— 需要 PHP 7.4+,只读扫描,用法:php webshell-scan.php /var/www declare(strict_types=1); $root = $argv[1] ?? '.'; $patterns = [ 'eval 调用' => '/\beval\s*\(/i', 'assert 字符串形式' => '/\bassert\s*\(\s*[\'"][^\'"]*[\'"]\s*\)/i', '变量函数带用户输入' => '/\$\w+\s*\(\s*\$_(?:GET|POST|REQUEST|COOKIE|SERVER)\b/i', 'base64 解码' => '/\bbase64_decode\s*\(/i', '压缩流解压' => '/\bgz(?:inflate|uncompress|decode)\s*\(/i', '字符串混淆' => '/\bstr_rot13\s*\(/i', '动态 include' => '/\b(?:include|require)(?:_once)?\s*\(?\s*\$|[\'"]\s*\.\s*\$_/i', '命令执行家族' => '/\b(?:system|exec|shell_exec|passthru|popen|proc_open)\s*\(/i', '超长编码字面量' => '/[\'"][A-Za-z0-9+\/=%]{400,}[\'"]/', ]; $hits = []; $scanned = 0; $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS) ); foreach ($iterator as $file) { if (!$file->isFile() || !in_array(strtolower($file->getExtension()), ['php', 'phtml', 'inc'], true) || $file->getSize() === 0 || $file->getSize() > 2 * 1024 * 1024) { continue; } $path = $file->getPathname(); $content = (string) file_get_contents($path); $scanned++; foreach ($patterns as $label => $regex) { if (preg_match($regex, $content) === 1) { $hits[$path]['mtime'] = $file->getMTime(); $hits[$path]['reasons'][] = $label; } } } printf("扫描文件数:%d\n\n", $scanned); // 按改动时间倒序输出,最近被写的排最前 uasort($hits, static fn(array $a, array $b): int => $b['mtime'] <=> $a['mtime']); foreach ($hits as $path => $info) { printf("%s\n 改动时间:%s\n 命中特征:%s\n\n", $path, date('Y-m-d H:i:s', $info['mtime']), implode('、', $info['reasons'])); }

关于这个扫描器要说清三点。第一,它只读,不删除也不修改任何文件,这是取证阶段必须遵守的原则。第二,特征扫描一定有误报,"超长编码字面量"在压缩过的合法库里也会命中,结果要人工复核,不能直接批量删除。第三,PHP 8.0 移除了create_function()、assert()的字符串形式和preg_replace的/e,老版本的经验在 8.0 上会漏掉新手法——真正的判据永远是文件被写入的时间点与访问日志的对应关系,不是特征命中数。

2.4 查持久化

只清掉 WebShell 而不查持久化,等于把门锁换了但窗户还开着。按下面的清单过一遍:

# 计划任务 crontab -l -u www-data; crontab -l -u root grep -rn "curl\|wget\|base64\|/tmp/" /etc/cron* 2>/dev/null # SSH 免密登录后门 / 异常账号:UID 为 0 的应该只有 root cat /root/.ssh/authorized_keys 2>/dev/null awk -F: '$3 == 0 {print}' /etc/passwd # 动态链接器劫持(rootkit 常用) cat /etc/ld.so.preload 2>/dev/null # Web 服务器层面的持久化 grep -rn "auto_prepend_file\|php_value" /etc/nginx/ /etc/apache2/ /var/www/ 2>/dev/null find /var/www -name ".htaccess" -o -name "*.user.ini"

最后两条特别容易被忽略。auto_prepend_file能在每次请求前悄悄加载一个文件,.user.ini则是 PHP 的 per-directory 配置文件——即使php.ini锁得很死,攻击者也能在某个目录下放一个.user.ini来局部开启危险选项。

三、加固:从入口堵住

3.1 PHP 层配置

; /etc/php/8.0/fpm/php.ini expose_php = Off ; 不再返回 X-Powered-By 头,减少指纹信息 display_errors = Off ; 生产环境绝不能把报错显示在页面上 log_errors = On error_log = /var/log/php/error.log open_basedir = /var/www/app:/tmp ; 限制文件操作范围,越出即拒绝 disable_functions = exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec,putenv ffi.enable = Off ; 关闭 FFI,否则 disable_functions 可能被绕过 session.use_strict_mode = 1 ; 拒绝客户端传来的未知会话 ID,防会话固定 session.cookie_httponly = 1 ; 禁止 JS 读取会话 Cookie session.cookie_secure = 1 ; 只通过 HTTPS 传输 session.cookie_samesite = Lax ; 缓解 CSRF

关于disable_functions要有清醒认识:它只对函数生效,管不了语言结构,历史上也多次出现绕过方式。它是一道降低攻击收益的减速带,不是保险箱。真正的防线是最小权限——让 PHP-FPM 用独立低权限用户运行,Web 目录不可写,只开放storage、uploads这类必须可写的目录。

如果是从 PHP 7.x 升级过来的php.ini,注意allow_url_include指令在 PHP 8.0 已被移除,保留这一行会触发未知指令警告。远程文件包含在 8.0 上已不可能,但本地文件包含依然存在,include $_GET['page']照样能读到/etc/passwd。

3.2 上传目录禁止执行 PHP

这是最容易被忽略、也最容易被利用的一点。上传目录只要能执行 PHP,一个伪装成图片的.php文件就是一个永久后门:

# 上传目录一律拒绝执行 PHP,正则要覆盖常见的可执行后缀 location ~* ^/(uploads|storage|files)/.*\.(php|php[0-9]?|phtml|pht|phar)$ { deny all; return 403; }

配合代码层的白名单校验。只校验扩展名不够,攻击者还有双扩展名、.php.jpg之类的技巧;正确做法是同时校验扩展名白名单与服务端读到的真实 MIME,并把文件存到 web 根目录之外。三道最关键的代码防线放在一起看:

<?php // hardening.php —— 需要 PHP 8.0+ declare(strict_types=1); // ✅ 上传:白名单 + 服务端读真实 MIME + 文件名由服务端生成 $allowed = ['jpg' => 'image/jpeg', 'png' => 'image/png']; $ext = strtolower(pathinfo($_FILES['f']['name'], PATHINFO_EXTENSION)); if (!isset($allowed[$ext]) || !is_uploaded_file($_FILES['f']['tmp_name'])) { throw new RuntimeException('非法的上传来源或类型'); } if ((new finfo(FILEINFO_MIME_TYPE))->file($_FILES['f']['tmp_name']) !== $allowed[$ext]) { throw new RuntimeException('文件内容与扩展名不符'); } $dest = '/var/app-storage/uploads/' . bin2hex(random_bytes(16)) . '.' . $ext; move_uploaded_file($_FILES['f']['tmp_name'], $dest); chmod($dest, 0644); // 明确去掉执行位 // ✅ SQL:占位符绑定,SQL 结构与数据彻底分离 $stmt = $pdo->prepare('SELECT id FROM users WHERE email = ?'); $stmt->execute([$email]); // ✅ 反序列化:能不用就不用,必须用时禁止实例化任何类 $safe = unserialize($payload, ['allowed_classes' => false]);

这三条防线针对的是同一件事:不要把数据当成代码。白名单防的是"让上传的文件被当作 PHP 执行",占位符绑定防的是"让输入被当作 SQL 执行",allowed_classes => false防的是"让输入被当作对象重建的蓝图执行"——PHP 对象注入(POP chain)利用的正是__destruct()、__wakeup()这类魔术方法在反序列化时自动触发的特性。

四、清理与恢复

步骤动作注意
1从干净的代码仓库重新部署,而不是逐个删除可疑文件手工比对极易遗漏
2轮换所有凭据数据库密码、API key、APP_KEY、SSH 密钥、第三方 token
3强制所有用户重新登录会话 ID 可能已被窃取
4检查数据库里是否被插入了管理员账号或计划任务有些应用把定时任务存在数据库里
5复盘并补上审计日志登录、上传、权限变更都要留痕

第 2 条最容易被跳过,但它的重要性不低于清理 WebShell。攻击者只要读到了.env文件,你所有的数据库密码和密钥就已经在对方手上了,不轮换等于没修。

常见坑点

❌ 发现可疑文件后立刻rm掉并重启服务 ✅ 先做现场快照(进程、网络连接、文件时间线、日志),删除和重启会销毁判断入口与影响范围的唯一线索

❌ 只删除 WebShell,不排查持久化机制 ✅ 必须检查crontab、systemd单元、SSHauthorized_keys、数据库里的定时任务表,以及容易被忽略的.user.ini和.htaccess

❌ 认为配了disable_functions就挡住了命令执行 ✅ 它只对函数生效,历史上多次出现绕过方式;必须配合ffi.enable = Off、open_basedir和 PHP-FPM 独立低权限用户

❌ 上传目录和业务代码放在一起,并且允许执行 PHP ✅ 上传目录必须在 web 根目录之外,同时在 Nginx 层用location明确拒绝php、phtml、phar等后缀

❌ 用unserialize()处理 Cookie、缓存或任何用户可控的数据 ✅ 优先改用json_decode();确实需要反序列化时传['allowed_classes' => false],禁止任何类被实例化

❌ 清理完成后只改代码,不轮换凭据 ✅ 数据库密码、APP_KEY、API key、SSH 密钥全部要换,并强制所有用户重新登录;攻击者读到.env就等于拿到了这些

❌ 用find -mtime -7之类的单一时间维度判断哪些文件被改过 ✅ 攻击者可以用touch伪造修改时间,要结合git status、包管理器完整性校验(debsums -c/rpm -Va)和访问日志时间线交叉验证

总结

阶段关键动作最容易犯的错
取证进程树、网络连接、文件时间线、日志快照先删文件导致证据丢失
定位入口被改文件的 mtime 与访问日志中的 POST 请求对齐只靠特征扫描,不做时间线关联
查持久化crontab、SSH key、.user.ini、.htaccess只清 WebShell
加固 PHPdisplay_errors = Off、open_basedir、disable_functions、ffi.enable = Off、会话安全项把disable_functions当成万能防线
加固上传与数据目录在 webroot 之外 + 白名单 + MIME 校验 + 预处理语句只查扩展名、拼 SQL
恢复从干净仓库重新部署 + 全面轮换凭据不轮换密码和密钥

被攻击之后最有价值的产出不是"把后门删掉了",而是一条完整的时间线和确定的入口。把这两样拿到手,才知道要加固哪里、影响范围有多大、需不需要通知用户。删除文件是最容易做的一步,也是最不该先做的一步。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询