
Un rilascio pubblico della sfida Steal My Bitcoin
Un callback PHP live contro la challenge Steal My Bitcoin, dall'inizio alla fine, in un singolo file Python con sola libreria standard.
Questo README spiega stealmybitcoin_poc.py, una riproduzione Python in un singolo file della sequenza di richieste usata nella mia esecuzione della CTF Steal My Bitcoin. Usa solo la libreria standard di Python. Lo script costruisce da sé le richieste, verifica ogni leak prima di proseguire e registra i byte HTTP di una nuova esecuzione.
Probabilmente si può fare più facilmente senza tutta la roba extra di debug e, come ho scoperto nel mio video su John Hammond riguardo alla Steam Local Privilege Escalation, potresti probabilmente ripulire questo brutto PoC.
[!NOTE] ⚠️ Onestamente, per favore ripuliscilo o crea la tua versione migliore che non sia accartocciata da Sol. ⚠️
Tutto quanto segue è scritto per il laboratorio Docker, non per la challenge live. Il laboratorio è un server CLI Alpine PHP 8.4.26 locale dietro nginx, che è la stessa build e lo stesso modello di worker su cui sono stati calibrati gli offset. Avvialo in un minuto e l'intera catena gira end to end sulla tua macchina. L'esecuzione live che ha effettivamente risolto la challenge è conservata come nota storica in fondo.
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, il gestore di PHP per system.print-flag sulla connessione ancora aperta.Perché due indirizzi? Il sistema operativo randomizza in modo indipendente dove finiscono il codice e lo heap di PHP. La fuga dello heap dice allo script dove collocare la struttura contraffatta, la fuga del codice gli dice dove risiede effettivamente la funzione che vuole chiamare. Gli indirizzi di un worker precedente o di un'esecuzione precedente non sono sostituti sicuri, e ogni volta che ci ho provato il tentativo è semplicemente fallito.
Gli offset in questo PoC sono calibrati per Alpine PHP 8.4.26 in esecuzione con il server CLI integrato e quattro worker. Il laboratorio riproduce esattamente questo, dietro nginx, su loopback.
[!IMPORTANT] La build conta più della stringa di versione. Uno stack Debian
php:8.4.26-fpmriporta lo stessoPHP/8.4.26ma ha un layout del binario diverso, quindi l'impronta del puntatore al codice non corrisponderà mai e l'esecuzione si interrompe nella fase di fuga del codice. Usa l'immagine Alpine qui sotto.
Tre piccoli file. Mettili in una directory vuota:
public/php-version.php (l'endpoint di health, e l'unica pagina di cui il PoC ha bisogno)
<?php
header('Content-Type: text/plain');
echo 'PHP ', PHP_VERSION, "\n";
echo 'PID: ', getmypid(), "\n";
print-flag (sostituto del binario della challenge, deve essere eseguibile)
#!/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;
}
}
}
Poi avvialo:
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
Controllalo:
$ curl -s http://127.0.0.1:18166/php-version.php
PHP 8.4.26
PID: 1
Il PID varia a seconda di quale worker risponde, e va bene così. La riga della versione è la parte che il PoC controlla.
Smontalo con docker rm -f ctf-cross-nginx ctf-cross-php && docker network rm ctf-cross-net.
[!TIP] Su Windows, esegui quei comandi
docker runda PowerShell, oppure sostituisci$PWDcon un percorso completo di unità comeC:/lab. In Git Bash$PWDsi espande in un percorso MSYS come/tmp/..., che Docker Desktop risolve all'interno della propria VM. I bind mount creano quindi silenziosamente directory vuote dove dovrebbero trovarsinginx.confeprint-flag, e i container si avviano in modo difettoso.
Un paio di note su quella ricetta. L'immagine è il tag fpm ma il container esegue php -S, quindi la SAPI è cli-server, che è quella su cui è stata misurata la calibrazione. PHP_CLI_SERVER_WORKERS=4 è ciò che dà alle fasi di leak più di un worker su cui gareggiare. Il mio harness di laboratorio passava anche --cap-add SYS_PTRACE --security-opt seccomp=unconfined così da poter collegare un tracer ai worker; il PoC stesso non lo fa mai, quindi sono stati omessi sopra.