
Una versión pública del desafío Steal My Bitcoin
Un callback PHP en vivo contra el desafío Steal My Bitcoin, de principio a fin, en un único archivo Python de biblioteca estándar.
Este README explica stealmybitcoin_poc.py, una reproducción en Python de un solo archivo de la secuencia de peticiones usada en mi ejecución del CTF Steal My Bitcoin. Solo utiliza la biblioteca estándar de Python. El script construye las peticiones por sí mismo, verifica cada filtración antes de continuar y registra los bytes HTTP de una nueva ejecución.
Esto probablemente se pueda hacer más fácilmente sin toda la parafernalia extra de depuración y, como descubrí en mi vídeo de John Hammond sobre la Escalada de Privilegios Locales de Steam, probablemente podrías limpiar esta fea PoC.
[!NOTE] ⚠️ Sinceramente, por favor límpiala o haz tu propia versión mejor que no esté destrozada por Sol. ⚠️
Todo lo que sigue está escrito contra el laboratorio Docker, no contra el desafío en vivo. El laboratorio es un servidor CLI local de Alpine PHP 8.4.26 detrás de nginx, que es la misma compilación y modelo de workers sobre el que se calibraron los offsets. Levántalo en un minuto y toda la cadena se ejecuta de principio a fin en tu propia máquina. La ejecución en vivo que realmente resolvió el desafío se conserva como nota histórica al final.
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, el manejador de PHP para system.print-flag a través de la conexión que sigue abierta.¿Por qué dos direcciones? El sistema operativo aleatoriza dónde se ubican el código y el heap de PHP, de forma independiente. La fuga del heap le indica al script dónde colocar la estructura falsificada, y la fuga del código le indica dónde vive realmente la función que quiere llamar. Las direcciones de un worker anterior o de una ejecución anterior no son sustitutos seguros, y cada vez que lo intenté, el intento simplemente falló.
Los offsets de esta PoC están calibrados para Alpine PHP 8.4.26 ejecutando el servidor CLI integrado con cuatro workers. El laboratorio reproduce exactamente eso, detrás de nginx, en loopback.
[!IMPORTANT] La compilación importa más que la cadena de versión. Una pila de Debian
php:8.4.26-fpmreporta el mismoPHP/8.4.26pero tiene un diseño de binario diferente, por lo que la huella del puntero de código nunca coincidirá y la ejecución se aborta en la etapa de fuga de código. Usa la imagen de Alpine a continuación.
Tres archivos pequeños. Colócalos en un directorio vacío:
public/php-version.php (el endpoint de salud, y la única página que la PoC necesita)
<?php
header('Content-Type: text/plain');
echo 'PHP ', PHP_VERSION, "\n";
echo 'PID: ', getmypid(), "\n";
print-flag (sustituto del binario del reto, debe ser ejecutable)
#!/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;
}
}
}
Luego actívalo:
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
Compruébalo:
$ curl -s http://127.0.0.1:18166/php-version.php
PHP 8.4.26
PID: 1
El PID varía según qué worker responda, y eso está bien. La línea de versión es la parte que verifica el PoC.
Desmontarlo con docker rm -f ctf-cross-nginx ctf-cross-php && docker network rm ctf-cross-net.
[!TIP] En Windows, ejecute esos comandos
docker rundesde PowerShell, o reemplace$PWDcon una ruta de unidad completa comoC:/lab. En Git Bash$PWDse expande a una ruta de MSYS como/tmp/..., que Docker Desktop resuelve dentro de su propia VM. Los bind mounts entonces crean silenciosamente directorios vacíos donde deberían estarnginx.confyprint-flag, y los contenedores arrancan rotos.