
Публичный релиз задачи Steal My Bitcoin
Один живой PHP-колбэк против задачи Steal My Bitcoin, от начала до конца, в одном файле Python со стандартной библиотекой.
Этот README описывает stealmybitcoin_poc.py — однофайловое воспроизведение на Python последовательности запросов, использованной в моём прохождении CTF Steal My Bitcoin. Используется только стандартная библиотека Python. Скрипт сам формирует запросы, проверяет каждую утечку перед переходом к следующему шагу и записывает HTTP-байты нового запуска.
Вероятно, это можно сделать проще, без всей лишней отладочной мишуры, и, как я обнаружил в своём видео про John Hammond о повышении привилегий в Steam Local, этот уродливый PoC наверняка можно причесать.
[!NOTE] ⚠️ Честно говоря, пожалуйста, причешите его или сделайте свою лучшую версию, не скомканную Sol. ⚠️
Всё ниже написано для Docker-лаборатории, а не для живого челленджа. Лаборатория — это локальный Alpine PHP 8.4.26 CLI-сервер за nginx, та же сборка и та же модель воркеров, на которых калибровались смещения. Поднимите её за минуту, и вся цепочка пройдёт от начала до конца на вашей машине. Живой запуск, который действительно решил задачу, сохранён как историческая заметка внизу.
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 — обработчик PHP для system.print-flag по всё ещё открытому соединению.Почему два адреса? ОС рандомизирует расположение кода и кучи PHP независимо. Утечка кучи сообщает скрипту, куда поместить поддельную структуру, а утечка кода сообщает, где на самом деле находится функция, которую он хочет вызвать. Адреса из более раннего воркера или более раннего запуска не являются безопасной заменой, и каждый раз, когда я пробовал это, попытка просто проваливалась.
Смещения в этом PoC откалиброваны для Alpine PHP 8.4.26, запущенного со встроенным CLI-сервером с четырьмя воркерами. Лаборатория воспроизводит именно это, за nginx, на loopback.
[!IMPORTANT] Сборка важнее строки версии. Стек Debian
php:8.4.26-fpmсообщает тот жеPHP/8.4.26, но имеет другую компоновку бинарника, поэтому отпечаток указателя кода никогда не совпадёт, и запуск прервётся на этапе утечки кода. Используйте образ Alpine, приведённый ниже.
Три небольших файла. Поместите их в пустой каталог:
public/php-version.php (эндпоинт проверки работоспособности и единственная страница, которая нужна PoC)
<?php
header('Content-Type: text/plain');
echo 'PHP ', PHP_VERSION, "\n";
echo 'PID: ', getmypid(), "\n";
print-flag (заглушка для бинарника задания, должна быть исполняемой)
#!/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;
}
}
}
Затем запустите его:
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
Проверьте:
$ curl -s http://127.0.0.1:18166/php-version.php
PHP 8.4.26
PID: 1
PID зависит от того, какой воркер отвечает, и это нормально. Строка версии — это то, что проверяет PoC.
Удалите это с помощью docker rm -f ctf-cross-nginx ctf-cross-php && docker network rm ctf-cross-net.
[!TIP] В Windows запускайте эти команды
docker runиз PowerShell или замените$PWDна полный путь к диску, напримерC:/lab. В Git Bash$PWDразворачивается в путь MSYS, такой как/tmp/..., который Docker Desktop разрешает внутри своей собственной VM. Тогда bind-монтирования молча создают пустые каталоги там, где должны бытьnginx.confиprint-flag, и контейнеры запускаются сломанными.
Несколько замечаний по этому рецепту. Образ — это тег fpm, но контейнер запускает php -S, поэтому SAPI — это cli-server, и именно относительно него проводилась калибровка. PHP_CLI_SERVER_WORKERS=4 — это то, что даёт этапам утечки более одного воркера для гонки. Мой собственный лабораторный стенд также передавал --cap-add SYS_PTRACE --security-opt seccomp=unconfined, чтобы можно было подключить трассировщик к воркерам; сам PoC этого никогда не делает, поэтому выше они опущены.
Python 3.11 или новее, только стандартная библиотека, ничего устанавливать не нужно:
python .\stealmybitcoin_poc.py --url http://127.0.0.1:18166/php-version.php
Запустите его без аргументов — он выведет справку и завершится, не обращаясь к сети.
Успешный запуск выглядит так: