1. 背景
公司官网(ThinkPHP 5.0)曾遭遇黑帽 SEO 注入。攻击者在 public/index.php 中插入了 base64 编码的远控地址,逻辑如下:
检测 User-Agent:如果是 Baiduspider / Sogou / 360 / Yisou 等爬虫,返回一个完全不同的假页面
来自搜索引擎的真实用户点击搜索结果 → 被重定向到垃圾站
这种注入在文件层面极其隐蔽——网页在浏览器中显示完全正常(非爬虫请求不受影响),只有从搜索引擎过来的流量才会中招。等到自然搜索流量大幅下滑才发现已经被黑了很久。
虽然恶意代码已经清理干净,但一个问题摆在面前:下次怎么第一时间知道文件被改了?
2. 需求
针对这次教训,我们列出了几条刚需:
不侵入项目代码 — 不能往 PHP 项目里加文件完整性检查的逻辑,避免引入新的风险面
零依赖 — 不能用 Tripwire / AIDE / OSSEC 等需要安装的第三方工具,生产环境能不动就不动
不需要监控服务 — 不接入收费 SaaS 监控,也不需要单独搭一套监控平台
每天检查一次就够了 — 黑帽 SEO 不会频繁注入,太高的检测频率没有意义
有变化时登录能看见 — 不需要单独发邮件或短信,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
以上脚本部署的时候注意修改后缀





