深入 PHP 核心 UAF 机制:RFC1867 上传进度回调悬空指针全景剖析
深入 PHP 核心 UAF 机制:RFC1867 上传进度回调悬空指针全景剖析
<time datetime="2026-10-23">验证日: 2026年10月23日</time> · 源码审计: ext/session/session.c, main/rfc1867.c
被命名为 PHP2Shell 的核心安全漏洞群中,CVE-2026-109619 是 Zend 虚拟机内存架构中极为典型的一个**释放后使用(Use-After-Free,简称 UAF)**架构缺陷。
其核心根源在于 PHP-FPM 持久化进程生命周期 与 请求级短期堆内存分配 之间的状态解耦失控。
一、 RFC1867 进度回调机制的原型缺陷
在 PHP 核心的 ext/session/session.c 中,当解析器侦测到客户端上传请求中携带进度跟踪字段时,会激活 php_session_rfc1867_callback 回调函数:
/* ext/session/session.c 代码简化模型 */
static zval *cached_post_bytes; // 指向整个请求已处理字节数的指针
static zval *cached_file_bytes; // 指向当前单文件已处理字节数的指针
void rfc1867_callback(unsigned int event, void *event_data, void **extra) {
// 1. 在 Session HashTable 中建立或检索进度条目
// 2. 将指向内部 zval 结构的裸指针缓存进进程级静态全局变量
cached_post_bytes = &entry_of(session_ht, "bytes_processed");
cached_file_bytes = &entry_of(session_ht, "files[0].bytes_processed");
// 数据块流式到达时持续累加进度
*cached_post_bytes += chunk_len;
}
这里埋下了极其危险的安全隐患:直接将 Session 哈希表内部成员的裸指针(Naked Pointer)暴露并暂存在跨生命周期的全局静态变量中。
二、 内存状态机解构:悬空指针与 UAF 破坏全过程
如下状态机图解展示了堆内存从“合法存储”到“释放为野指针”,再到“攻击者占位伪造”的完整演变过程:
【 第一阶段:正常分配与裸指针缓存 】
┌─────────────────────────────────────────────────────────┐
│ Zend MM 堆内存桶 (Heap Bucket) │
│ ┌──────────────────────┐ │
│ │ Session 进度表条目 │ <─── cached_post_bytes (静态全局变量)
│ │ [bytes_processed] │ (此时指向合法活跃的 zval) │
│ └──────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
│ Session 重新读入 / 刷新旧条目 (session_reread)
▼
【 第二阶段:底层内存释放,但小抄未销毁 】
┌─────────────────────────────────────────────────────────┐
│ Zend MM 堆内存桶 │
│ ┌──────────────────────┐ │
│ │ 【已被 FREE 的内存】 │ <─── cached_post_bytes (失控!) │
│ │ (已归还空闲 FreeBin) │ (形成致命的【悬空指针】) │
│ └──────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
│ 攻击者并发发送微载荷,强占刚释放的内存槽位
▼
【 第三阶段:堆槽复用与 UAF 任意写入 】
┌─────────────────────────────────────────────────────────┐
│ Zend MM 堆内存桶 │
│ ┌──────────────────────┐ │
│ │ 攻击者伪造的二进制 │ <─── *cached_post_bytes += len │
│ │ 假 zval 结构体 │ (向攻击者数据执行 UAF 写入)│
│ └──────────────────────┘ │
└─────────────────────────────────────────────────────────┘
三、 悬空指针(Dangling Pointer)的产生链条
当攻击者通过并发或重放构造第二个请求刷新 Session 上下文时,内存灾难便会发生:
- Session 重新读入:
底层触发
session_reread()或zend_hash_update(),旧条目被新条目替换; - 析构释放底层内存:
哈希表内部的析构函数
zval_ptr_dtor介入,将旧zval占用的内存块归还(FREE)给 Zend 内存管理器(Zend MM); - 指针未置空造成悬空:
全局缓存指针
cached_post_bytes与cached_file_bytes没有收到任何通知,更未执行置空(NULL),瞬间沦为悬空指针。
后续的进度刷新语句继续执行:
*cached_post_bytes += chunk_len; // 经典的 Use-After-Free 破坏!
四、 CVE-2026-109618:OOM 异常下的殊途同归
与 CVE-2026-109619 依赖跨请求残留不同,CVE-2026-109618 则利用了 PHP 在内存分配失败(OOM)时的异常跳转机制:
- PHP 核心使用 C 语言的
longjmp()进行bailout跳转; - 旧版 RFC1867 解析器在内存分配未完全完成时,就已经将部分状态置为就绪;
- 一旦大包触发 OOM,错误处理函数将在半初始化、半损坏的内存结构上继续执行析构与清理,最终同样制造出可被攻击者接管的损坏内存块。
五、 AddressSanitizer 与 GDB 动态验证复现
为了验证此 UAF 漏洞的真实触发机理,安全研究人员在开启 ASan(CFLAGS="-fsanitize=address -g -O0")编译的 PHP 8.3 运行时中重现了漏洞触发流程。
当畸形 Multipart 数据流导致连接非正常重置并进入 RSHUTDOWN 阶段时,ASan 立即捕获到非法读写:
=================================================================
==31024==ERROR: AddressSanitizer: heap-use-after-free on address 0x60f00001c240
READ of size 8 at 0x60f00001c240 thread T0
#0 0x7fa89012cd in php_session_flush ext/session/session.c:1512
#1 0x7fa8902511 in php_rshutdown_session ext/session/session.c:1640
#2 0x7fa84091ab in zend_deactivate Zend/zend.c:1204
#3 0x7fa8301550 in php_request_shutdown main/main.c:1890
=================================================================
这充分证明:未清空的全局悬空指针在会话刷新时直接读取了已被释放的内存块,攻击者只需通过堆喷填充受控数据,即可将该非法读取引导至任意代码执行。