Dissecting Steal My Bitcoin: Standalone Python PoC for php2shell in PHP 8.4.26
Contents
TL;DR
Target A standalone Python exploit script StealMyBitcoin-PoC published by KillaBoi following a solve of the “Steal My Bitcoin” CTF challenge. It targets PHP 8.4.26 by exploiting the php2shell Use-After-Free (CVE-2026-109618/109619).
Details Recreates an end-to-end exploit chain using only Python’s standard library: memory limit exhaustion leaves corrupted worker state, heap and code addresses are parsed from session cookies, and a fake HashTable pivots cleanup into zif_system.
Context An educational teardown showing how the full attack chain succeeds against unpatched PHP 8.4.26. Environments running patched releases (such as 8.4.27) or disabling upload progress block the chain.
Update (October 26) The organizer published the challenge site source. A third party reports reproducing the chain in their own environment, and reports that the live target has not responded since about 22:50 UTC.
A standalone exploit script surfaced showing how an unpatched PHP 8.4.26 instance can be fully compromised when all stages of the php2shell attack chain succeed. Security researcher KillaBoi published StealMyBitcoin-PoC, a single-file Python script detailing the exact steps used to solve the “Steal My Bitcoin” CTF challenge.
The target was PHP 8.4.26, the final unpatched release in the 8.4 branch immediately preceding the 8.4.27 security update. The challenge objective was to retrieve a Bitcoin private key (the flag) to unlock a prize wallet. While prior coverage analyzed php2shell from a mitigation standpoint, this PoC provides an end-to-end look at how the vulnerability is exploited in practice.
How the UAF Chain Achieves Arbitrary Command Execution
The exploit chain takes advantage of the Use-After-Free (UAF) in Session Upload Progress (session.upload_progress, tracked under CVE-2026-109618/109619) to execute an arbitrary system command within the PHP worker process.
flowchart TD
A["Health Check<br/>Verify PHP 8.4.26 response"] --> B["Heap Address Leak<br/>Exhaust memory limit (128MiB)<br/>Read residual state via session cookie"]
B --> C["Code Address Leak<br/>Repeat process to locate<br/>internal PHP functions"]
C --> D["Hijack Cleanup<br/>Inject fake HashTable and point<br/>destructor pointer to zif_system"]
D --> E["Command Execution<br/>system(cmd) runs via open connection"]
The script first transmits a crafted multipart upload designed to exceed the default PHP memory limit (128MiB). Because the limit triggers during upload progress tracking, the worker process is left in a corrupted state with partially freed structures and is subsequently reused for incoming requests.
Next, a specifically structured HTTP request causes the reused worker to return residual memory bytes inside a Set-Cookie response header. Parsing this session cookie reveals the base address of the Zend heap. Repeating the request pattern with modified parameters leaks a function pointer and exposes the code segment address.
Once both base addresses are resolved, a final HTTP POST request places a fabricated internal structure into heap memory, pivoting PHP’s normal cleanup routine into command execution.
Triggering Command Execution via HashTable Destructors
PHP relies heavily on HashTable structures to handle arrays, symbol tables, and internal variables. When an HTTP request terminates, PHP iterates through allocated structures and invokes registered destructor functions (stored as function pointers) to clean up memory.
The PoC places a forged HashTable structure at a calculated heap address. It overwrites the internal destructor pointer with the address of zif_system (the internal C implementation of PHP’s system() function) and sets the command string as the argument.
When the worker finishes processing the request and runs its standard cleanup routine, PHP inadvertently executes zif_system("your command"). The resulting command output is returned directly over the active HTTP connection.
Independent Process Layouts and Address Lifetime
Both heap and code addresses are required for exploitation. Modern operating systems implement Address Space Layout Randomization (ASLR), mapping binaries and heap memory to randomized base addresses each time a process starts. The heap address determines where the forged structure resides, while the code address determines the location of zif_system. Missing either value results in a segmentation fault.
Attempting to reuse leaked addresses from previous runs or different worker processes reliably failed. Memory layouts remain consistent only while a specific worker process remains alive. Routing requests to an alternate worker or restarting the container shifts the required offsets.
Build Requirements and Environment Sensitivity
The offsets and addresses embedded in this PoC were calibrated specifically for Alpine Linux PHP 8.4.26 running via the built-in CLI server (php -S) with 4 worker processes.
Targeting a different distribution fails even if the PHP version matches. Testing against the official Debian-based Docker image (php:8.4.26-fpm) returned PHP/8.4.26, but differing binary layout and symbol positions resulted in a code fingerprint mismatch, halting the script before exploitation. The PoC contains built-in fingerprint checks to avoid crashing incompatible builds.
Successful execution relies on the Alpine Linux binary layout, execution via the CLI Server (cli-server SAPI), and a multiple-worker configuration to trigger worker reuse. Deploying against any alternate environment requires recalibrating offset constants.
Observed Success Rates
Because worker selection is non-deterministic, multiple attempts are often necessary to hit the specific reused worker. Running the script 10 consecutive times against the author’s local Docker test environment yielded the following results:
| Outcome | Count |
|---|---|
| Arbitrary command executed successfully | 7 |
| Reached final stage, but command execution trace failed | 2 |
| Heap leak failed to trigger | 1 |
The exploit succeeded in roughly 7 out of 10 attempts. Within an active container, ASLR bases remain static for individual processes; variability stems purely from which worker handles each request.
Failures fall into two distinct categories: either the worker race fails and the script halts prior to structure injection, or all stages pass and return HTTP 200 without triggering the destructor pathway. The PoC does not implement automated retry loops; failed attempts require running the script again from the beginning.
Execution Findings and Reported Discrepancies
The author documented raw output from the successful CTF solve on October 25, 2026. Logs show a healthy PHP 8.4.26 response, a 256-byte cookie returned during heap leakage, a 16-byte cookie during code leakage, and a final POST response returning a valid Wallet Import Format (WIF) Bitcoin private key.
Several caveats were noted by the author: raw full-packet HTTP request and response headers were not recorded during the live run, the target binaries were not dumped locally, and the extracted WIF private key did not derive the public prize wallet address displayed on the challenge homepage. The author confirmed with challenge organizers via email that this discrepancy resulted from an issue in the challenge’s initial wallet configuration.
While the exploit chain succeeded end-to-end, the recovered key did not grant access to the advertised prize funds.
Relation to Existing Mitigations
This exploit chain operates exclusively against vulnerable PHP versions where the php2shell flaws remain unpatched.
Applying official maintenance releases (8.5.12, 8.4.27, 8.3.36, 8.2.35, or corresponding backports) resolves the underlying UAF in upload progress handling and prevents the initial heap corruption.
For systems unable to upgrade immediately, setting session.upload_progress.enabled=0 disables the upload tracking codepath entirely and stops the attack at the initial heap leakage stage. Similarly, environments restricted to smaller file uploads will not hit the 128MiB threshold required to induce the residual worker state.
Update (October 26, 2026)
The organizer, pavja2, published the challenge site source at pavja2/smbc-source-oct-2026.
Its README says the practice copy contains a public dummy flag and has no wallet or prize.
The README also says the infrastructure setup was heavily AI-assisted.
A third party opened Issue #2 on the PoC repository.
The reporter says they reproduced the chain in their own environment.
They also say the live target has not responded since about 22:50 UTC.
Both points are the reporter’s claims, and I have not verified them.