一个自包含、OOP 风格的 PHP 概念验证,用于演示并差异验证 Composer 的 Perforce 仓库驱动中的命令注入漏洞。
该 PoC 在两个 Composer 二进制文件上运行相同的恶意 composer.json——一个受影响版本(2.9.5)和一个已修复版本(2.9.6)——并通过观察副作用(由注入的 shell 命令写入的标记文件)来证明该 bug,该副作用在受影响版本上触发,而在已修复版本上不会触发。
⚠️ 仅供授权安全研究与防御性测试使用。 参见负责任使用。
| CVE | CVE-2026-40176 |
| 组件 | Composer — Perforce(perforce)仓库/VCS 驱动 |
| 类型 | 通过攻击者控制的仓库 URL 进行操作系统命令注入 |
| 攻击面 | 一个 composer.json,其中包含恶意构造的 repositories 条目(type: perforce) |
| 受影响版本 | Composer 2.9.5 |
| 已修复版本 | Composer 2.9.6 |
| 触发条件 | 针对恶意清单解析/更新依赖(composer update) |
| 影响 | 在运行 Composer 的机器上执行任意命令 |
| PoC 语言 | PHP(单文件,无外部依赖) |
Composer 可以从多种版本控制系统中解析包。对于 Perforce,仓库通过 p4:// URL 标识,该 URL 编码了主机、端口和用户/流。当 Composer 的 Perforce 驱动构建底层 p4 命令行时,取自攻击者控制的 URL 的字段在交给 shell 之前未经过充分清洗。
由于清单作者完全控制仓库 URL,攻击者只要能诱使受害者针对恶意 composer.json 运行 composer update/composer install(例如,被投毒的依赖、恶意仓库,或处理不可信项目文件的 CI 作业),就能突破预期的 p4 调用,并以 Composer 进程的权限执行任意操作系统命令。
这属于历史上的 Composer VCS 驱动参数注入问题同一家族:URL/分支/流值未经转义就流入 shell 命令。Composer 2.9.6 加固了 Perforce 驱动,使注入的载荷不再执行。
此处所演示行为的权威描述是 PoC 源码本身(
CVE202640176Test.php);上游修复细节请查阅官方公告和 Composer 变更日志。
该 PoC 是一个单独的类 CVE202640176Test,执行受控的 A/B(差异)实验:
2.9.5)和已修复(2.9.6)两个 Composer 二进制文件的 --version,如果任一无法调用则提前中止。composer.json,其 repositories 部分包含一个 perforce 条目,该条目带有携带注入 shell 载荷的恶意 p4:// URL。composer update。finally 块始终恢复项目目录中原始的 composer.json。PASS。对于每次运行,validateRun() 检查三件事:
| 检查项 | 证明什么 |
|---|---|
| 标记文件存在且包含运行 ID | 注入的 touch/echo 载荷确实执行了——即命令注入成功。 |
Composer 输出提到 p4 | 到达了 Perforce 驱动代码路径(载荷由正确的组件处理,而非某个无关步骤)。 |
| 解析出的 Composer 版本 == 预期值 | 运行的是正确的二进制文件(2.9.5 对比 2.9.6)。 |
只有三项全部通过,一次运行才算“OK”。当受影响版本运行为 OK 且已修复版本运行不为 OK 时,整体测试通过——这是随后被修补的真实漏洞的精确特征。
恶意仓库 URL 在 writeComposerJson() 中构建:
p4://127.0.0.1:1666:attacker_user;touch <marker> && echo '<runId>' > <marker>:client_test
分解如下:
p4://127.0.0.1:1666:attacker_user — 一个看起来格式正确的 Perforce URL(主机、端口 1666、用户)。;touch <marker> && echo '<runId>' > <marker> — 注入的 shell 命令。开头的 ; 终止预期的 p4 命令;touch 创建标记文件,echo '<runId>' > <marker> 将唯一的运行 ID 写入其中,以便 PoC 确认该文件是由载荷(而非某个无关进程)产生的。:client_test — 尾部文本,使其余 URL 的解析保持合理。在受影响驱动上,shell 元字符会被执行,标记文件被创建。在已修复驱动上,该值被正确转义/引用,因此相同的字符串被当作惰性数据处理,不会出现标记。
注意:PoC 使用唯一的、带时间戳的运行 ID,并将其标记写入隔离的临时目录中,因此载荷是良性的且可自清理,而非破坏性的。
2.9.5(受影响)2.9.6(已修复)exec() 运行 cd … && php …)。为 Linux/macOS 设计。composer.json(启动时读取,复制到每次临时运行中,并在之后恢复)。通常你不需要实时的 Perforce 服务器:漏洞在于 Composer 如何构建
p4命令行,而注入的载荷会在任何真实p4连接之前/周围运行。Composer 可能会记录 Perforce 连接错误——这是预期的,不影响标记文件证明。
克隆/放置 PoC 到工作目录。
提供一个 composer.json,放在与 PoC 相同的目录中。最小化即可:
{
"name": "research/cve-2026-40176-poc",
"description": "Base manifest for the CVE-2026-40176 differential PoC",
"require": {}
}
获取两个 Composer 二进制文件并放置到 PoC 期望的位置(默认路径如下):
/usr/local/bin/composer-2.9.5.phar # affected
/usr/local/bin/composer-2.9.6.phar # fixed
你可以从官方归档下载特定的 Composer 版本,例如:
curl -Lo /usr/local/bin/composer-2.9.5.phar https://getcomposer.org/download/2.9.5/composer.phar
curl -Lo /usr/local/bin/composer-2.9.6.phar https://getcomposer.org/download/2.9.6/composer.phar
如果你的路径不同,请编辑
CVE202640176Test.php底部的两个构造函数参数。
php CVE202640176Test.php
测试框架依次运行两个 Composer 版本并打印最终结论。即使某次运行失败,原始的 composer.json 也会自动恢复(所有工作都在一次性的临时目录中完成)。
该仓库附带一个容器化实验环境,可精确复现环境:PHP CLI 运行时,加上 PoC 期望路径下的两个固定版本 Composer,运行时完全网络隔离。
docker compose run --rm poc
这将构建 cve-2026-40176-lab:latest(在构建期间下载 Composer 2.9.5 和 2.9.6,并验证各自的 --version),并在一个无特权、无外部出口的容器中运行差异测试。
该实验环境保证:
poc 服务运行在 internal 桥接网络上(无主机/互联网出口),启用 cap_drop: ALL 和 no-new-privileges。注入载荷保持受限。通过构建参数重新固定版本(必须与 PoC 中的两个构造函数路径保持同步):
docker compose build --build-arg COMPOSER_AFFECTED_VERSION=2.9.5 --build-arg COMPOSER_FIXED_VERSION=2.9.6
可选——实时 Perforce 服务器。 full-lab 配置文件下提供了一个 p4d 服务(docker compose --profile full-lab up)。基于标记的证明不需要它;它供想要实时 p4:// 端点的研究人员使用。注意,PoC 载荷的目标是 127.0.0.1:1666,因此要通过单独的 p4d 容器路由,需要将 PoC URL 指向主机 p4d。
如实告知: 针对真实的、已发布的 Composer
2.9.5和2.9.6,该 PoC 目前不会触发,实验环境报告INCONCLUSIVE / FAIL。
对受影响的 Composer(2.9.5)运行 PoC 的恶意清单,会在 Composer 内部、在构建任何 p4/shell 命令之前抛出异常:
In PerforceDriver.php line 40:
[ErrorException]
Undefined array key "depot"
PerforceDriver::initialize() 首先读取 $this->repoConfig['depot'],但 PoC 的仓库条目只提供了 type 和 url(没有 depot 键)。驱动在该点中止,因此 URL 中注入的 ;touch <marker> 载荷永远不会被执行,也不会创建标记。网络隔离不是原因——在完全网络出口的情况下也会出现同样的错误。
这意味着什么:
INCONCLUSIVE 结果是 PoC 载荷的属性,而非环境的属性。depot 键(实际上还需要 full-lab 配置文件下的实时 p4d 端点)。把载荷细化到那一步属于“搭建实验环境”之外的漏洞利用开发,此处有意将其排除在范围之外。下面的“预期输出”是 PoC 的预期/理想化结果,保留供参考;它不是当前载荷针对真实驱动所产生的结果。
一次成功的演示大致如下(路径和 ID 会有所不同):
=== CVE-2026-40176 PoC started ===
- Composer 2.9.5 version: 2.9.5
- Composer 2.9.6 version: 2.9.6
Prepared temp dir: /tmp/cve20264176_5_20260610_142233
Written malicious composer.json to /tmp/cve20264176_5_20260610_142233
Running Composer in /tmp/cve20264176_5_20260610_142233…
- Parsed Composer version: 2.9.5
- Marker /tmp/cve20264176_5_.../poc_marker_5.txt created with expected ID.
- Output shows Perforce driver activity.
- Affected run exit code: 1
Prepared temp dir: /tmp/cve20264176_6_20260610_142233
Written malicious composer.json to /tmp/cve20264176_6_20260610_142233
Running Composer in /tmp/cve20264176_6_20260610_142233…
- Parsed Composer version: 2.9.6
✘ Marker file /tmp/cve20264176_6_.../poc_marker_6.txt not found.
- Output shows Perforce driver activity.
- Fixed run exit code: 1
=== CVE-2026-40176 PoC finished ===
=== TEST RESULT: PASS (affected succeeded, fixed failed) ===
Composer 退出码非零是正常的——composer update 最终无法获取(伪造的)包。证据是标记文件,而不是 Composer 的退出状态。
| 结果 | 含义 |
|---|---|
| PASS (affected succeeded, fixed failed) | 已确认:2.9.5 执行了注入的命令,2.9.6 没有。漏洞及其修复均被复现。 |
| INCONCLUSIVE / FAIL | 一项或多项检查未对齐。检查每次运行的 ✓/✗ 行:二进制路径错误、版本不匹配、受影响版本标记缺失(环境/转义差异),或已修复版本运行意外创建标记。 |
结果不确定的常见原因:
PerforceDriver.php:40 处以 Undefined array key "depot" 中止 — 仓库配置缺少 depot 键,因此 Composer 永远不会到达 p4 命令构造路径。这正是 PoC 当前载荷针对真实 2.9.5/2.9.6 时发生的情况(参见复现状态(实测))。2.9.5 / 2.9.6。exec() 被沙箱化/禁用。p4(未到达驱动路径)。