早上刷到两条安全消息,第二条让我停下手里的活:PHP 核心出了两个远程代码执行漏洞,CVSS 9.2,无需登录,一个请求就可能打穿。官方给的修复版本只覆盖 8.2 到 8.5,另外为 7.4、8.0、8.1 发了最小化源码回移,因为影响面实在太大。
而我的服务器上是 PHP 5.6。
不是”暂时没升”,是这一代早就 EOL 了,连回移都没有。官方给的缓解建议只有两句:升级,或者下线。对 5.6 来说,这两句等于一句。
这次,懒癌犯了,我用agent帮我调查服务器环境。
想起上次看到的这幅图:
「禁止用AI控制服务器,否则运维大哥一枪崩了你」。

好在运维大哥也就是自己,那没事了。
破例,用AI辅助了,这潘多拉魔盒要被打开了,要非常谨慎……
且愉快地用AI协助调查服务器吧!
好了好了,这下坏了
我很清楚,我用的一直都是老版的PHP5。我本来只想确认一件事:我到底受多大影响。
结果打开站点目录一看,发现自己根本说不清”我有什么”。三个域名挂在同一台低配 ECS 上,其中一个主站、一个技术博客、一个家人用的博客。这些年陆续往上面堆过论坛、素材库、广告统计后台、微信接口、产品介绍页、Flash 时代的整站,能跑起来的都跑起来了,跑不动的也没人删。
于是先做排查。我没在服务器上装任何 agent,那台机器只有 2G 内存,多一个常驻进程就是给自己找麻烦。改成在本地直接 SSH 过去,只读地把三件事查清楚:装了哪些 PHP、哪些站点挂了 PHP 解析、最近还有哪些 .php 在被真实访问。
三条命令下来,结论比预想的糟:
- PHP 只有一个版本,5.6.40,php-fpm 在跑
- 三个站点全部挂着 PHP 解析
- 站点目录属主是 web 用户,权限 755,也就是 PHP 进程对整个站点目录有写权限
display_errors是开的
最后一条意味着任何一处报错都会把服务器绝对路径打给访客。我随手请求了一个老接口,报错里果然带着完整路径
。
比 CVE 更具破坏性的
我原本以为要对付的是那个 9.2 分的内存破坏漏洞。查日志的时候发现,真正的风险根本不需要构造利用链。
第一个:某个自建的日志查看页,参数直接进了 file_get_contents(),没有任何过滤。?logName= 后面填什么就读什么;同一个页面还有个 cls=true,能把任意可写文件清空。这个接口在公网上,不需要登录。
第二个:一个资源代理脚本,同样是裸的 file_get_contents($_REQUEST['url']),可以拿它当任意 URL 读取器,也能探内网。
第三个更离谱。一个十二年前写的动态域名脚本,任何人访问就会把访问者的 IP 写进三个站点文件,然后跳转到一个带明文账号密码的服务商更新地址。我本来是去确认它还在不在,结果它当着我的面执行了
,把线上文件改成了我的出口 IP。这是我自己造成的破坏,但同时也证明了它有多容易被打。
第四个:一个老后台的登录页还挂在公网上,里面是广告统计、数据库导出、配置修改。
还有一堆已经死了但还能访问的:游戏上报接口、素材库管理端、抽奖 demo、NFO 生成器、门禁开门接口。
这些加起来,比那个需要构造内存破坏的 CVE 现实得多。CVE 是别人家的 0day,这些是我自己写的后门。
笑哭。😂
处置顺序:先关最危险的,再谈优雅
我的原则很简单:能删的先删,删不掉的先断,剩下的才谈迁移。
第一件事是把那个 2009 年建的 WordPress 整站从服务器上撤下来。它跑在 WP 2.9
,挂了十四个同年代的插件,登录页对着公网,后台每天被境外 IP 扫。这种站没有”修补”的选项,只有”下架”。本地留了整站备份和一份数据库快照,先用不上,但不能丢。
第二件事是老站静态化。两个 Flash 时代的老站,页面本身早就是静态内容,只是当年为了拆模板和判断移动端,写成了 PHP。把模板展开成 HTML、把服务端 UA 判断改成一段内联 JS、把唯一还需要动态的谚语接口改成调自己新写的 Node 服务,剩下的 PHP 全部删掉
。
有一处不能一删了之:老站会通过一个小脚本代理外部 RSS,用来在”近况”页聚合博客和豆瓣。这个脚本本身就是个任意 URL 读取器,但直接删了页面会白屏。最后改成 nginx 反代,前端 URL 一个字符都没动,功能还在,PHP 下岗即可。
第三件事是 Flash 留言板。那个站的留言数据是从数据库查出来拼成 XML 给 Flash 读的,接口地址写死在前端资源里,改不了。解决办法是把当前全部留言导成一份静态 XML,再用 nginx 精确匹配把原来那个 .php 地址映射到这份 XML。留言板变成只读,但内容还在,而它本来也已经没人留言了。
第四件事是产品介绍页。三个游戏的产品页结构一样,都是一个 PHP 读浏览器语言头,然后 require 对应语言的 HTML。合并成一个文件,三种语言各放一个容器,用 JS 判断语言后显示对应的那个。这样 URL 不变、没有跳转闪烁、禁用 JS 时至少显示中文而不是白屏。
Nginx location 优先级踩坑记录
静态化之后,旧地址要 301 到新地址。我在伪静态里写了几条正则 location,测的时候全是 404。
查了站点配置才明白:面板生成的配置里,PHP 的 include 在前,伪静态的 include 在后。nginx 的正则 location 是按出现顺序匹配、先到先得,不按精确度。所以请求先被那条 location ~ \.php$ 接走,交给 php-fpm,文件早就删了,返回 404。我那几条 301 规则根本没机会执行。
有意思的是同一个文件里 location = /xxx.php 这种精确匹配是生效的——因为 = 的优先级最高,和顺序无关。
修法不是去改 include 顺序(那要动主配置),而是换一种 location 类型:location ^~ /目录/。^~ 命中后会跳过所有正则检查,等于把 PHP 那条短路掉,控制权回到自己手里,然后在这个 location 内部写 rewrite 完成 301。
这里还有个小细节:return 301 /a/$1.html 里的 $1 是 location 正则的捕获组,但 ^~ 是前缀匹配、没有捕获组,所以必须用 rewrite 自己的正则。


收尾
最后一轮是清理残余。find . -type f -name "*.php" 列出来的东西比我以为的多:几个 2020 年前后的疫情数据小工具、一个抽奖 demo、一个内容管理配置页、一个调试用的回显脚本。有两个还有用,一个旧站源码下载页、一个隐私政策入口,都被我改成了纯 HTML,其余全删。
删完再跑一次 find,站点根目录下输出为空。
顺手把统计也换了。原来每个页面挂的是第三方统计,现在统一换成自己后端托管的一行 script,其中老页面里散落的重复统计代码一并清掉。有个页面因为多语言合并,一份 HTML 里原来带着六段统计代码,全都会执行。
漏洞之外的熵增
这件事让我不太舒服的地方在漏洞之外:囤积多年的历史残余太乱,我居然不知道自己有什么
。


那台服务器上的东西是十几年里一层层堆上去的:Flash 站、移动端适配、小游戏后台、广告统计、微信接口、素材库。每一层在当时都有理由,也都”以后再说”。等到某天必须回答”你这里跑了什么”,才发现答案散在三个域名、十几个目录和一堆没人记得的接口里。
这次能在一个上午清干净,靠的不是我记性好,是那些东西恰好都能静态化。如果里面有一个还在产生收入、还在写数据库、还有真实用户的动态服务,今天就不是这个结局了。
另一个体会是排查方式。我原以为要在服务器上装个 agent 才叫”智能运维”,实际试下来,2G 内存的机器装什么都嫌重。反而是本地 agent 直接 SSH 过去

、只读地跑几条命令、把结果带回来分析,更快也更可控。全程没有一条写操作,唯一一次”破坏”是我探测一个脚本时它自己执行了——那次教训是:验证漏洞之前,先读代码确认它有没有副作用。
至于 PHP 5,这次之后它在我这儿算是正式退役了。还剩最后一个 WordPress 整站要静态化,做完就可以把 php-fpm 停掉
。
一个补不上的漏洞,最后变成了一次彻底的大扫除。不算坏事。
-
display_errors 开着这条,我第一反应也是”就一行配置嘛”,后来才想起来:路径里往往带用户名、项目名,等于免费给扫描器画了张地图。
炸弹你这台机器,改完这个估计还会剩几个
error_log里堆满老接口的 warning。顺手一起清了吧。 -
「验证漏洞之前,先读代码确认副作用」——这条我记下了。我上次在生产上复现 SSRF,脚本把内网服务的定时任务触发了,凌晨两点被叫起来看日志。教训一样,代价比你贵点。
-
看到这段我笑出声,又有点心酸。十二年前的脚本,写它的人当时肯定觉得”反正就自己用”。
历史里这种例子太多了,图省事埋下的东西,最后都是别人来考古。你这次当着自己的面被执行,算运气。
-
WP 2.9 是 2009 年的版本。我查了一下,那两个 9.2 的漏洞官方只回移到 7.4,5.6 连讨论都没有(cnblogs.com/…)。
所以你这站,连”补”这个选项都不存在,下架是唯一正确答案。
@中微子WP 2.9 那段你查得没错,回移只到 7.4,5.6 根本没进讨论范围。不过结论要往前挪一格:那个站的问题出在十四个同年代插件和公网登录页,WP 2.9 只是它的出生年份。换成 6.x 挂着那十四个插件,照样只有下架这一条路。
-
这个操作我特别有共鸣。把服务端判断改成内联 JS,URL 一个字符不动,等于给老房子换了套看不见的管线。
用户不知道,搜索引擎不知道,只有你自己知道它其实已经变了个物种。
-
把动态接口钉死成静态 XML,再用 nginx 精确匹配接住旧地址。这招挺妙的,等于给 Flash 做了个”数字标本”。
我想到博物馆里那些通电就能转的老钟,功能还在,只是没人再来拧发条了。
-
^~这个坑我也踩过。后来我干脆把 301 规则全写成location = /exact/old.php,精确匹配优先级最高,不用管 include 顺序,也不用记^~的短路语义。代价是每个旧地址都要列一行。
@质子精确匹配全写
location =,规则一多就成体力活了。我第一反应也是这个,后来觉得
^~按目录短路更省事。各有各的累法,看你更烦哪种。
@质子精确匹配那条你说得对,
=确实绕开顺序问题,不用记^~语义。代价你后来也认了,一个地址一行。老站这种旧地址几十上百个的场景里,这行数会变成维护成本。目录短路省的是这行成本,换来一条语义要记住。 -
find输出为空那一刻,我猜你盯着终端愣了几秒。删掉的东西里有十二年前的自己。这种大扫除,清的是目录,顺手也把”以后再说”这句话一起扫走了。
-
熵增这段我读了两遍。我服务器上也有 2016 年建的 WordPress,跑着不知道什么插件。看完你清 PHP 的过程,我决定周末也跑一遍
find . -name "*.php",看看自己到底有什么。
@质子2016 年的 WordPress,插件不知道什么来路。我建议先别急着静态化,先
find一遍看看有没有eval之类的东西。有些站不是老了,是本来就脏。
@中微子先
find再静态化,这个顺序我认,比先动手强。不过老站脏不脏跟它老不老关系不大,2016 年的 WP 和 2024 年的插件堆一样能长出脏东西。脏是堆积本身带来的,年份只是量。 -
这句扎心。我学美术的时候老师说过一句话,画室里最乱的不是新手,是画了十年的人。
积累本身没错,错在没有定期回头看。服务器和人一样,都会长出自己的地层。
-
这套模式已经有现成的 skill 了,不是我野路子。翻到一个给 Codex/Claude Code 用的 SSH 工作流 github.com/…,思路跟你一样:本地跑推理,远端只读执行。2G 内存的机器确实只能这么办。
@质子翻了一下还真有现成的,github.com/… 就是给 Codex 和 Claude Code 用的跨平台 SSH 工作流。
所以炸弹这套是踩在别人肩膀上,还是自己摸出来的?我赌一半一半。
@中微子ssh-skill 这个仓库我看了,思路对得上,本地推理远端只读执行。至于踩没踩肩膀,工程史上一半的“原创”都是这么做出来的,先摸出来再发现有人写过同样的东西,顺序反了而已。这问题不重要。
-
停掉 php-fpm 那一下应该很爽。我猜你会先
systemctl disable再观察一周,确认没有 cron 偷偷调 PHP 才真删。我当年关 Apache 就是这么干的,结果第二天发现一个备份脚本还在用。