Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
StealMyBitcoin-PoC — A public release of the Steal My Bitcoin challenge | Kitploit
Tools/GitHubGitHub/killaboi/stealmybitcoin-poc
Memory ForensicsVulnerability AnalysisExploitationWeb Application ExploitationCTFPayload DevelopmentBinary ExploitationLabs & Practice
GitHubkillaboi/stealmybitcoin-poc

StealMyBitcoin-PoC

A public release of the Steal My Bitcoin challenge

View Repository
21641531 day agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

sponsored with ❤️ by hextree.io

Steal My Bitcoin CTF proof of concept

One live PHP callback against the Steal My Bitcoin challenge, start to finish, in a single standard-library Python file.

Python Dependencies Target Lab

This README explains stealmybitcoin_poc.py, a single-file Python reproduction of the request sequence used in my Steal My Bitcoin CTF run. It uses only Python's standard library. The script builds the requests itself, checks each leak before moving on, and records the HTTP bytes from a new run.

This can probably be done easier without all the extra debug crap and as in my John Hammond video about the Steam Local Privilege Escalation I discovered, you could probably clean up this ugly PoC.

[!NOTE] ⚠️ Honestly please clean it up or make your own better version that isn't crumpled by Sol. ⚠️

Everything below is written against the Docker lab, not the live challenge. The lab is a local Alpine PHP 8.4.26 CLI server behind nginx, which is the same build and worker model the offsets were calibrated on. Stand it up in a minute and the whole chain runs end to end on your own machine. The live run that actually solved the challenge is kept as a historical note at the bottom.

  stage                 what goes out                        what comes back
  ─────────────────────────────────────────────────────────────────────────────
  0  health      GET /php-version.php                 ->  PHP 8.4.26 confirmed
  1  heap leak   249,042 B producer + 8 x   6,672 B   ->  256 B cookie
                 (OOM poisons a worker)                   └─ Zend chunk base
  2  code leak   251,557 B producer + 8 x  92,388 B   ->   16 B cookie
                 (OOM poisons a worker)                   └─ zval_ptr_dtor
  3  callback    368,812 B POST                       ->  forged HashTable runs
                 (fake HashTable + command)               └─ zif_system(cmd)
  ─────────────────────────────────────────────────────────────────────────────
  20 HTTP requests at most. No looping, no retries.

The short version

  1. A small multipart upload makes PHP chew through far more memory than the wire size suggests.
  2. When it runs out of memory partway through session upload-progress parsing, PHP leaves stale state behind in a worker that then gets reused.
  3. Shape the follow-up requests right and bytes fall out through session cookies. I read those as a heap address and a PHP code address.
  4. With both addresses, a final request drops a forged PHP structure where cleanup will find it, and cleanup calls zif_system, PHP's handler for system.
  5. That command sends back a fresh proof marker and one print-flag result over the connection that is still open.

Why two addresses? The OS randomizes where PHP's code and heap land, independently. The heap leak tells the script where to put the forged structure, the code leak tells it where the function it wants to call actually lives. Addresses from an earlier worker or an earlier run are not safe substitutes, and every time I tried it the attempt just failed.

Contents

  • The lab
  • Start here
  • What each request does
    • 1. How a small producer reaches the memory limit
    • 2. How the heap address is recovered
    • 3. How the PHP code address is recovered
    • 4. How the final request reaches system
  • Reading the result
  • How often it actually works
  • The live run that solved it
  • Jargon, briefly
  • Thanks

The lab

The offsets in this PoC are calibrated for Alpine PHP 8.4.26 running the built-in CLI server with four workers. The lab reproduces exactly that, behind nginx, on loopback.

[!IMPORTANT] The build matters more than the version string. A Debian php:8.4.26-fpm stack reports the same PHP/8.4.26 but has a different binary layout, so the code-pointer fingerprint will never match and the run aborts at the code-leak stage. Use the Alpine image below.

Three small files. Put them in an empty directory:

public/php-version.php (the health endpoint, and the only page the PoC needs)

<?php
header('Content-Type: text/plain');
echo 'PHP ', PHP_VERSION, "\n";
echo 'PID: ', getmypid(), "\n";

print-flag (stand-in for the challenge binary, must be executable)

#!/bin/sh
printf '%s\n' 'LOCAL_DUMMY_WIF_NEVER_USE'

nginx.conf

events {}
http {
    server {
        listen 80;
        server_name _;
        location / {
            proxy_pass http://ctf-cross-php:8000;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
        }
    }
}

Then bring it up:

docker network create ctf-cross-net

docker run -d --name ctf-cross-php --network ctf-cross-net \
  --read-only --tmpfs /tmp:rw,size=32m,mode=1777 --memory 1g \
  -v "$PWD/public:/app:ro" \
  -v "$PWD/print-flag:/usr/local/bin/print-flag:ro" \
  -e PHP_CLI_SERVER_WORKERS=4 \
  php:8.4.26-fpm-alpine3.23 php -S 0.0.0.0:8000 -t /app

docker run -d --name ctf-cross-nginx --network ctf-cross-net \
  -p 127.0.0.1:18166:80 \
  -v "$PWD/nginx.conf:/etc/nginx/nginx.conf:ro" \
  nginx:1.27-alpine

Check it:

$ curl -s http://127.0.0.1:18166/php-version.php
PHP 8.4.26
PID: 1

The PID varies with whichever worker answers, and that is fine. The version line is the part the PoC checks.

Tear it down with docker rm -f ctf-cross-nginx ctf-cross-php && docker network rm ctf-cross-net.

[!TIP] On Windows, run those docker run commands from PowerShell, or replace $PWD with a full drive path like C:/lab. In Git Bash $PWD expands to an MSYS path such as /tmp/..., which Docker Desktop resolves inside its own VM. The bind mounts then silently create empty directories where nginx.conf and print-flag should be, and the containers come up broken.

A couple of notes on that recipe. The image is the fpm tag but the container runs php -S, so the SAPI is cli-server, which is what the calibration was measured against. PHP_CLI_SERVER_WORKERS=4 is what gives the leak stages more than one worker to race for. My own lab harness also passed --cap-add SYS_PTRACE --security-opt seccomp=unconfined so it could attach a tracer to the workers; the PoC itself never does that, so those are left out above.


Start here

Python 3.11 or newer, standard library only, nothing to install:

python .\stealmybitcoin_poc.py --url http://127.0.0.1:18166/php-version.php

Run it with no arguments and it prints its help and exits without touching the network.

A good run looks like this:

[health] PHP 8.4.26 confirmed
[heap] strict Zend base 0x78113e800000
[code] strict zval_ptr_dtor 0x615b1f512930
{"stage": "final", "nonce": "1ef0e73d8615157a", "execution_proof_verified": true, "response_path": "..."}
Download Tool