
Une version publique du challenge Steal My Bitcoin
Un callback PHP en direct contre le challenge Steal My Bitcoin, du début à la fin, dans un seul fichier Python utilisant uniquement la bibliothèque standard.
Ce README explique stealmybitcoin_poc.py, une reproduction en un seul fichier Python de la séquence de requêtes utilisée lors de mon run du CTF Steal My Bitcoin. Il n'utilise que la bibliothèque standard de Python. Le script construit lui-même les requêtes, vérifie chaque fuite avant de passer à la suite, et enregistre les octets HTTP d'un nouveau run.
Cela peut probablement être fait plus simplement sans tout le débogage superflu et, comme je l'ai découvert dans ma vidéo John Hammond sur l'élévation de privilèges locale de Steam, vous pourriez probablement nettoyer ce PoC peu élégant.
[!NOTE] ⚠️ Sérieusement, nettoyez-le ou faites votre propre meilleure version qui ne soit pas chiffonnée par Sol. ⚠️
Tout ce qui suit est écrit pour le lab Docker, pas pour le challenge en direct. Le lab est un serveur local Alpine PHP 8.4.26 en CLI derrière nginx, soit la même build et le même modèle de workers que ceux sur lesquels les offsets ont été calibrés. Montez-le en une minute et toute la chaîne s'exécute de bout en bout sur votre propre machine. Le run en direct qui a réellement résolu le challenge est conservé comme note historique en bas.
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.
zif_system, le gestionnaire de PHP pour system.print-flag sur la connexion toujours ouverte.Pourquoi deux adresses ? Le système d'exploitation randomise indépendamment l'emplacement du code et du tas de PHP. La fuite du tas indique au script où placer la structure forgée, la fuite du code lui indique où réside réellement la fonction qu'il veut appeler. Les adresses provenant d'un worker antérieur ou d'une exécution précédente ne sont pas des substituts sûrs, et à chaque fois que je l'ai essayé, la tentative a simplement échoué.
Les offsets de ce PoC sont calibrés pour Alpine PHP 8.4.26 exécutant le serveur CLI intégré avec quatre workers. Le lab reproduit exactement cela, derrière nginx, sur l'interface loopback.
[!IMPORTANT] La compilation compte plus que la chaîne de version. Une pile Debian
php:8.4.26-fpmrapporte le mêmePHP/8.4.26mais a une disposition binaire différente, donc l'empreinte du pointeur de code ne correspondra jamais et l'exécution s'interrompt à l'étape de fuite de code. Utilisez l'image Alpine ci-dessous.
Trois petits fichiers. Placez-les dans un répertoire vide :
public/php-version.php (le point de terminaison de santé, et la seule page dont le PoC a besoin)
<?php
header('Content-Type: text/plain');
echo 'PHP ', PHP_VERSION, "\n";
echo 'PID: ', getmypid(), "\n";
print-flag (substitut du binaire du challenge, doit être exécutable)
#!/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;
}
}
}
Puis lancez-le :
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
Vérifiez-le :
$ curl -s http://127.0.0.1:18166/php-version.php
PHP 8.4.26
PID: 1
Le PID varie selon le worker qui répond, et c'est normal. La ligne de version est l'élément que le PoC vérifie.
Démontez-le avec docker rm -f ctf-cross-nginx ctf-cross-php && docker network rm ctf-cross-net.
[!TIP] Sous Windows, exécutez ces commandes
docker rundepuis PowerShell, ou remplacez$PWDpar un chemin de lecteur complet commeC:/lab. Dans Git Bash,$PWDs'étend en un chemin MSYS tel que/tmp/..., que Docker Desktop résout à l'intérieur de sa propre VM. Les montages bind créent alors silencieusement des répertoires vides là où devraient se trouvernginx.confetprint-flag, et les conteneurs démarrent cassés.