OpenAI 的 php2shell-checker 适合做第一轮排查,但不能据此证明服务器已经安全。它会将 HTTP 响应头或 php -v 暴露的 PHP 版本,与工具内置的补丁版本阈值进行比较。因此,隐藏版本响应头可能导致结果为 UNKNOWN;而发行版通过回移植补丁修复漏洞时,也可能让已经修复的服务器看起来仍然存在漏洞。
先看懂几种结果状态
OpenAI 官方仓库会返回三种有实际意义的状态。把它们当作排查信号即可,不要视为完整的漏洞评估结果。
| 输出 | 退出码 | 它说明了什么 | 下一步 |
|---|---|---|---|
OK | 0 | 检测到的分支不低于该分支的最低修复版本 | 确认实际处理 Web 流量的 PHP 进程使用的确实是这个版本 |
VULNERABLE | 1 | 检测到的版本低于最低修复版本 | 更新承载 Web 流量的运行时,重启 Worker,然后重新执行两种检查 |
UNKNOWN | 3 | 检查器无法确认一个可识别的版本或响应 | 在服务器本地检查;不要把它当作已经修复 |
| 参数无效 | 2 | 命令本身不符合要求 | 修正命令后重新运行 |
OpenAI 提供了什么
公开的 php2shell-checker 仓库包含一个 Node.js 脚本、测试,以及两个 Codex 技能:面向可访问网站的 phpcheck-url,以及面向拥有服务器访问权限场景的 phpcheck-server。它做的是版本证据盘点,不会利用目标,也不会扫描应用代码。
按正确模式运行检查
检查公开 URL
先克隆或下载仓库,再用它提供的 Node.js 脚本检查网站:
node phpcheck.js "https://example.com"
远程检查会先发送 HTTP HEAD 请求,只有服务器返回 405 或 501 时才会退回使用 GET。它读取 X-Powered-By 和 Server 响应头,不跟随重定向,也不会检查响应正文。
CDN、反向代理、expose_php=off 或自定义服务器配置,都可能在响应到达检查器之前移除 PHP 版本信息。遇到 VULNERABLE 必须修补;如果只是缺少版本响应头,则需要通过服务器本地证据进一步确认。
检查本地 PHP 二进制文件
在安装了 PHP 的机器上运行:
node phpcheck.js --local
该命令会针对当前 PATH 中可用的二进制文件执行 php -v。但这不能证明网站使用的就是同一个二进制文件:同一台主机上可能同时存在 CLI、PHP-FPM、CGI、Apache 模块、容器或源码编译安装等不同环境。
完成本地检查后,还要确认与 Web 服务器连接的实际运行时。在容器化部署中,应检查正在运行的容器,而不是只看镜像标签。对于通过软件包安装的服务,则要检查对应的 PHP-FPM pool 或 Apache 模块,以及发行商的软件包变更日志。
安装并使用 Codex 技能
官方 README 提供了一条同时安装两个技能的 Codex 命令:
$skill-installer install both skills from https://github.com/openai/php2shell-checker
之后根据你的访问条件选择对应的接口:
$phpcheck-url check https://example.com
$phpcheck-server check the PHP version serving my site
Codex 并非必需。对于 CI 任务和极简主机,Node.js 命令仍然是最直接的接口。自动化使用时可以加上 --json;这样会通过 patch_status 字段返回 ok、vulnerable 或 unknown,脚本无需解析终端中的自然语言输出。
验证下载的检查器
该仓库使用 Node 内置的测试运行器:
node --test phpcheck.test.js
测试套件会通过本地 HTTP 服务器验证 CLI,并在本地场景中使用伪造的 PHP 可执行文件。根据 README,它既不需要真实的 PHP 安装,也不需要访问外部网站。
把 UNKNOWN 当作调试结果
应把 UNKNOWN 视为证据不完整,而不是安全结论。目标可能隐藏了 PHP 版本、使用了检查器无法识别的分支、没有正常响应,或者运行的是更新的预发布版本;这些情况都不能证明修复已经存在。
一位真实用户 @jvr0x 这样概括了这一限制:
“它只是拿版本号和 10 月 20 日的阈值做比较,所以隐藏版本响应头时,结果就会是 UNKNOWN。”
远程命令看到的是 HTTP 响应,而不是反向代理后面的实际进程。版本响应头被隐藏后,应该进行服务器端检查,而不是反复探测 URL。
遇到 UNKNOWN 后,可以按以下顺序排查:
- 在承载网站的主机上运行
node phpcheck.js --local。 - 确认请求实际到达的是 PHP-FPM、CGI、Apache
mod_php,还是某个容器。 - 查看发行商软件包的变更日志,确认是否回移植了补丁。
- 更新后重启相关的 PHP-FPM 或 Apache Worker。
- 通过实际承载请求的链路验证,而不只是检查 Shell 默认调用的
php二进制文件。
如果没有 SSH 权限,URL 模式可能就是唯一可用的第一步。如果共享主机隐藏了版本响应头,则需要让托管服务商确认实际运行时、软件包修订版本以及重启状态。
版本表并不能单独决定是否已修复
检查器将 PHP 8.5.12、8.4.27、8.3.36 和 8.2.35 视为各分支的最低上游修复版本。其官方测试确认 8.4.26 的结果为 VULNERABLE,而 8.4.27 的结果为 OK;PHP 官方则将 8.5.12、8.4.27、8.3.36 和 8.2.35 列为安全版本。
| 分支 | 最低上游修复版本 | 被判定为存在漏洞的上一版本 |
|---|---|---|
| PHP 8.5 | 8.5.12 | 8.5.11 |
| PHP 8.4 | 8.4.27 | 8.4.26 |
| PHP 8.3 | 8.3.36 | 8.3.35 |
| PHP 8.2 | 8.2.35 | 8.2.34 |
| 部署情况 | 为什么简单比较版本可能产生误判 | 应使用的证据 |
|---|---|---|
隐藏 X-Powered-By 或 Server 响应头 | 远程检查器无法识别 PHP | 本地运行时清单 |
| Debian 或 Ubuntu 安全软件包 | 发行商可能在较旧的上游版本号下回移植补丁 | 发行商公告和软件包变更日志 |
| 同一主机同时运行 PHP-FPM 和 CLI | php -v 可能描述的是 CLI,而不是处理 HTTP 请求的 Worker | Web 服务器和 FPM 配置 |
| 缓存的容器或固定的 digest | 仓库标签可能已经更新,但正在运行的容器仍然是旧版本 | 正在运行的容器镜像 ID 和版本 |
| Apache 或 FPM Worker 未重启 | 磁盘上的文件已经修复,但旧 Worker 仍在运行 | 进程启动时间和重启后的检查结果 |
| 不受支持的分支或预发布版本 | 检查器可能无法识别发行商特有的补丁状态 | PHP 或发行商的安全公告 |
仓库中的限制说明指出,该工具假设列出的分支中,后续稳定版本都包含修复,但无法独立验证补丁是否真正生效。发行版可能在不改变上游版本号的情况下应用安全回移植补丁,因此相比单独查看显示的版本号,软件包变更日志往往是更有力的证据。
更新 WordPress、Drupal、Laravel 或其他 PHP 应用,并不能修补 PHP 引擎本身。应更新运行时软件包或镜像,重启 Worker,并确认真正接受 Web 请求的进程已经使用修复后的版本。
常见问题
哪种输出可以关闭生产环境的修复工单?
不能仅凭任何一种输出关闭工单。即使结果为 OK,也应同时确认实际使用的 SAPI、发行商回移植补丁状态、所有相关虚拟主机或容器,以及打补丁后 Worker 确实已经重启。