
Uma versão pública do desafio Steal My Bitcoin
Um callback PHP ao vivo contra o desafio Steal My Bitcoin, do início ao fim, em um único arquivo Python de biblioteca padrão.
Este README explica o stealmybitcoin_poc.py, uma reprodução em Python de arquivo único da sequência de requisições usada na minha execução do CTF Steal My Bitcoin. Ele usa apenas a biblioteca padrão do Python. O script constrói as requisições por conta própria, verifica cada vazamento antes de prosseguir e registra os bytes HTTP de uma nova execução.
Isso provavelmente pode ser feito de forma mais fácil sem toda a parafernália extra de depuração e, como descobri no meu vídeo do John Hammond sobre a Escalação de Privilégios Local do Steam, você provavelmente poderia limpar esta PoC feia.
[!NOTE] ⚠️ Honestamente, por favor, limpe isso ou faça sua própria versão melhor que não esteja amassada pelo Sol. ⚠️
Tudo abaixo foi escrito para o laboratório Docker, não para o desafio ao vivo. O laboratório é um servidor CLI Alpine PHP 8.4.26 local atrás do nginx, que é a mesma build e modelo de worker nos quais os offsets foram calibrados. Monte-o em um minuto e toda a cadeia roda de ponta a ponta na sua própria máquina. A execução ao vivo que de fato resolveu o desafio está mantida como nota histórica no 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, o handler do PHP para system.print-flag pela conexão que ainda está aberta.Por que dois endereços? O SO randomiza onde o código e o heap do PHP ficam, de forma independente. O vazamento de heap diz ao script onde colocar a estrutura forjada, o vazamento de código diz onde a função que ele quer chamar realmente reside. Endereços de um worker anterior ou de uma execução anterior não são substitutos seguros, e toda vez que tentei, a tentativa simplesmente falhou.
Os offsets neste PoC são calibrados para Alpine PHP 8.4.26 executando o servidor CLI embutido com quatro workers. O laboratório reproduz exatamente isso, atrás do nginx, em loopback.
[!IMPORTANT] A build importa mais que a string de versão. Uma stack Debian
php:8.4.26-fpmreporta o mesmoPHP/8.4.26, mas tem um layout de binário diferente, então a impressão digital do ponteiro de código nunca corresponderá e a execução aborta no estágio de vazamento de código. Use a imagem Alpine abaixo.
Três pequenos arquivos. Coloque-os num diretório vazio:
public/php-version.php (o endpoint de saúde, e a única página que o PoC precisa)
<?php
header('Content-Type: text/plain');
echo 'PHP ', PHP_VERSION, "\n";
echo 'PID: ', getmypid(), "\n";
print-flag (substituto do binário do desafio, deve ser executável)
#!/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;
}
}
}
Em seguida, inicialize-o:
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
Verifique:
$ curl -s http://127.0.0.1:18166/php-version.php
PHP 8.4.26
PID: 1
O PID varia conforme o worker que responde, e isso é normal. A linha de versão é a parte que o PoC verifica.
Derrube tudo com docker rm -f ctf-cross-nginx ctf-cross-php && docker network rm ctf-cross-net.
[!TIP] No Windows, execute esses comandos
docker runa partir do PowerShell, ou substitua$PWDpor um caminho completo de unidade comoC:/lab. No Git Bash,$PWDexpande para um caminho MSYS como/tmp/..., que o Docker Desktop resolve dentro de sua própria VM. Os bind mounts então criam silenciosamente diretórios vazios ondenginx.confeprint-flagdeveriam estar, e os containers sobem quebrados.
Algumas observações sobre essa receita. A imagem é a tag fpm, mas o container executa php -S, então o SAPI é cli-server, que é contra o qual a calibração foi medida. PHP_CLI_SERVER_WORKERS=4 é o que dá aos estágios de vazamento mais de um worker para disputar. Meu próprio harness de laboratório também passava --cap-add SYS_PTRACE --security-opt seccomp=unconfined para poder anexar um tracer aos workers; o PoC em si nunca faz isso, então esses parâmetros foram omitidos acima.
Python 3.11 ou mais recente, apenas biblioteca padrão, nada para instalar: