Quick Answer: The Steal My Bitcoin CTF challenge is a PHP exploitation exercise built around the session upload-progress feature. When an upload exhausts PHP's memory limit, stale state can survive in a reused worker. Attackers then use information leaks to defeat address randomization and reach code execution. The public KillaBoi/StealMyBitcoin-PoC repository documents this against a local lab.

A file upload that crashes with a memory error looks like a dead end. In the Steal My Bitcoin CTF, that crash was the first step of the chain. The public KillaBoi/StealMyBitcoin-PoC repository reproduces the challenge as a single Python script, and the README makes one point clear: the interesting part is what the failure leaves behind. This article explains the ideas behind that chain, why they work, and how to stop them in your own PHP applications.

What the challenge actually tests

Steal My Bitcoin is a capture-the-flag challenge built around a PHP target. The goal is not to break cryptography or find a hidden endpoint. It is to chain several low-level behaviours into remote code execution, then run a command on the server. The flag in the lab is a dummy value, but the path to reach it is the same one a real attacker would use against a vulnerable host.

That matters for how you read the challenge. CTF writeups often make exploitation look like magic. Here, every step rests on something real in PHP's runtime: how it parses multipart requests, how it stores session data, how it manages memory per worker, and how the operating system randomizes addresses. Understanding those pieces is more useful than memorizing any single payload.

Why PHP's upload progress feature is a liability

PHP has shipped a feature called session upload progress since version 5.4. When a request is a multipart upload and carries a PHP_SESSION_UPLOAD_PROGRESS field, PHP writes progress data into the session while it is still parsing the body. Front-end code can poll that session value to draw a progress bar.

The feature is useful, but it puts session writes inside the request-parsing path. That is the first design decision worth questioning. Parsing untrusted input and touching shared state at the same time gives an attacker more ways to shape what the runtime does.

Here's the part most guides skip: the feature is on by default in the built-in configuration. If your app doesn't use upload progress bars, you're running a parser feature you never asked for.

Memory exhaustion leaves stale state behind

Most people treat a fatal error as a failed attempt. If PHP runs out of memory, the request dies, the client gets an error, and nothing else happens. That is the lazy assumption, and this challenge shows why it is wrong.

When a worker hits its memory limit partway through parsing, the cleanup path may not reset everything. Some session and allocator state can remain in a worker that goes back into the pool and serves the next request. The request that caused the crash is not the same request that gets affected.

This is the counter-intuitive finding: the fatal error is the signal, not the noise. A Allowed memory size exhausted message in your logs on a multipart endpoint should be treated as a possible security event. The default memory_limit in PHP is 128M, which is generous enough that a crafted body can reach it while the wire size stays small.

Common mistake: teams see a memory fatal, raise memory_limit to make the error go away, and move on. That removes the symptom and keeps the state problem.

Why address leaks matter under ASLR

Even with stale state in a worker, an attacker still needs to know where things are in memory. Modern systems use Address Space Layout Randomization (ASLR), which places code and heap regions at unpredictable base addresses each run. A corrupted pointer pointing at a guessed address will usually crash the process instead of running attacker-chosen code.

That's why the challenge uses information leaks. A leak returns memory contents that reveal real addresses, which the attacker can then use to compute the locations they need. The README describes two separate leaks, one for heap data and one for code, because the two regions are randomized independently. An address from one run or one worker can't be reused for another.

The useful defensive takeaway is that ASLR raises the cost of exploitation but does not stop it when a leak exists. Your real protection is preventing the leak and the corruption in the first place.

Comparing the options that actually protect you

Here is a practical view of the settings that matter most for this class of problem. Values reflect common PHP defaults and typical hardened deployments.

ControlDefault behaviourRecommended settingTrade-off
session.upload_progress.enabledOnOff, unless you use progress barsProgress bars stop updating
memory_limit128MKeep it modest; never raise it to hide fatalsLarge legitimate uploads may need chunking
Upload size (upload_max_filesize, post_max_size)2M / 8MSet only as large as your feature needsUsers with big files need another path
Server runtimeBuilt-in php -S is single-purposePHP-FPM behind nginx or ApacheMore moving parts to configure
Logging of memory fatalsWritten to error logAlert on repeated fatals from one clientRequires log pipeline work

Notice that the most effective control is also the simplest. If you don't use upload progress, turn it off. The rest reduce the damage if something slips through.

Cause and effect in a real application

Consider a typical PHP app with a profile photo upload. The developer enables upload progress for a nicer UI, nobody watches the error log, and a memory fatal on the upload endpoint appears a few times a week. Each one looks like a flaky network or a large file. In reality, each one is a worker that may be carrying state into the next request.

Now the cause and effect is concrete. When a fatal goes unexamined, the worker keeps running, the pool keeps serving other users, and the attacker has time to refine a second request. The fix is not a bigger memory limit. It is to disable the unused feature, alert on fatals from upload routes, and run PHP under a process manager that recycles workers.

This next part matters more than it looks. Lab fingerprints can break in the wild. Public exploit code is often tuned to one specific PHP build, and the README notes that a Debian and an Alpine image of the same PHP version have different binary layouts. For defenders, that means version numbers alone don't prove you're safe or exposed. Check your actual configuration.

Why the lab matters more than the flag

The author of the public repo is candid that the script is messy and invites a cleaner rewrite. That honesty is useful. Real exploitation work is full of debug output, timing hacks, and fragile offsets. The lesson for learners is to read those rough edges as evidence of how brittle exploitation is, and to focus on the underlying behaviour.

If you want to study this safely, do it in an isolated lab you own, on loopback, with no internet exposure. Use CTF challenges you're authorized to attack. Never point tools at systems you don't control.

Frequently Asked Questions

What is the Steal My Bitcoin CTF?

The Steal My Bitcoin CTF is a PHP exploitation challenge where the goal is to chain a memory-exhaustion bug, information leaks, and a final request into remote code execution. It is run as a capture-the-flag exercise, and the flag in lab versions is a dummy value.

How does a PHP memory limit error lead to code execution?

A memory fatal during multipart parsing can leave stale session and allocator state in a worker that gets reused. Attackers then use leaks to find real addresses despite ASLR and craft a follow-up request. Each step depends on the previous one, so disabling the unnecessary feature or handling fatals properly breaks the chain.

Why does ASLR matter for PHP exploits?

ASLR randomizes where code and heap regions load on each run, so a guessed address usually crashes the process. Information leaks defeat that by revealing real addresses. ASLR raises the difficulty of exploitation but is not a substitute for patching or disabling risky features.

how do I disable PHP session upload progress?

Set session.upload_progress.enabled = Off in your php.ini or pool configuration, then restart PHP-FPM or your web server. Confirm with php -i | grep upload_progress. If you rely on progress bars, keep it on only for the specific routes that need it, and monitor those routes for memory fatals.

Is raising PHP's memory_limit a good fix for upload errors?

No. Raising memory_limit hides the symptom and leaves the underlying parser behaviour unchanged. Find out which request triggers the fatal, reduce the upload surface, disable unused features, and only then adjust limits for legitimate use cases.

Pull the StealMyBitcoin-PoC README and read the sections on the request sequence and the lab setup. Then, in your own isolated lab, check whether session.upload_progress.enabled is set on your PHP stack and turn it off if you don't need it this week. Pass this to anyone running a PHP app with file uploads who has seen a memory fatal in their logs and assumed it was harmless.