vulnerabilities rfc1867 callback uaf

深入 PHP 核心 UAF 机制:RFC1867 上传进度回调悬空指针全景剖析

php2shell.run Research Lab •

深入 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 上下文时,内存灾难便会发生:

  1. Session 重新读入: 底层触发 session_reread() 或 zend_hash_update(),旧条目被新条目替换;
  2. 析构释放底层内存: 哈希表内部的析构函数 zval_ptr_dtor 介入,将旧 zval 占用的内存块归还(FREE)给 Zend 内存管理器(Zend MM);
  3. 指针未置空造成悬空: 全局缓存指针 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
=================================================================

这充分证明:未清空的全局悬空指针在会话刷新时直接读取了已被释放的内存块,攻击者只需通过堆喷填充受控数据,即可将该非法读取引导至任意代码执行。