Skip to content
A single Python file reproduces a PHP 8.4.26 remote code execution chain

A single Python file reproduces a PHP 8.4.26 remote code execution chain

News.Lavx.Hu • October 26, 2026

Security researcher KillaBoi released stealmybitcoin_poc.py, a standard-library Python script that reproduces the full exploit chain from the Steal My Bitcoin CTF challenge. The PoC targets Alpine PHP 8.4.26 CLI servers, using multipart upload progress parsing to trigger memory exhaustion, recover heap and code addresses from session cookies, and forge a PHP HashTable that calls zif_system through cleanup.

The GitHub repository StealMyBitcoin-PoC contains stealmybitcoin_poc.py, a single-file Python reproduction of the request sequence used in the Steal My Bitcoin CTF run. The script uses only Python's standard library, sends at most 20 HTTP requests, avoids loops and retries, and writes all evidence to a timestamped output directory before validating anything.

The target build matters more than the version string. The offsets are calibrated for Alpine PHP 8.4.26 running the built-in CLI server with four workers behind nginx. A Debian php:8.4.26-fpm stack reports the same PHP version but carries a different binary layout; the code-pointer fingerprint check rejects it immediately. The fingerprint examines the low 21 bits of the zval_ptr_dtor pointer, expecting exactly 0x112930, the page offset in the Alpine image. ASLR does not move that offset.

The chain operates in four stages. First, a health check verifies PHP 8.4.26 and a working endpoint. Second, a 249,042-byte multipart producer triggers a 128 MiB memory limit fatal during session upload-progress parsing. The PHP worker retains stale session state after the failure. Eight pipelined 6,672-byte consumers follow on the same connection; one may return a 256-byte PHPSESSID cookie containing leaked heap bytes. The script parses that cookie as little-endian 64-bit words and accepts only a base address where at least four pointer-looking values fall within the same aligned 2 MiB Zend heap region.

Third, a 251,557-byte producer triggers another memory exhaustion. Its eight 92,388-byte consumers hunt for a 16-byte cookie encoding the zval_ptr_dtor pointer. The script calculates zif_system from that pointer using the calibrated offset zval_ptr_dtor - 0x204BC0.

Fourth, a 368,812-byte multipart POST delivers a forged PHP HashTable. The structure overlaps a fake call frame and a zend_string holding the command argument. A 276-byte primer supplies a pointer into the controlled region. The request then carries a long form-field name that triggers another memory exhaustion. During cleanup, the interpreter reads the forged table's pDestructor as zif_system and passes the string argument to it.

The exploit calculates the forged table's address from the worker's heap layout: fake_table = zend_heap_base - 0x8400000 + 0x16D1D8. That distance depends on the four-worker Alpine setup. Change the image, the memory limit, or the worker count and the calculation collapses.

The command executed in the original live run printed a fresh nonce, the output of id, and the SHA-256 hash of the resolved print-flag binary. The script resolves the active socket under /proc/self/fd rather than relying on standard stdout, which returned nothing in testing.

Results from ten consecutive runs against the local Docker lab show seven verified executions with full markers present, two failures where the heap leak never landed, and one failure where the callback missed. The script saves every request and response as plaintext HTTP bytes, plus parsed JSON summaries, regardless of success or failure. Both leaks are stable once a container is up; every run against the same container returned the same zend_heap_base and the same zval_ptr_dtor, because the workers persist and ASLR is per process, not per request. What varies between runs is purely which worker answers which pipelined consumer.

The live run that solved the challenge occurred at 15:47:38 UTC on Oct. 25, 2026. It recovered a checksum-valid WIF, though the challenge creator later acknowledged the returned private key did not derive the advertised prize address. The original run did not preserve full outgoing HTTP bytes or response headers; the author reconstructed the complete request afterward from the builders and remaining metadata.

Users should treat this as an authorized testing tool only. The producer deliberately exhausts PHP memory. The script aborts if the endpoint does not match the expected health response or if the URL points outside loopback or the challenge domain. Every multipart body stays under 1 MiB, though each leak stage pipelines nine requests over a single connection.

The repository includes the full Docker recipe: an Alpine PHP 8.4.26 container with four workers, an nginx proxy, and the health endpoint. The README warns Windows users MSYS path expansion in Git Bash, which can silently break bind mounts.

This release demonstrates how session upload-progress parsing, worker reuse, and precise memory layout calibration combine into a concrete exploitation primitive against this specific Alpine build. The code is readable in attack order, with calibration constants grouped at the top and in the STAGES table. For anyone studying PHP internals, the file offers a direct map from multipart parsing to zif_system execution without external dependencies.

Extracted Entities

Attack Types (1)

Platforms (1)

Tools (2)