Skip to content

Commit 2479e2d

Browse files
Add vessel stage-3 RCE, verified profiles and reliability fixes
Adds the vessel stage-3 variant, the full per-build profiles, and reliability fixes: Set-Cookie '+' decoding, post_max_size/display_errors probe, implausible-leak retry, stronger tests and evidence. Co-authored-by: Matthias Lutter <esoteric-programmer@users.noreply.github.com>
1 parent dff9f1e commit 2479e2d

100 files changed

Lines changed: 4144 additions & 247 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 77 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ not match the profile.
3535
| PHP 8.5.11 aarch64 | `php:8.5.11-apache` | `php-8.5.11-bookworm-aarch64` | yes | yes | **yes** | `uid=33(www-data)` |
3636
| PHP 8.5.11 amd64 | `php:8.5.11-apache` | `php-8.5.11-bookworm-amd64` | yes | yes | **yes** | separate offsets, same `htdata` |
3737
| PHP 8.5.10 aarch64 | `php:8.5.10-apache` | `php-8.5.10-bookworm-aarch64` | yes | yes | **yes** | `uid=33(www-data)` |
38+
| PHP 8.5.10 amd64 | `php:8.5.10-apache` | `php-8.5.10-bookworm-amd64` | yes | yes | **yes** | `uid=33(www-data)` |
39+
| PHP 8.5.5 amd64 | `php:8.5.5-apache` | `php-8.5.5-bookworm-amd64` | yes | yes | **yes** | vessel stage 3; `uid=33(www-data)` |
40+
| PHP 8.5.0 amd64 | `php:8.5.0-apache` | `php-8.5.0-bookworm-amd64` | yes | yes | **yes** | vessel stage 3; `uid=33(www-data)` |
41+
| PHP 8.5.1 – 8.5.9 amd64 | `php:8.5.{1..9}-apache` | `php-8.5.<v>-bookworm-amd64` | yes | yes | **yes** | vessel stage 3; `uid=33(www-data)` |
3842
| PHP 8.4.26 amd64 | `php:8.4.26-apache` | `php-8.4.26-bookworm-amd64` | yes | yes | **yes** | reference build |
3943
| PHP 8.4.26 deb13/apxs2 amd64 | source build (Debian 13, Apache 2.4.69 apxs2) | `php-8.4.26-deb13-apxs2-amd64` | yes | yes | **yes** | third stage-3 layout; instance-dependent; requires `--profile deb13-apxs2` |
4044
| PHP 8.4.26 FPM amd64 | php-8.4.26 source tarball (nginx+FPM) | `php-8.4.26-fpm` | http (stale string) | http (stale string) | **yes** | `BG(unserialize)` chain; two-slot reclaim; HTTP-only `no-oracle` endgame |
@@ -47,44 +51,73 @@ not match the profile.
4751
| PHP 7.4.33 FPM amd64 | php-7.4.33 source tarball (nginx+FPM) | `php-7.4.33-fpm` | http (stale string) | http (stale string) | **yes** | same chain; `poison` also works |
4852
| PHP 7.2.34 FPM amd64 | php-7.2.34 source tarball (nginx+FPM) | — (plant only) | no | no | no | arms the stale state; reclaim shape not found; `poison` plant, no RCE profile |
4953
| PHP 8.4.25 aarch64 | `php:8.4.25-apache` | `php-8.4.25-bookworm-aarch64` | yes | yes | **yes** | probabilistic stage 3; retries |
50-
| PHP 8.5.0 aarch64 | `php:8.5.0-apache` | `php-8.5.0-bookworm` | yes | yes | no | stage-3 layout not found |
54+
| PHP 8.4.25 amd64 | `php:8.4.25-apache` | `php-8.4.25-bookworm-amd64` | yes | yes | **yes** | `uid=33(www-data)` |
55+
| PHP 8.4.20 amd64 | `php:8.4.20-apache` | `php-8.4.20-bookworm-amd64` | yes | yes | **yes** | `uid=33(www-data)` |
56+
| PHP 8.4.19 amd64 | `php:8.4.19-apache` | `php-8.4.19-bookworm-amd64` | yes | yes | **yes** | `uid=33(www-data)` |
57+
| PHP 8.4.11 amd64 | `php:8.4.11-apache` | `php-8.4.11-bookworm-amd64` | yes | yes | **yes** | `uid=33(www-data)` |
58+
| PHP 8.4.10 amd64 | `php:8.4.10-apache` | `php-8.4.10-bookworm-amd64` | yes | yes | **yes** | `uid=33(www-data)` |
59+
| PHP 8.4.13, 8.4.8 amd64 | `php:8.4.{8,13}-apache` | `php-8.4.<v>-bookworm-amd64` | yes | yes | no | stage-3 sweep attempts exhausted |
60+
| PHP 8.4.14 – 8.4.18, 8.4.12, 8.4.7, 8.4.6 amd64 | `php:8.4.<v>-apache` | `php-8.4.<v>-bookworm-amd64` | yes | pending | no | stage-2 leak needs re-run (cookie decode fix) |
61+
| PHP 8.5.0 aarch64 | `php:8.5.0-apache` | `php-8.5.0-bookworm` | yes | yes | no | lab build; stage-3 layout not found |
62+
| PHP 8.3.35 amd64 | `php:8.3.35-apache` | `php-8.3.35-bookworm-amd64` | yes | yes | no | stage-3 reference alive; vessel geometry does not fit this heap |
5163
| PHP 8.3.35 aarch64 | `php:8.3.35-apache` | `php-8.3.35-bookworm-aarch64` | yes | yes | no | stage-3 reference alive |
5264
| PHP 8.3.33 mod_php | `wordpress:7.1.1-apache` | `php-8.3.33-wordpress-mod_php` | yes | yes | no | stage-3 reference alive |
65+
| PHP 8.4.5 amd64 | `php:8.4.5-apache` | `php-8.4.5-bookworm-amd64` | yes | no | no | stale block at v0892; no libphp pointer |
66+
| PHP 8.3.20 amd64 | `php:8.3.20-apache` | `php-8.3.20-bookworm-amd64` | yes | no | no | stale block at v0892; no libphp pointer |
67+
| PHP 8.4.2 amd64 | `php:8.4.2-apache` | `php-8.4.2-bookworm-amd64` | no | no | no | UAF; leak shape differs |
5368
| PHP 8.3.0 aarch64 | `php:8.3.0-apache` | `php-8.3.0-bookworm` | no | no | no | `PS(id)` dangling; cookie not reclaimed |
69+
| PHP 8.2.34 amd64 | `php:8.2.34-apache` | `php-8.2.34-bookworm-amd64` | no | no | no | UAF; leak shape differs |
5470
| PHP 8.2.34 aarch64 | `php:8.2.34-apache` | `php-8.2.34-bookworm-aarch64` | no | no | no | UAF; leak shape differs |
55-
| PHP 8.1.34 aarch64 | `php:8.1-apache` | `php-8.1.34-bookworm-aarch64` | no | no | no | UAF; leak shape differs |
71+
| PHP 8.2.28 amd64 | `php:8.2.28-apache` | `php-8.2.28-bookworm-amd64` | no | no | no | UAF; leak shape differs |
72+
| PHP 8.1.34 amd64 | `php:8.1-apache` (bullseye) | `php-8.1.34-bullseye-amd64` | no | no | no | UAF; leak shape differs; EOL branch |
73+
| PHP 8.1.34 aarch64 | `php:8.1-apache` | `php-8.1.34-bookworm-aarch64` | no | no | no | UAF; leak shape differs; EOL branch |
5674
| PHP 8.0.30 aarch64 | `php:8.0-apache` | `php-8.0.30-bookworm-aarch64` | no | no | no | UAF; leak shape differs |
75+
| PHP 7.4.33 amd64 | `php:7.4-apache` (buster) | `php-7.4.33-buster-amd64` | no | no | no | UAF; leak shape differs; EOL branch |
5776
| PHP 7.4.33 aarch64 | `php:7.4-apache` | `php-7.4.33-bookworm-aarch64` | no | no | no | UAF; leak shape differs |
5877
| PHP 8.2.0 aarch64 | `php:8.2.0-apache` | — (offsets only) | n/a | n/a | no | no heap profile |
59-
60-
**22 profiles (14 Apache/mod_php + 8 PHP-FPM), 14 with verified RCE. Offsets
61-
extracted for 12 official-image Apache builds.** The PHP-FPM profiles are
62-
**contributor-reported, not independently confirmed** and environment-specific (the compile-time prefix
63-
shifts the heap); re-run `calibrate` on the host that will run the exploit
64-
instead of trusting the shipped geometry. They have no Apache leak stages: the
65-
chain abuses the stale `BG(unserialize)` struct instead.
66-
`no-oracle` is fully remote — it leaks the chunk and php-fpm PIE bases over
67-
HTTP from a stale-session string OOB read (or takes them from `--chunk`/
68-
`--pie`) and fires without touching `/proc/<pid>/mem`; the lab-only `endgame`
69-
can still read them through the oracle
70-
([docs/fpm-chain.md](docs/fpm-chain.md)).
71-
72-
`Heap leak` is stage 1 and `libphp leak` is stage 2; `oracle (heap)` and
73-
`oracle (PIE)` mean the FPM chain reads the heap chunk and the php-fpm PIE base
74-
from `/proc/<pid>/mem` in local lab mode instead. `RCE` is a real command proof
75-
on an isolated lab. `no` means not verified, not necessarily impossible; `n/a`
76-
means no heap profile exists for that build. The 8.2.0 row is an
77-
offsets-only entry. All `yes` Apache rows use the lab configuration: prefork
78-
`mod_php`, one worker, `display_errors=On`, `memory_limit=8M` and
79-
`session.upload_progress=On`; the FPM row uses the nginx+FPM lab. Per-image
78+
| PHP 8.0.30 amd64 | `php:8.0-apache` | — (offsets only) | n/a | n/a | no | no heap profile |
79+
80+
**56 profiles (48 Apache/mod_php + 8 PHP-FPM), 30 with verified RCE. Offsets
81+
extracted for 48 image/arch rows covering 39 official-image Apache builds.**
82+
83+
Independent re-verification on 2026-10-30 reproduced the command proof
84+
(`uid=33(www-data)`) for every RCE profile added by the stage-3 PR: 8.4.10,
85+
8.4.11, 8.4.19, 8.4.20, 8.4.25 amd64 and all of 8.5.0–8.5.10 amd64 plus
86+
8.5.11 aarch64. Those profiles say
87+
`contributor-reported, maintainer-confirmed` in their `status`: the contributor
88+
supplied the calibration and the maintainer reproduced it. The PHP-FPM profiles
89+
are **contributor-reported, not independently confirmed** and
90+
environment-specific (the compile-time prefix shifts the heap); re-run
91+
`calibrate` on the host that will run the exploit. The 8.4.26 deb13/apxs2
92+
source build is likewise **contributor-reported, not independently confirmed**.
93+
The four Apache profiles that already existed on `main` before the stage-3 PR
94+
were verified earlier and were not re-run during this review.
95+
96+
`Heap leak` is stage 1, `libphp leak` is stage 2 and `RCE` is a real command
97+
proof on an isolated lab. `no` means not verified, not necessarily impossible;
98+
`pending` means the sweep predates the cookie `'+'` decoding fix and the leak
99+
stage must be re-run; `n/a` means no heap profile exists for that build. The
100+
8.2.0 and 8.0.30-amd64 rows are offsets-only entries. All `yes` Apache rows use
101+
the lab configuration: Apache prefork `mod_php`, one worker,
102+
`display_errors=On`, `memory_limit=8M` and `session.upload_progress=On`. The
103+
FPM rows use the nginx+FPM lab: `oracle (heap)`/`oracle (PIE)` mean the FPM
104+
chain reads the chunk and php-fpm PIE base from `/proc/<pid>/mem` in local lab
105+
mode; the `no-oracle` path leaks them over HTTP and is fully remote. Per-image
80106
offsets and `libphp` sha256 are in
81-
[the offset inventory](docs/offset-inventory.md). The 8.4.25 profile needs the
82-
default retries because its stage 3 is probabilistic. The 8.4.26 deb13/apxs2
83-
row is a **contributor-reported, not independently confirmed source build**,
84-
not an official image: its stage-3 phase is instance-dependent (the contributor reported 4/4 runs on one Apache master and
85-
0/5+ on others), so treat `rce: true` as “reproduced on the reporter's lab” and
86-
retry on a fresh worker/master. Because two 8.4.26 amd64 profiles exist, the
87-
dispatcher requires `--profile deb13-apxs2` for this build.
107+
[the offset inventory](docs/offset-inventory.md). The 8.4.25 (aarch64) profile
108+
needs the default retries because its stage 3 is probabilistic. The 8.5.0 –
109+
8.5.9 rows use the **vessel stage 3** (`tools/php2shell_vessel.py`, documented
110+
in [the profile inventory](docs/profile-inventory.md)): the early-8.5 heap puts
111+
the dangling reference out of the upstream carrier's reach, so request S
112+
steers it one page up onto B2's blob page and a blob embeds the reference
113+
pattern itself. The 8.1/8.2/7.4 and 8.4.2 rows are the real-world "older
114+
subversion" builds: on those the stage-1 UAF shapes the heap differently and
115+
the leak does not reproduce with the 8.4.10+ parameters — an honest negative,
116+
reported as such. The 8.4.26 deb13/apxs2 row is a contributor-reported source
117+
build, not an official image: its stage-3 phase is instance-dependent (the
118+
contributor reported 4/4 runs on one Apache master and 0/5+ on others), so
119+
retry on a fresh master and select it with `--profile deb13-apxs2` because
120+
two 8.4.26 amd64 profiles exist.
88121

89122
## Features
90123

@@ -93,8 +126,11 @@ dispatcher requires `--profile deb13-apxs2` for this build.
93126
parameters explicitly. `--php-version` covers servers that hide their version;
94127
`--arch` selects aarch64 or amd64 (auto-detected for local containers,
95128
required for remote targets), and `--profile` forces an exact build.
96-
- **Memory-limit probe** — one request reveals the target `memory_limit` from
97-
the fatal message; the chain then runs once instead of sweeping every value.
129+
- **Configuration probe** — one request reveals the target `memory_limit`,
130+
`post_max_size` and whether request-startup diagnostics render
131+
(`display_errors`). The chain then runs once instead of sweeping every
132+
memory limit, and a `post_max_size` that would silently reject the OOM body
133+
is reported as `CONFIG BLOCKED` before any exploit attempt.
98134
- **Worker pinning** — keeps N-1 prefork workers busy with idle keep-alive
99135
connections so the whole chain lands on one process. Automatic for loopback
100136
laboratories; opt-in for remote infrastructure.
@@ -168,8 +204,9 @@ Expected shape:
168204
00:00.00 DISCOVER http://127.0.0.1:8105/index.php
169205
00:00.01 OK PHP 8.4.26 · Apache/2.4.69 (Debian)
170206
00:00.01 PREFLIGHT php-8.4.26-bookworm-amd64
171-
00:00.15 PROBE memory_limit probe: 16 MiB body
207+
00:00.15 PROBE config probe: 16 MiB body
172208
00:00.16 OK detected memory_limit=8M
209+
00:00.16 OK display_errors=On (request-startup diagnostics are rendered)
173210
00:00.17 EXECUTE memory_limit=8M · huge_mb=12
174211
│ [+] chunk base = 0x7ffffc400000
175212
│ [+] libphp base = 0x7ffffe013000
@@ -324,13 +361,18 @@ The shipped calibration targets:
324361

325362
- Apache prefork + `mod_php` with **one worker**;
326363
- `display_errors=On`, `memory_limit=8M`, `max_input_vars=1000`;
364+
- **`post_max_size` ≥ `memory_limit` + 12M** (the labs ship 512M): stage 1
365+
posts a body of `memory_limit` + 12M, and PHP rejects a body larger than
366+
`post_max_size` *before* parsing it — no OOM, no leak, no error. The stock
367+
default `post_max_size=8M` blocks the whole chain on its own;
327368
- `session.upload_progress.enabled=On`, `session.use_cookies=1`,
328369
`session.use_strict_mode=0`.
329370

330371
A hardened production PHP (128M+, `display_errors=Off`, multiple workers, FPM)
331372
does not match the shipped Apache calibration. Multi-worker Apache targets are
332373
handled with `--pin-workers`; `display_errors=Off` remains an open research
333-
item. The FPM chain runs against two supported shapes — run `probe` against a
374+
item. The dispatcher reports a blocking `post_max_size` as `CONFIG BLOCKED` and
375+
skips memory-limit candidates whose OOM body the target would reject. The FPM chain runs against two supported shapes — run `probe` against a
334376
target to see which ones its configuration satisfies:
335377

336378
- **Stock installation (single surface, `--arm-mode growth`, auto when
@@ -572,7 +614,7 @@ OOM, which is why the 8.4/8.5 stage-3 carrier does not transfer.
572614
configuration.
573615
- **`expose_php=Off` changes stage 3.** Hiding `X-Powered-By` shifts the startup
574616
heap; the known stage-3 pads do not fit and a dedicated calibration is needed.
575-
- **8.5.0 / 8.3.0** have leaks or UAF preconditions but no verified stage 3.
617+
- **8.3.0** has UAF preconditions but no verified stage 3.
576618
- **8.4.25** is probabilistic; use the default retries.
577619
- **Remote proof is not guaranteed.** `id>p1` only works if the webroot is
578620
writable and the file is served at `/p1`.

0 commit comments

Comments
 (0)