Skip to content

About

php2shell (CVE-2026-109618/109619) 利用工具:带来源与日期的指纹库、两条引擎、免 gdb 标定;含源码级结论——本链下限 PHP 7.0。

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

php2shell 利用链概览

php2shell

CVE-2026-109618 / CVE-2026-109619 — PHP 预认证 RCE 利用、构建指纹与检测工具

CVSS v4.0 9.2 Pre-authentication attack surface Affected PHP branches Fixed PHP versions This chain requires PHP >= 7.0 Verified on four builds across two engines

MIT No third-party dependencies No nm / readelf / pip requirements v1.0.0

中文 · English · 出处与许可 · docs

快速开始 · CVE 与影响范围 · 利用原理 · 指纹数据 · 利用条件 · 实测状态 · 可靠性 · 退出码 · 检测与缓解 · 出处


项目概览

本项目聚焦 session.upload_progress 请求预初始化路径中的 OOM 状态破坏,提供本地靶场利用、构建指纹匹配、布局测量和检测工具。利用结果与 PHP 构建、SAPI、glibc 和 profile 数据有关;仓库会区分已验证的 RCE、仅复现内存破坏和仍待测量的情况。

  • exploit/php2shell.py:利用与研判驱动,支持 pure Python 引擎和 eness 引擎;
  • data/offsets.db.json:按构建记录的指纹库,包含来源、适用环境和验证状态;
  • tools/calibrate_layout.py、tools/scanmem.php:免 gdb 的布局测量工具;
  • lab/、bench/、scan/:可重复靶场、复现脚本、被动检测与配置审计。

快速开始

以下示例只针对本机靶场。利用请求可能造成 worker 崩溃,不要对未获授权的系统运行。

./lab/run_target.sh start php:8.4.26-apache 18110 p2s stock

python3 exploit/php2shell.py http://127.0.0.1:18110/index.php \
  --command 'id>/out/p1' --proof-file out/p1 --marker uid=
candidate under test (unproven) :: x86-64-php_8.4.26_apache#pure  status=RCE-VERIFIED-HERE
    confirmed only once the leak's page-offset test passes
profile confirmed by the target's own leak :: x86-64-php_8.4.26_apache#pure
    libphp=0x7fc594f08000 arena=0x7fc592e00000 delta=0x2108000 (positional page-offset test passed)
leak picture │ decoded=819200B ptrs=8380  cluster ███████████░ 83/87  delta │···◂·······│ 0x2108000 ∈ [0x1000000..0x4000000]
STAGE A2 + B2 :: main=0x7fc592e00000 S=0x7fc592ef8398 zif_system=0x7fc59536e370
CHAIN COMPLETE :: proof out/p1 (owner uid 33): uid=33(www-data) …
VERDICT: REMOTE CODE EXECUTION CONFIRMED

两段措辞的分工是刻意的:candidate under test (unproven) 表示"数据库里有个候选,还没被这个目标证明"; 只有目标的泄露通过位置式页偏移测试之后,才会打印 profile confirmed by the target's own leak。 所以任何一次运行里都不会出现"装作匹配成功"——架构判错时会显示按证据改选, 无法证明架构时(远程且无二进制)就只报候选、不报匹配。

默认流程会从目标响应推断架构并匹配候选 profile。命中情况和阶段判据会写入终端;RCE 结果需要通过证据文件或带外通道确认。

CVE 与影响范围

CVE GHSA CVSS v4.0
CVE-2026-109618 GHSA-w8hw-w9fx-j79w 9.2
CVE-2026-109619 GHSA-6wmf-4rv5-p3rp 9.2
PHP 分支 受影响版本 修复版本 本项目实测状态
8.2 8.2.0–8.2.34 8.2.35 内存破坏可复现;该族布局尚未测量
8.3 8.3.0–8.3.35 8.3.36 内存破坏已复现;pure 链无越界读,eness 链泄露成立、stage 3 缺载体常量
8.4 8.4.0–8.4.26 8.4.27 x86-64 RCE 已验证;aarch64 泄漏成立,缺少 s_off
8.5 8.5.0–8.5.11 8.5.12 RCE 已验证(8.5.11 x86-64,eness 引擎及其前提,连开三次);pure 链在此配置下生产者根本不 OOM

修复提交:php-src@3584a98。上游将相关漏洞路径追溯至更早的 PHP 版本;本仓库当前利用链要求 PHP 7.0 及以上,实际适用范围仍取决于构建与 SAPI。

利用原理

session.upload_progress 使会话解码发生在 RINIT 内部、用户脚本之前 (php_hash_environment() → sapi_handle_post() → rfc1867_post_handler() → php_session_rfc1867_callback() → php_session_initialize() → php_session_decode())。 此阶段若分配器 OOM,修复前的实现以 longjmp 跳转至请求收尾,跳过成对的清理逻辑, 结果是 PS(id)、BG(serialize)、BG(unserialize) 仍指向已释放的请求内存。

随后的请求收尾会调用 php_session_send_cookie(),其内部 php_url_encode(ZSTR_VAL(PS(id)), ZSTR_LEN(PS(id))) 直接采用已释放 zend_string 头中的 长度字段,从而形成未授权的堆越界读,内容经 Set-Cookie 回显 —— 这一步在不读取任何 本地文件的前提下解决 ASLR。将伪造结构放入 arena 后,pDestructor = zif_system、 arData 指向命令串,zend_hash_destroy() 在 shutdown 阶段执行命令。由于执行发生在响应 发出之后,结果需通过产物文件或带外通道确认,不存在天然回显。

补丁 3584a98bda0f(Arnaud Le Blanc,2026-10-18)包含两项关键变更:在早读取之前将上述 全局清零(新增 php_basic_early_rinit(),由 main.c 调用),以及取消 OOM 路径上的 longjmp(软 OOM 在 VM 安全点处理,硬 OOM 直接 _exit())。我们在已修复的 8.4.27 上 验证了后者的可观测后果:相同生产者请求下,目标日志中预用户态 OOM 计数为 0。

目录结构

按职责划分:驱动、数据、测量工具、靶场、文档、证据分离;第三方代码集中放置并在 NOTICE 中标注。

php2shell-exploit/
├── exploit/
│   └── php2shell.py            利用与研判驱动(标准库实现,不要求 nm/readelf)
├── data/
│   ├── offsets.db.json         指纹库,schema 2:一个构建一条 profile,引擎作为子项
│   ├── offsets/*.env           社区互操作格式(9bie 的 offsets.py 写、eness PoC 读)
│   └── profiles/*.json         自标定回写的 profile(含实测 delta、指纹、出处)
├── tools/
│   ├── elf_syms.py             零依赖符号提取:ELF64/Mach-O64,stripped 走重定位
│   ├── macho_syms.py           macOS 符号读取(上游同名工具的清单只认 ELF 裸符号名)
│   ├── php_struct_offsets.c    以目标同版本头文件量结构体偏移
│   ├── harvest_offsets.py      符号批量采集:镜像或包 -> 提取 -> 写库并产出 offsets.env
│   ├── calibrate_layout.py     布局测量:挂起请求 + 检索预置标记,无需 gdb
│   └── scanmem.php             上一步使用的全子进程 /proc/<pid>/mem 扫描器
├── lab/
│   ├── run_target.sh           靶场拉起:单 worker mod_php,两种配置(stock / eness)
│   ├── cloud_matrix.sh         云机验证矩阵:一镜像一容器,逐格执行自动模式
│   ├── harvest.sh              批量采集与逐格记账
│   ├── repro.sh                内存破坏复现,含 6 组配置对照
│   ├── serve.sh                仅启动服务,用于手工验证
│   ├── instrument.sh           gdb 控制流验证(拒绝无证据的假阳性结论)
│   ├── arm64_calibrate.py      aarch64 指纹与 delta 测量
│   ├── asan_repro.sh           ASan 构建下的根因复现
│   └── offset_probe.py         结构体与运行时偏移探针
├── bench/                      靶场配置:apache.conf(MaxRequestWorkers=1)、php.ini、www/
├── upstream/pure/              被调用的载荷生成器(原样收录;请求形状本身属于标定内容)
├── engines/eness_poc.py        第二引擎(vendored,stage-based)
├── scan/
│   ├── detect.py               被动研判:版本与行为,不终止 worker
│   ├── audit.sh                主机侧 php.ini 姿态审计
│   └── nuclei-*.yaml           官方被动模板
├── docs/
│   ├── 01-root-cause.md        根因:预初始化消费、OOM longjmp、补丁改了什么
│   ├── 02-exploitation.md      利用:阶段、判据、观测特征、载荷形态
│   ├── 03-fingerprints.md      指纹库结构、三类常量、三条实测规律、互操作
│   ├── 04-reliability.md       可靠性:实测序列、命中率、被排除的错误解释
│   ├── 05-detection-and-mitigation.md  检测特征、规则、缓解、修补后验证
│   └── 06-limits.md            边界:为什么不能承诺"任意站点"
├── evidence/                   跑出来的原始记录(矩阵表、复现对照、审计与研判回执、崩溃报告)
├── LICENSE  NOTICE.md  README.md  README.en.md
└── assets/                     封面 cover.png;social-preview.svg(1280×640,可上传到 About → Social preview)

自动识别与实测输出

架构不需要你告诉它:版本+架构在库里有多行时,工具先发一对「生产者+泄漏」, 按泄漏指针的取值范围判定架构(0x7f… ⇒ x86-64,0xffff… ⇒ aarch64), 再据此过滤候选行。此前这一步靠人工传 --candidates,漏传就会把 aarch64 的行套到 x86-64 靶上,表现为每发都 no-page-offset-match —— 看着像"突然打不动了",其实是选错行。

未指定 profile、偏移或版本号时的实测输出(原始记录见 evidence/):

ABI from leaked pointers :: decoded=819200B families=[x86-64:8755]
runtime profile selected (proven by the target's own leak) ::
    x86-64-php_8.4.26_apache#pure php=8.4.26 status=RCE-VERIFIED-HERE
    libphp=0x7f058055c000 arena=0x7f057e200000 delta=0x235c000
  [>] target ABI :: 8.4.26 / debian / x86-64
  [>] SAPI boundary :: Apache prefork/mod_php
  [>] selected payload :: x86-64-php_8.4.26_apache#pure
CHAIN COMPLETE :: proof out/p1 (owner uid 33): uid=33(www-data) …

架构判定不依赖 banner,而来自泄漏数据中指针的取值区间(0x7f… 对应 x86-64 glibc 映射, 0xffff… 对应 aarch64),因此 expose_php=Off 时同样有效。profile 的选定也不依赖信任: 每个候选都需通过开火阶段使用的同一组算术判据 —— 窗口内指针的页偏移与候选 zval_ptr_dtor 相同、据此推出的模块基址满足 4KB 对齐、且模块之下存在 2MiB 对齐的分配器 区域。不匹配的候选由目标自身否掉。

三类常量的获取方式

常量 参照系 获取方式
zval_ptr_dtor、zif_system 文件内偏移,与 ASLR 无关 tools/elf_syms.py 解析 ELF64/Mach-O64;包被 strip 时改由函数注册表与 R_*_RELATIVE 重定位恢复(x86-64 与 aarch64 均已验证)
运行时基址 每次 worker 重启后变化 由链上的越界读给出
s_off、hsv_off、泄漏几何 构建相关 tools/calibrate_layout.py:保持请求处于处理中状态,遍历全部子进程的 /proc/<pid>/mem 检索预置标记;无需 gdb

符号侧的获取成本恒定且可批量执行;布局侧需要按构建族实测。因此覆盖率的扩展方式不是 "遍历所有版本",而是"每个族量一次几何、每个构建读一次符号"。

三条实测结论

  1. 符号随补丁级别变化:8.4.25 为 0x65dc60,8.4.26 为 0x65eb60,8.4.27 为 0x66a620。 已发布 profile 亦会漂移:devploit 记录 8.4.26-bookworm 的 dtor 为 0x65eb60, 而当前同名镜像中提取到的是 0x64eb60。因此规则确定为:目标二进制优先于任何表值。
  2. 布局常量可在同一发行版构建族的相邻补丁级别之间复用,但不跨 minor 版本,也不跨 glibc 代际。8.4.25 一格使用的是 8.4.26 的几何;guaidao2 独立记录了同一 PHP 版本在 bookworm(glibc 2.36)可对齐、在 trixie(glibc 2.41)不可对齐。
  3. 布局观测必须在同一进程代际内完成。已发生崩溃的容器中测得的常量回喂后不产生 shell。 这一点决定了扫描必须作为利用流程内的阶段(--measure-layout)实现,而非独立脚本。

两个引擎

单一流程,两个引擎,均为社区实现。

pure(本项目实现) eness(vendored)
配置前提 stock php.ini-production memory_limit<=8M、display_errors=On、max_input_vars=1000
已验证构建 8.4.26、8.4.25(x86-64) 8.4.26-bookworm、8.5.11(x86-64,本仓库复现);上游另报 3 条待复采
引擎选择依据 生产者响应 500 且无正文 响应含 Allowed memory size of N bytes,同时披露有效限额

选择由 --auto 完成。派发到 eness 时,符号由本项目提取后经 --zif-offset/--dtor-offset/--htdata-offset 注入 engines/eness_poc.py,其结果按本项目的 判读口径统一上报。

利用条件

  • PHP ≥ 7.0。 这是源码事实而不是覆盖范围:泄露件伪造的是 ZSTR_LEN(PS(id)) (7.0 起 php_session_send_cookie 才用 ZSTR_LEN;5.x 是 strlen(PS(id)),读到下一个 NUL 为止,长度不可伪造),终端件用的是 7.x/8.x 的 packed zend_array 布局 (nTableMask = -nTableSize + arData 连续桶;5.x 是 arBuckets/pListHead/nApplyCount)。 漏洞面本身可追到 5.4.0(MULTIPART_EVENT_FORMDATA 在 5.6 也无条件接受 body 里的 PHPSESSID),所以 PHP < 7.0 判 UNSUPPORTED_CHAIN,不是"未受影响"。
  • 未修复版本;session.upload_progress.enabled=1(默认);持久 worker 的 mod_php。 Server: LiteSpeed 场景下首个请求即退出,不应预期 RCE。
  • memory_limit 必须有限。注意 post_max_size 只丢弃带声明 Content-Length 的请求: 我们的生产者用无长度(chunked)请求,因而不受它限制——早先"限额须低于 post_max_size" 的说法来自探针而不是链,已更正(依据见 docs/02)。
  • 前置不得改写或丢弃 Set-Cookie,且需放行 multipart 与不含 Content-Length 的 chunked 请求。
  • 生产者与消费者需落在同一 worker。N 个 worker 时单次配对命中概率约为 1/N, 由 attempts 信封承担。靶场固定 MaxRequestWorkers=1 属实验条件, 多 worker 环境的命中率尚未统计。
  • 需要一条结果确认通道:docroot 内可写且可访问的路径,或可用的带外通道(HTTP / DNS)。

实测状态

  • 真实云主机(x86-64)上的 8 格矩阵,原始表见 evidence/cloud-matrix.tsv: 2 格 RCE、5 格内存破坏、1 格正确判定为已修复。
  • 符号覆盖 7.4.33 … 8.5.12,含 amd64、aarch64 以及一个 musl 静态构建(fpm-alpine, 符号取自 /usr/local/bin/php)。三处数值与上游公布结果完全一致,构成互相验证: devploit 的 8.5.11-aarch64,guaidao2 与 eness 的 8.4.26-bookworm 常量组。
  • 8.3.35 的失败位置已经定位清楚:生产者每发都返回 500(确实 OOM),但泄漏侧页号在 flood ∈ {511,255,640} × forge ∈ {0x137,0x130,0x140} 内全部返回 decoded=26B (未越界)⇒ 不是常量填错,而是 8.3 上悬挂 id 所在 chunk 落在别处,即泄漏形状本身不跨 minor 复用。 工具的失败诊断会区分"生产者未 OOM"与"生产者已 OOM 但泄漏形状不符"两种情形,并把 A1 状态码连同每个组合一起打出来。
  • 8.3.35 x86-64(真实云机)两条链分别的结论,避免读成互相矛盾:
    • pure 链(stock 128M):生产者 OOM 成立,但读回来的仍在界内 ⇒ 泄漏形状不跨 minor 复用;
    • eness 链(8M + display_errors=On):泄露侧完全成立 —— N=841 由泄露自身报名、 越界读出 chunk=0x7ff3e1600000、页偏移测试给出 libphp=0x7ff3e36c0000; stage 3 的载体常量不在已知集合内,所以这一格停在内存破坏(DoS 级),没有假装 RCE。
  • 尚未闭合:8.2 的布局测量;aarch64 上 pDestructor 未出现在窗口 +8 处 (判据 no-page-offset-match),推测为伪造几何随架构移动;hsv_off 的自量方法尚未验证。
  • 上游声明可 RCE 的构建里,8.5.11 x86-64 与 8.4.26-bookworm x86-64 已由我们复现 (PROOF: uid=33(www-data));其余仍属上游声明,单次抽样未落地时记为待复采,不作否证。

可靠性与代价

单次生产者请求即可能终止一个 worker,在生产环境中表现为一次 5xx 或超时。为此工具输出 可审计的执行账本:

candidate                          verdict          chunk base       delta       fp ms
x86-64-php_8.4.26_apache#pure      LEAK_FAILED      -                -           0 1522
x86-64-php_8.4.26_apache#pure      RCE_CONFIRMED    0x7f0ef7800000   0x1cd8000  83 5140
SUMMARY candidates=1 proofs=1 misses=0

--sweep 按状态等级顺序尝试同一 ABI 下的全部候选;前两名获得完整的 --attempts 预算, 其余各两次。将预算均匀分配至 16 个候选时,正确候选反而因尝试不足而未命中 —— 该策略由此调整。

判据一律取自架构级不变量:模块映射页对齐、glibc arena 2MiB 对齐且位于模块之下、指针页偏移 与候选 dtor 相同。特定构建上观察到的经验值(众数指针身份、桶计数 87、 0x1A00000..0x1F00000 的 delta 窄带)仅作为 profile 提示保留:同一镜像在不同 ASLR 世代下 实测 delta 为 0x1bb7000 / 0x1c6a000 / 0x1cd8000 / 0x235c000 / 0x247a000。

检测与缓解

python3 scan/detect.py https://SITE/
nuclei -t scan/nuclei-CVE-2026-109618.yaml -u https://SITE/
bash scan/audit.sh

最具判别力的网络特征是异常长的 Set-Cookie: PHPSESSID=(可达数百 KB,且包含百分号转义的 不可打印字节)。缓解以升级为准;过渡期可关闭 session.upload_progress,8.5 分支配合 fatal_error_backtraces=0。memory_limit=-1 不构成缓解,它只是移除了本链的触发条件。 检测规则与验证方法见 docs/05-detection-and-mitigation.md。

日志与配色

--color always 是为了 ssh host '...' 这种场景:没有 tty 时旧的 not isatty() 规则会把整个 调色板丢掉,看起来"没有颜色"。现在三种取值 auto|always|never,也认环境变量 P2S_COLOR。

除时间戳日志外,每次运行额外给两行可视化:

leak picture │ decoded=819200B ptrs=8380  cluster ███████████░ 83/87  delta │···◂·······│ 0x1ccb000 ∈ [0x1000000..0x4000000]
chain │ ✓recon(8.4.26) ✓producer(oom) ✓leak(arena=0x7f3c0ca00000) ✓layout ✓terminal
  • cluster 是实测桶指针数与 profile 期望值的比值条;
  • delta 是本次测量值落在允许带里的位置(不是"通过/不通过"两个字母,看得出离边界多远);
  • chain 一行显示六个阶段各自走到哪一步:绿 ✓ 通过、红 ✗ 失败、黄 ◐ 待定、灰 · 未执行。 失败时这一行就是"卡在哪"的定位器,不用往上翻日志。

判定与退出码

码 结论表述
0 仅表示拿到了执行证据(附 proof 与 --json 回执);越界读成立但未开火不给 0
2 内存破坏(DoS 级):崩溃成立,且泄露本身也没形成,不应表述为未受影响
2↔5 区分方式:泄露形成但几何不符时报 5 并附具名理由(如 dtor-pointer-not-in-window),因为下一步动作是「去量这个构建」,而不是「这台打不了」
4 未受影响 / 被前置拦截 / 请求不可达(PATH_NOT_REACHABLE 也归此类,与"破坏"区分)
5 漏洞已证、未取得执行:越界读成立但 profile 缺 s_off/hsv_off、无可匹配行、或已标定而本轮未落地
6 漏洞面存在但本链不适用(PHP < 7.0:ZSTR_LEN 不可伪造、packed 布局不同)

失败原因均具名,尤其区分以下两项,其处置方式不同:leaked-our-own-flood (越界读成立但内容为自身洪泛数据,属生产者状态未形成,与二进制无关,可重跑)与 no-page-offset-match(候选 .so 与目标构建不符)。

致谢与来源

机制、载荷形状、常量与标定方法都来自社区,逐条出处(含链接、上游当前许可状态、 我们收录时的 commit)见 NOTICE.md。摘要:

用途 来源 许可
pure 引擎的载荷生成器与 chunked 传输(upstream/pure/,逐字收录) 9bie/php2shell-bypass-aslr-exp 上游未附 LICENSE
eness 引擎(engines/,逐字收录) eness/php2shell-CVE-2026-109618-CVE-2026-109619-PoC 上游未附 LICENSE
按构建的 profile 数据 + 标定算法(--auto-calibrate 的过程) devploit/php2shell MIT
免 gdb 的载体偏移测量(扣尾 + /proc/<pid>/mem 扫描) guaidao2/php2shell-exploit-labstyle 未附 LICENSE(思想引用)
多形状生产者、逐原语三态预检表、post_max_size 与 chunked 的关系 ikow/php2shell-poc MIT
检测与缓解口径 NCSC-NL/php2shell、ronoski/php2shell-detect EUPL-1.2 / MIT

漏洞本体的一手出处:GHSA-w8hw-w9fx-j79w · GHSA-6wmf-4rv5-p3rp · 修复提交 php-src@3584a98。

一处值得单独说明的更正:我们曾把"12MB 单字段返回 200 空响应"读成"worker 被打死"。 真相是 post_max_size 只丢弃带声明 Content-Length 的请求,而链的生产者用无长度 (chunked)请求,因而不受它限制——这条由 ikow 的分析文档给出并被我们复现,细节见 docs/02-exploitation.md。

使用范围与授权

本工具是可终止 worker 的远程利用,而非被动扫描,请先取得书面授权再对生产资产使用。 关于"任意站点"的准确表述是:可利用集合为"指纹库中存在其构建(或同族几何)记录的目标"。 该集合可以扩展 —— --auto --auto-calibrate 在缺失记录时现场测量并写回,同一构建的下一个 站点即转为查表命中。

Topics: php-security · CVE-2026-109618 · CVE-2026-109619 · RCE · session-upload-progress · mod-php · PHP-FPM · Zend Engine · ASLR · exploit-development · proof-of-concept

About

php2shell (CVE-2026-109618/109619) 利用工具:带来源与日期的指纹库、两条引擎、免 gdb 标定;含源码级结论——本链下限 PHP 7.0。

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages