网页挂马检测实战:注入脚本排查与清理

网页挂马检测的难点不在删代码,而在找全落点。攻击者往往同时在模板文件、数据库正文、前端引用的第三方脚本里各埋一处,删掉一处其余照旧生效,站长于是反复”清理成功又复发”。本文按文件、数据库、配置层、响应差异四条线逐一排查,再给出清理与固化的完整顺序。

适用场景

以下现象任意出现两条就应启动排查:搜索结果页出现”该网页可能已被篡改”提示;访客反馈点击站内链接被跳到博彩或色情页面;移动端访问正常而 PC 端被跳转,或者反过来;站点多出一批从未发布过的页面与目录;无推广活动期间出站流量和带宽明显上涨。

企业官网、WordPress 博客、老版本 CMS 以及长期不更新的静态站都是重灾区。静态站并不天然安全,只要面板或 FTP 口令弱,攻击者可以直接改 HTML 文件,甚至不需要碰后端。

前置条件

  • 服务器 shell 权限,或至少面板的文件管理权限;
  • 一份可信基线:Git 仓库、上线前的完整备份,或同版本程序的官方源码包;
  • 可导出比对的数据库账号;
  • 能自定义 User-Agent 的 curl 环境,用于验证差异化返回。

没有基线也能查,但要靠特征串和修改时间推断,漏检概率明显上升。这也是为什么清理完成后必须建立基线,具体做法在最后一节说明。

原理说明

挂马落点可归为四类。模板层:在 header、footer、公共 include 文件里插入一段加密 JS,所有页面自动中招,隐蔽性最好,也最常见。数据层:把恶意 iframe 写进文章正文或站点配置项,改模板没用,因为内容是动态读出来的。外链层:站内代码本身干净,但引用的第三方统计、客服、字体脚本被替换,或者站点被指向某个被劫持的 CDN 资源。配置层:.user.ini 的 auto_prepend_file、.htaccess 的改写规则、Nginx 的 sub_filter,在请求进入业务代码之前就完成注入或跳转,这类最难靠关键词扫描发现。

检测思路上,时间维度解决”改了什么”,内容维度解决”改成了什么”,行为维度解决”对谁改”。三者交叉能大幅缩小范围:如果只有搜索引擎爬虫的 UA 会拿到跳转代码,那就是按 UA 分发的隐形跳马,正常访客和你的浏览器都看不到,只看页面源码永远查不出来。

操作步骤

步骤一:按修改时间锁定可疑文件

先看最近被动过的文件,排除缓存、日志、上传目录这些高频变更路径。攻击者通常会顺手把 mtime 改回去,所以还要用 -ctime 看 inode 变更时间,两者不一致的文件优先复核。

cd /www/wwwroot/example.com
find . -type f -newermt "2026-10-01" \
  ! -path "./runtime/*" ! -path "./cache/*" \
  ! -path "./uploads/*" ! -path "./data/log/*" \
  -printf '%TY-%Tm-%Td %TH:%TM %p\n' | sort -r | head -50
# 对比 ctime,发现 mtime 被回改的文件
find . -type f -name "*.php" -printf '%C+ %T+ %p\n' | awk '{print $1, $3}' | sort -r | head -20

如果一批文件的 mtime 集中在同一个时间点,基本可以确定是批量注入,直接把这批文件全部列入复核清单,逐个 diff 比对最省时间。

步骤二:特征串扫描

PHP 后门与注入代码有稳定的特征集合。以下命令只列文件名,不输出内容,避免终端刷屏。

# PHP 一句话与编码混淆特征
grep -rIl --include="*.php" -E 'eval\([[:space:]]*(base64_decode|gzinflate|str_rot13|gzuncompress)|assert\([[:space:]]*\$_(POST|GET|REQUEST)|preg_replace\(.*/e' .

# 前端注入特征
grep -rIl --include="*.html" --include="*.js" --include="*.php" \
  -E 'eval\(atob\(|document\.write\(unescape|top\.location|window\.location\.href[[:space:]]*=[[:space:]]*["'"'"']https?:' .

# 外部引用域名统计
grep -rIoh -E '(src|href)="https?://[a-z0-9.-]+' --include="*.php" --include="*.html" . \
  | sed -E 's/.*:\/\///' | sort | uniq -c | sort -rn | head -20

最后一条命令统计站内引用最多的外部域名。正常站点通常只有统计、客服、字体、CDN 寥寥几个。出现陌生域名且引用次数很少的,几乎都是注入点,这一条比任何特征库都好用。

步骤三:排查配置层注入

配置层文件不在业务目录里,最容易被漏掉。逐个检查以下位置,任何一处出现自动前置文件、替换过滤器、改写规则指向外部地址,都是高危信号。

# PHP 自动前置文件
grep -rn "auto_prepend_file\|auto_append_file" /www/server/php/*/etc/php.ini /www/wwwroot/example.com/.user.ini 2>/dev/null

# Apache 改写规则
grep -rn "RewriteRule\|php_value" /www/wwwroot/example.com/.htaccess 2>/dev/null

# Nginx 运行时注入
grep -rn "sub_filter\|add_header" /www/server/panel/vhost/nginx/*.conf | grep -v "#"

步骤四:数据库内容扫描

模板干净但页面仍有跳转,问题就在数据库。导出后 grep 是成本最低的做法,注意用 –single-transaction 避免锁表影响线上。

mysqldump --single-transaction -u root -p dbname > /tmp/db.sql
grep -n -E '<script|<iframe|onerror=|atob\(|eval\(' /tmp/db.sql | head -30
grep -c -E '<iframe' /tmp/db.sql

WordPress 站重点看 wp_options 里的站点地址配置项和 wp_posts 的正文;传统 CMS 则常见在配置表里被写入一段 base64 内容。定位到表和字段后再精确替换,不要整库覆盖,否则会丢评论和新内容。

步骤五:UA 差异验证

用不同 UA 请求同一 URL,比对响应体哈希。哈希不同说明存在按 UA 分发的注入。

for ua in "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" \
          "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)" \
          "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)"; do
  printf '%-16s ' "${ua:0:16}"
  curl -s -A "$ua" --max-time 15 https://www.example.com/ | md5sum
done

三个 UA 返回三个不同哈希是危险信号。正常站点应完全一致,若页面含动态时间戳或随机广告位,先过滤掉对应片段再算哈希。

步骤六:按正确顺序清理

顺序错了会白干。正确顺序是:先备份当前状态,再定位入口(后门不除,清理只是给攻击者演示),然后替换文件、清理数据库字段、更换所有相关口令,最后清缓存与 CDN。

  • 备份:tar czf /root/site-broken-$(date +%F).tar.gz -C /www/wwwroot example.com
  • 用基线回滚:Git 站执行 git checkout -- .;非 Git 站从官方源码包覆盖核心文件,保留上传目录与配置文件。
  • 数据库:用 UPDATE 精确替换恶意片段,或从清理前的备份中只导回被污染的文章记录。
  • 改密:面板、FTP、数据库、SSH、站点后台全部更换,主账号与数据库账号不共用同一口令。
  • 清缓存:PHP OPcache、页面缓存插件、CDN 边缘缓存逐层清,否则改了代码前台照旧。

配置验证

  • 重复步骤二、四的扫描命令,返回结果为空。
  • 步骤五的 UA 对比三个哈希完全一致。
  • 浏览器强制刷新与无痕窗口分别访问,跳转行为一致且无异常。
  • 搜索引擎侧:百度搜索资源平台提交抓取诊断与安全复核,Google Search Console 的安全问题板块提交审核请求,并重新提交站点地图。
  • 观察七十二小时:访问日志里不再出现来源不明的 POST 请求,出站流量回落到正常区间。

常见问题

Q1:恶意代码删掉了,访问还是被跳转?

按概率排序通常是四个原因:CDN 或反向代理缓存了旧页面;PHP OPcache 未重置仍在执行旧字节码;数据库里还有第二处注入;自动前置文件仍在加载外部脚本。前两个清缓存即可,后两个需要回到步骤三、四。还有一种情况是跳转由前端 JS 完成,浏览器本地缓存了旧脚本,用无痕窗口可以排除。

Q2:搜索引擎的危险网站提示多久消除?

清理完成到标记消除没有固定时长,取决于平台复核进度,通常是数天到数周。关键动作是主动提交复核并保证站点可正常抓取:robots.txt 不要拦爬虫、返回码正常、站点地图可访问。复核期间若再次被检出注入,等待时间会重新计算,所以务必先确认没有残留入口再提交。

Q3:纯静态 HTML 站也要做这些检查吗?

要做,但重点不同。静态站没有数据库注入面,主要风险是面板或 FTP 弱口令、同服务器其他站点跨目录写入、以及页面引用的第三方脚本被替换。检查时把步骤二的前端特征扫描与域名统计跑一遍即可,另外确认 Nginx 未开启目录列表、备份压缩包不在 Web 目录下。

总结

挂马排查的核心是找全而不是删净。文件、数据库、配置、外链四个落点必须全部过一遍,行为维度的 UA 对比是唯一能发现隐形跳转的手段。清理之后建议把站点纳入 Git 管理或用文件完整性工具建立基线,配合模板目录只读、上传目录禁执行、内容安全策略限制外链脚本,把下一次被注入的窗口压到最小。