Skip to content

Feat/fpm http leak NGINX PHP-FPM - #6

Open
VABISMO wants to merge 12 commits into
devploit:mainfrom
VABISMO:feat/fpm-http-leak
Open

VABISMO wants to merge 12 commits into
devploit:mainfrom
VABISMO:feat/fpm-http-leak

Conversation

@VABISMO

@VABISMO VABISMO commented Oct 24, 2026

Copy link
Copy Markdown

NGINX PHP-FPM VERSIONS

The Apache stage-1/2/3 chain does not transfer to PHP-FPM: a pre-RINIT
OOM kills the worker (FPM_EXIT_SOFTWARE). This adds the FPM-viable
primitive instead, exploiting the same CVEs through the stale
BG(unserialize) state:

- producer: post-RINIT OOM leaves {level=1, data=d} dangling
- consume: pre-RINIT rfc1867 reuses d; a 4 MB builder-pool body
  reclaims the 8216-byte struct and forges var_hash
- R:2 becomes an IS_REFERENCE -> fake HashTable whose pDestructor
  is zif_system; session shutdown fires the command (verified:
  uid=1000 proof in the nginx+FPM lab)

Ships tools/php2shell_fpm.py (arm/reclaim/fire client, patchcheck),
tools/php2shell_fpm_labs.py (user-prefix nginx+FPM lab, no Docker),
profiles/8.4.26-fpm.json, schema extensions (sapi: fpm, fpm block),
FPM dispatch in php2shell_auto.py (--sapi/--builder), unit and
lab-gated integration tests, and docs/fpm-chain.md. patchcheck
confirms 8.4.27 is clean (level=0, workers survive).
patchcheck iterates both lab builds and takes no --base; endgame and
primitive require --base/--builder before the subcommand name.
- redact the local lab username from the published proof example
- document the FPM profile in docs/profile-inventory.md (table synced
  with php2shell_inventory.py output, including the pre-existing
  missing 8.3.0/8.5.0 rows)
- state the two-slot reclaim/retry behaviour in the profile status and
  the README table (probabilistic disclosure)
- correct the README profile counters (14 profiles, 6 verified RCE)
- add `leak` and `no-oracle` subcommands: leak the Zend-MM chunk and
  php-fpm PIE bases from a stale short-session string OOB read, then
  brute-force the two reclaim slots without reading /proc/<pid>/mem
- keep `--chunk`/`--pie` overrides and a /proc/<pid>/maps fallback; make
  `endgame` fall back to the no-oracle geometry when the oracle probe
  finds no cover
- record the verified leak geometry and no-oracle candidates in the FPM
  profile, extend the profile schema, and document the builder arm page
- add leak/dechunk unit tests, a live no-oracle RCE test, and lab helper
  pages (`bigvar.php`, `leakvar.php`, `refb.php`) plus lab-only app
  diagnostics
- update README, the FPM chain guide, and the profile inventory; correct
  the unit-test count and remote-FPM limitation wording

Verified on the 8.4.26 nginx+FPM lab: leak prints chunk=0x7f4f48800000
pie=0x55d75fc00000; no-oracle reaches uid=1000 without --chunk/--pie;
51 unit tests pass (3 jsonschema live tests skipped unless installed),
and primitive/endgame/no-oracle integration tests pass.
Replace the FPM client and labs launcher with the version-aware chain and
add the supporting tools:

- php2shell_fpm.py gains a fully remote `calibrate` (derives the leak and
  reclaim geometry of any 8.0+ build over HTTP), the pre-auth session-file
  `poison` that covers PHP <= 7.x, and a `no-oracle` endgame that refuses
  the local /proc fallback unless explicitly asked.
- php2shell_fpm_versions.py is the single source of truth for the
  affected-version matrix (is_vulnerable / fixed_in / source_is_fixed
  classify a tarball by the upstream fix marker, not a version string).
- php2shell_fpm_profile.py drafts a profile from an unstripped php-fpm
  using nm plus php2shell_fpm_offsets.c, compiled per build for the struct
  layouts.
- php2shell_fpm_matrix.py verifies the remote chain on every unpatched
  build, one lab at a time.
- build_php.sh retries truncated tarballs and patches the two glibc
  misdetections that stop 7.x from building with a current toolchain;
  build_all.sh builds several versions in parallel.

Verified on 8.2.34, 8.3.35, 8.4.0, 8.4.25 and 8.4.26 (amd64). 8.5.x is
intentionally out of scope; the fix ships in 8.5.12.
Ship the calibrated profiles for the last vulnerable release of every
tracked branch plus the earliest 8.4, all marked verified/rce:

  8.2.34, 8.3.35, 8.4.0, 8.4.25, 8.4.26

Each records the symbols, the BG(unserialize) offsets, the reclaim
geometry, the HTTP-only leak/no-oracle geometry and the compiled struct
layout for that build.

Introduce profiles/fpm-schema.json as the strict validator for FPM
profiles (it requires the fpm block, status, oom_mb and distro), and add
the `layout` block to the generic profiles/schema.json so every build
profile -- Apache and FPM alike -- still validates in a single pass. The
inventory test skips both schema files instead of mistaking one for a
profile.
The pre-existing research tools were written before ruff was adopted and
never linted (the old `lint` target only ran gitleaks). Now that ruff
enforces E/F/W/I/UP/B/SIM, fix the findings so `make lint` and CI pass
repo-wide:

- wrap one-shot open(...).read() calls in context managers (SIM115);
- drop the redundant UTF-8 coding declarations and open() mode args;
- use builtin generics and f-strings instead of typing/percent formats;
- sort the imports.

Behavior is unchanged; the unit suite still passes.
Makefile: add targets for building nginx/PHP (build-nginx, build-php,
build-all), drafting a profile (profile-draft), remote calibration
(calibrate-remote), the remote chain (remote) and the unpatched matrix
(matrix). Split the rootless FPM lab targets (labs-fpm-*) from the Docker
labs, add labs-fpm-smoke/ports, and split profile validation into the
generic schema and the strict FPM schema. `lint` now runs ruff (matching
CI) and the gitleaks scan moves to its own `secrets` target.

CI: lint with ruff, compile the tools, check shell syntax, run the unit
suite, and validate both the generic and FPM profile schemas, alongside
the existing gitleaks secrets job. Add the manual live-lab workflow
(build + exploit 8.4.x) and the tag-triggered release workflow, plus a
dev requirements file (jsonschema, ruff) the CI installs.

pyproject: keep the php2shell identity but modernize the build backend
(setuptools>=77, build_meta, PEP 639 license-files) and add the php-fpm
keyword.
README: the status table now lists the five verified FPM builds, the
features section describes the remote calibrate/poison/no-oracle flow and
the version registry, and the FPM lab quick start uses the corrected
`up --only` CLI with build/calibrate/poison/matrix examples. Update the
repository layout and drop the stale claim that the FPM chain always
needs a /proc oracle -- only the lab endgame/primitive modes do.

docs/fpm-chain.md: add the affected-version table, the PHP <= 7.x
session-file plant (poison), remote calibration and the unpatched matrix,
expand the lab port map to every verified build, and fix `up --only`.

docs/labs.md: give the rootless nginx+FPM laboratory its own section with
the build steps and the full port map, linking back to fpm-chain.md.
@VABISMO VABISMO changed the title Feat/fpm http leak Feat/fpm http leak NGINX PHP-FPM Oct 24, 2026
@VABISMO VABISMO mentioned this pull request Oct 24, 2026
…/8.1 chain

Everything from the FPM extraction, adapted to this repository:

- arm modes (--arm-mode, auto by origin): the classic two-surface builder
  and a single-surface AMPLIFIED session_decode for stock installations -
  one nginx port, one pool, memory_limit=128M, pm=dynamic, stock php.ini.
  The payload multiplies decode memory ~22x per byte while
  BG(unserialize).level == 1, so one POST arms the producer from an empty
  session; a ladder, a growth fallback and a big-body probe adapt to body
  caps (nginx needs client_max_body_size 5m; stock 1m is too small).
- --workers fans the arm/consume over a pm=dynamic pool (default 5) and
  --builder-prefix /b runs both classic surfaces behind one port.
- probe: a per-target checklist of every chain precondition (version,
  session_start, reflection, reference, arm physics, body budget).
- the leak reclaim buffer is TILED with the read length: any overlap of the
  freed short string yields a sane oversized length, so calibration sweeps
  reclaim shapes instead of one pre-placed header (which crashed workers on
  uncalibrated builds).  Phase 1 is round-robin with set-based stability and
  bounded collection: crash-heavy shapes no longer degrade the state of the
  shapes after them.
- profiles now carry per-environment geometry: the classic fields stay the
  reference and fpm.growth holds the default-install calibration; the schema
  (fpm-schema.json and the generic schema) accepts the new section.
- 7.4.33, 8.0.30 and 8.1.33 verified end to end over HTTP (calibrate ->
  uid=1000 proof; no-oracle reproduced on each): php_session_cancel_decode
  only unlinks the session file, the struct is released by the Zend MM on
  every branch, so chain_applies() now covers 7.4 and all 8.x.  7.2 arms
  but no reclaim shape covers its struct yet, and 5.x has no zend_reference
  - both keep the session-file plant.  registry docstrings corrected.
- 8.4.26 recalibrated against THIS checkout (per-environment geometry);
  verification.json records the matrix verdicts and the live-fpm workflow
  calibrates on the runner before testing, so CI is environment-independent.
- lab: --mode default renders the stock-installation pool; grow.php is the
  amplifier/growth stand-in; up() reloads a running nginx instead of
  serving a stale config; down() sweeps orphan masters.
- tests: DefaultModeTests, a poison lab test, dynamic LAB_PORTS, the 7.4+
  registry reality, and geometry assertions relaxed to per-environment
  invariants (the exact offsets live in the profiles).
- docs: README status/quick start/requirements/multi-worker/limitations,
  docs/fpm-chain.md (versions, arm modes, tiling, laboratory, driving,
  matrix, plant), docs/labs.md, the profile inventory, and new
  probe-remote / remote-default / labs-fpm-up-default make targets.
The matrix runner records its verdicts in profiles/verification.json, which
is not a build profile; the Makefile target and test_core already skipped
it, but the workflow's own inline validation loop did not, so every push
failed validating verdict records against the profile schema.
The leak geometry is per environment (compile-time prefix and checkout
paths shift the heap), and the shipped 8.4.25 profile had been calibrated
on a different machine.  Re-ran the HTTP-only calibrate against this
repo's own php-8.4.25 build: draft extracted from the binary, phase-1
geometry (chunk_ptr_off=0x87e8, PIE pointer @leak+0x17668), reclaim
candidate 0x81000/0xfe8, and a uid=1000 RCE proof recorded in the profile
(verifier out/calib8425).  The two tests that pinned the old reference
values follow the recalibrated profile.

Also removed the draft 7.2.34 profile: the stale struct arms (level=1) but
lives in a recycled small-bin slot (d_off=0x5fc58) that no consume shape
reclaims - an 8K-class spray of 48 fields per request never lands on it -
so 7.2 stays a session-file plant target, as documented.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant