chaihongjun.me

ThinkPHP 项目文件完整性监控:用 Shell 脚本对抗黑帽 SEO 注入

1. 背景

公司官网(ThinkPHP 5.0)曾遭遇黑帽 SEO 注入。攻击者在 public/index.php 中插入了 base64 编码的远控地址,逻辑如下:

  • 检测 User-Agent:如果是 Baiduspider / Sogou / 360 / Yisou 等爬虫,返回一个完全不同的假页面

  • 来自搜索引擎的真实用户点击搜索结果 → 被重定向到垃圾站

这种注入在文件层面极其隐蔽——网页在浏览器中显示完全正常(非爬虫请求不受影响),只有从搜索引擎过来的流量才会中招。等到自然搜索流量大幅下滑才发现已经被黑了很久。

虽然恶意代码已经清理干净,但一个问题摆在面前:下次怎么第一时间知道文件被改了?

2. 需求

针对这次教训,我们列出了几条刚需:

  1. 不侵入项目代码 — 不能往 PHP 项目里加文件完整性检查的逻辑,避免引入新的风险面

  2. 零依赖 — 不能用 Tripwire / AIDE / OSSEC 等需要安装的第三方工具,生产环境能不动就不动

  3. 不需要监控服务 — 不接入收费 SaaS 监控,也不需要单独搭一套监控平台

  4. 每天检查一次就够了 — 黑帽 SEO 不会频繁注入,太高的检测频率没有意义

  5. 有变化时登录能看见 — 不需要单独发邮件或短信,SSH 进服务器的时候直接看到警告

总结成一句话:"每天跑一遍、文件变了就警告、登录就能看见"。

3. 方案选型

满足上述需求,最简单的方案就是 Shell 脚本 + cron + /etc/profile.d/

核心命令只有三条:

find   # 递归找出所有 PHP/HTML/JS/CSS 文件
md5sum # 计算每个文件的哈希值
diff   # 对比基线快照和当前快照

为什么不用其他工具:

方案问题
Tripwire / AIDE需要安装、维护策略文件、学习成本高
ThinkPHP CLI 命令需要加载框架,依赖 PHP 环境,比纯 Shell 慢
Git diff生产环境不一定有 Git 历史,且 .gitignore 外的文件可能漏检
inotify 实时监控资源开销大,黑帽 SEO 不需要秒级发现
find + md5sum + diff零依赖,3 条命令,70 行脚本搞定

选 Shell 的另一个好处:部署新代码后更新基线只需一条命令,不需要重建索引或等待同步。

4. 检测原理

snapshot — 建立基线

扫描 PROJECT 目录下 *.php *.html *.js *.css
    跳过 runtime/ uploads/ .git/ .codegraph/
    └→ 对每个文件执行 md5sum
        └→ 按文件路径排序,输出到 /var/secure/thinkphp-checksum.txt

check — 比对检查

同上重新扫描,生成临时快照
    └→ diff 基线文件 vs 临时快照
        ├── < 开头的行 = 基线有但当前没有 → [缺失](文件被删)
        └── > 开头的行 = 当前有但基线没有或哈希不同 → [变更](新增/篡改)
            └→ 写入 /var/run/thinkphp-integrity.flag

如果文件被篡改(哈希变化),diff 中会同时出现一行 <(旧哈希)和一行 >(新哈希),脚本统一归类为 [变更]

登录告警联动

/etc/profile.d/ 下的脚本在每次 SSH 登录时自动执行。我们放置的 thinkphp-warning.sh 读取 /var/run/thinkphp-integrity.flag:有内容就展示红色警告,flag 不存在或为空则不输出任何内容。

cron(凌晨3点) → check 脚本
    ├── 无差异 → 删除 flag → SSH 登录无任何提示(无事发生)
    └── 有差异 → 写入 flag → SSH 登录显示红色警告

5. 涉及的脚本

整个监控体系由三个文件组成:

文件用途
thinkphp-integrity.sh主脚本:snapshot 建立基线 / check 比对检查
etc-profile-d-thinkphp-warning.sh部署到 /etc/profile.d/,SSH 登录时自动展示告警
README.md使用文档

主脚本核心逻辑

#!/bin/bash
SNAPSHOT="/var/secure/thinkphp-checksum.txt"
CURRENT="/tmp/thinkphp-checksum-$$.txt"
FLAGFILE="/var/run/thinkphp-integrity.flag"

do_check() {
    find "$PROJECT" -type f \( -name "*.php" -o -name "*.html" \
        -o -name "*.js" -o -name "*.css" \) \
        ! -path "*/runtime/*" ! -path "*/uploads/*" \
        ! -path "*/.git/*" ! -path "*/.codegraph/*" \
        -exec md5sum {} \; | sort -k2 > "$CURRENT"

    diff "$SNAPSHOT" "$CURRENT" | grep '^[<>]' | \
        sed "s/^</[缺失] /;s/^>/[变更] /" | \
        awk '{print $1, $NF}' > "$FLAGFILE"

    if [ -s "$FLAGFILE" ]; then
        echo "警告:$(wc -l < "$FLAGFILE") 个文件发生变化!"
        cat "$FLAGFILE"
    else
        echo "所有文件正常"
        rm -f "$FLAGFILE"
    fi
}

6. 部署步骤

第一步:部署主脚本

sudo cp thinkphp-integrity.sh /usr/local/bin/thinkphp-integrity.sh
sudo chmod +x /usr/local/bin/thinkphp-integrity.sh

第二步:建立基线

sudo /usr/local/bin/thinkphp-integrity.sh snapshot

基线文件存储在 /var/secure/thinkphp-checksum.txt,权限 root:root 700,Web 用户无法写入。

第三步:部署登录警告

sudo cp etc-profile-d-thinkphp-warning.sh /etc/profile.d/thinkphp-warning.sh
sudo chmod 644 /etc/profile.d/thinkphp-warning.sh

第四步:添加计划任务

sudo crontab -e
# 加入以下行:
0 3 * * * /usr/local/bin/thinkphp-integrity.sh check

验证

# 手动检查
sudo /usr/local/bin/thinkphp-integrity.sh check

# 退出 SSH 重新登录,确认无差异时无提示
exit
ssh you@server

7. 输出示例

正常情况

所有文件无变化时,cron 凌晨 3 点静默执行,登录时无任何提示——无事发生即是最好的消息

有文件被篡改时

$ ssh admin@ruihanzx.com
Last login: Mon Jul 14 08:30:22 2026 from 10.0.0.1

!!!!!!!!!!!!!!!!!!!!!!!!!! 文件完整性警告 !!!!!!!!!!!!!!!!!!!!!!!!!!
以下文件发生变化(新增/删除/篡改):
  [变更] public/index.php

执行以下命令确认并更新基线:
  sudo /usr/local/bin/thinkphp-integrity.sh snapshot
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

部署新代码后更新基线

# 部署完成后
sudo /usr/local/bin/thinkphp-integrity.sh snapshot
基线已建立:3421 个文件

8. 一些细节

为什么会有 [缺失] 文件

runtime/ 目录下的日志文件不在扫描范围内,但如果应用程序在运行时生成或删除了缓存文件(且路径未被正确排除),diff 会报 [缺失] 或 [变更]。排查时将这类路径加入 ! -path 排除即可。

维护成本

这套脚本上线后几乎零维护。唯一需要操作的是:部署新代码后跑一次 snapshot 更新基线。如果把这个动作也忘了,check 会报大量 [变更],此时跑一次 snapshot 即可恢复。

反过来想

70 行 Shell 脚本解决了一个可能让网站被降权、搜索流量归零的安全问题。不是所有安全方案都需要微服务架构或昂贵的商业产品,有时候 find | md5sum | diff 就够了。

本文涉及的脚本

etc-profile-d-thinkphp-warning.txt

thinkphp-integrity.txt


以上脚本部署的时候注意修改后缀

知识共享许可协议本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。作者:柴宏俊»