# REPRO-2026-00329: JetBrains TeamCity On-Premises unauthenticated RCE via agent polling protocol ## Summary Status: published Severity: critical CVSS: Unknown CWE: CWE-502 (Deserialization of Untrusted Data) (Deserialization of Untrusted Data) Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00329 CVE: CVE-2026-63077 ## Package Name: Unknown Ecosystem: Unknown Affected: Unknown Fixed: Unknown ## Root Cause # CVE-2026-63077 — Root Cause Analysis ## Summary JetBrains TeamCity On-Premises is vulnerable to unauthenticated remote code execution (CWE-502, deserialization of untrusted data) in its **agent polling protocol**. The server-side handler `jetbrains.buildServer.agentServer.polling.Error.fromXml()` (and the sibling `XStreamHolder`s in `PollingRemoteAgentConnection`, `RunBuildCommandResult`, and `NodesAwareLogMessagePersister`) deserializes attacker-controlled HTTP request bodies with an XStream instance configured with `AnyTypePermission.ANY` and only a small denylist. An unauthenticated attacker first registers a synthetic build agent via `POST /app/agents/v1/register` (which issues a valid `TeamCity-AgentSessionId` without any credentials), then posts a crafted XStream XML document to `POST /app/agents/v1/commands/error`. The embedded gadget chain starts an HSQLDB connection whose `connectionInitSqls` drop a self-deleting `.jspws` webshell into the TeamCity webroot; a single GET to that file executes an arbitrary OS command with the privileges of the TeamCity server process. ## Impact - Package/component: JetBrains TeamCity On-Premises server (`webapps/ROOT` webapp, classes in `server-core.jar`, `common-impl.jar`, `messages.jar`, `web-core.jar`). - Affected versions: all TeamCity On-Premises versions before 2025.11.7 / 2026.1.3 (verified vulnerable: 2025.11.6, build 208214; verified fixed: 2025.11.7). - Risk: CVSS 3.1 9.8 Critical (AV:N/AC:L/PR:N/UI:N). Listed in CISA KEV (added 2026-08-05) with confirmed in-the-wild exploitation. Full server compromise: arbitrary OS command execution as the TeamCity server user, access to build secrets, source code, CI/CD pipeline integrity. ## Impact Parity - Disclosed/claimed maximum impact: unauthenticated remote code execution. - Reproduced impact from this run: unauthenticated remote OS command execution (`touch ` executed as `tcuser`, the TeamCity server process user, inside the official `jetbrains/teamcity-server:2025.11.6-linux` container), proven by the command-created marker file and by the one-shot JSPWS response token. - Parity: **full**. - Not demonstrated: nothing material — the claim is unauthenticated RCE and exactly that was demonstrated, twice, through the real HTTP surface. ## Root Cause The agent polling protocol is served by `jetbrains.buildServer.controllers.agentServer.AgentPollingProtocolController` (`web-core.jar`), reachable under `/app/agents/v1/...` with **no servlet-level authentication**: agent identity is established only by the `TeamCity-AgentSessionId` header (`:`), and a fresh valid session is handed out by the unauthenticated `register` action to any caller (`createRegisteredAgentWithPollingConnection` → `registerAgent` → session id in the `TeamCity-AgentSessionId` response header). For the `commands/error` sub-path, `AbstractAgentCommandsRequestsProcessor. handleCommandIsFailedRequest` executes: ```java Error error = Error.fromXml(StreamUtil.readTextFrom(request.getReader())); // <- sink int n = Integer.parseInt(request.getHeader("TeamCity-AgentCommandId")); ``` `Error.fromXml` → `XStreamWrapper.deserializeObject(xml, ourXStreamHolder)`. `jetbrains.buildServer.messages.XStreamHolder` (messages.jar) configures its XStream as: ```java xstream.addPermission(AnyTypePermission.ANY); xstream.denyTypes(new String[]{ "java.beans.EventHandler", "java.lang.ProcessBuilder", "javax.imageio.ImageIO$ContainsFilter", "jdk.nashorn.internal.objects.NativeString", "com.sun.corba.se.impl.activation.ServerTableEntry", "com.sun.tools.javac.processing.JavacProcessingEnvironment$NameProcessIterator", "sun.awt.datatransfer.DataTransferer$IndexOrderComparator", "sun.swing.SwingLazyValue"}); xstream.denyTypesByRegExp(/* LazyIterator, LazyEnumeration, GetterSetterReflection, PrivilegedGetter, java.rmi, javax.crypto, ServiceNameIterator, JavaFX, BCEL */); ``` i.e. an "allow everything except a 2016-era blacklist" configuration. Bundled libraries (commons-collections 3.2.2, freemarker 2.3.31, commons-dbcp2/pool2, hsqldb, plus TeamCity's own classes) provide all the gadget classes needed for code execution. The exploit gadget chain (identical to the in-the-wild chain captured by honeypots and documented by Rapid7): 1. `linked-hash-map` entry value typed as TeamCity's own `jetbrains.buildServer.serverSide.metadata.impl.metadata.HSQLMetadataStorage$SchemaMismatchException` (a `Throwable`, so it passes XStream 1.4.20's default hierarchy permission). Its declared fields instantiate `HSQLStorage` with a DBCP2 `BasicDataSource` whose `driverClassName=org.hsqldb.jdbc.JDBCDriver`, `url=jdbc:hsqldb:mem:`, and three attacker-controlled `connectionInitSqls`. 2. A `freemarker.ext.beans.HashAdapter` whose `falseModel.object` is an XStream `reference=` to that `BasicDataSource`, giving a `Map` view whose `get("connection")` invokes `BasicDataSource.getConnection()` via FreeMarker bean introspection. 3. A `set` containing `org.apache.commons.collections.keyvalue.TiedMapEntry` (not covered by commons-collections 3.2.2's `readObject` serialization guard) bound to that map with key `"connection"`. During `HashSet` population, `TiedMapEntry.hashCode()` → `getValue()` → `map.get("connection")` → `BasicDataSource.getConnection()` → DBCP runs the three init SQL statements against the in-memory HSQLDB: `CREATE TABLE`, `INSERT ''`, and `SCRIPT '../webapps/ROOT/.jspws'`, which writes a polyglot SQL/JSP webshell into the TeamCity webroot. 4. `GET /.jspws` compiles and runs the scriptlet, which deletes itself and calls `java.lang.Runtime.getRuntime().exec()`, printing a per-run token. Fix (confirmed by decompiling the official `fix_CVE_2026_63077.zip` security patch plugin, build limit `max-build="222648"`): the patch reflectively replaces every `XStreamHolder` used by the polling protocol (`PollingRemoteAgentConnection.myXStreamHolder`, `Error.xStreamHolder`, `RunBuildCommandResult.ourXStreamHolder`, `NodesAwareLogMessagePersister.xStreamHolder`) with a wrapper whose `getXStream()` adds `NoTypePermission.NONE` plus an explicit allowlist of ~100 `jetbrains.buildServer.*` data classes. It also installs an `AddToQueuePreprocessor` that strips queued builds carrying the `teamcity.agent.internal.passwords.values` parameter. Fixed releases 2025.11.7 / 2026.1.3 ship the same allowlist natively. ## Reproduction Steps 1. `bundle/repro/reproduction_steps.sh` (self-contained; requires docker, python3, curl). 2. The script: - pulls the pinned official images `jetbrains/teamcity-server@sha256:a435d8…4176` (2025.11.6, vulnerable) and `…@sha256:d3875b…56d8` (2025.11.7, fixed); - starts both servers and drives the real first-run setup wizard over HTTP (`/mnt/do/goNewInstallation` → `/mnt/do/goNewDatabase` (internal HSQLDB) → `/mnt/do/acceptLicenseAgreement`) until the server leaves maintenance mode; - health-checks the attack surface by registering an agent **without credentials** and verifying a `TeamCity-AgentSessionId` header is issued; - runs the exploit (`bundle/repro/exploit_cve_2026_63077.py`, vendored Rapid7 PoC) twice against the vulnerable server and twice against the fixed server, with per-run random markers; - requires, on the vulnerable server: exploit exit 0 **and** the marker file present inside the container (created by the TeamCity server process); - requires, on the fixed server: exploit failure, no marker file, and `com.thoughtworks.xstream.security.ForbiddenClassException` in the server log (the exact IoC JetBrains names for a blocked exploit attempt). 3. Expected evidence: `[+] Command executed: touch /tmp/CVE_2026_63077_PWNED_` for 2025.11.6, `HTTP 404` for the webshell on 2025.11.7, and `RESULT: … CONFIRMED`. ## Evidence - `bundle/logs/reproduction_steps.log` — full orchestration log. - `bundle/logs/exploit_vulnerable.log` — two successful exploit runs: register → `TeamCity-AgentSessionId: :` → `/app/agents/v1/commands/error` HTTP 500 (deserialization side effects already committed) → `GET /.jspws` HTTP 200 with the per-run response token. - `bundle/repro/marker_vulnerable.txt` — `ls -la` of the marker file (owner `tcuser`) and `id` of the server process user inside the container. - `bundle/logs/teamcity_vuln_server.log` — vulnerable server log containing the `com.thoughtworks.xstream.converters.ConversionException` IoC named in JetBrains' guidance. - `bundle/logs/exploit_fixed.log`, `bundle/logs/teamcity_fixed_server.log` — fixed server: same requests, `ForbiddenClassException` ×2, webshell GET → HTTP 404, no marker. - `bundle/repro/payload_vulnerable.xml` — the exact attack XML generated for the run. - `bundle/repro/analysis/` — patch-diff evidence: decompiled JetBrains security patch plugin classes, decoded allowlist, decompiled `Error`/`AgentPollingProtocolController`/ `AbstractAgentCommandsRequestsProcessor`/`XStreamHolder` from 2025.11.6, and the in-the-wild honeypot pcap (`CVE-2026-63077-itw.pcap`, BoredHackerBlog) showing the identical request sequence. - Environment: official Docker images on linux/amd64; TeamCity 2025.11.6 (build 208214) with bundled Tomcat 9.0.109 / JetBrains Runtime 21; no sanitizer, no instrumentation. ## Recommendations / Next Steps - Upgrade to TeamCity 2025.11.7 or 2026.1.3, or install JetBrains' `fix_CVE_2026_63077` security patch plugin (2017.1+; restart required on 2017.1–2018.1). - Restrict network access to the server (the agent polling protocol is same-port HTTP(S)) to trusted build-agent networks. - Detection: server logs containing `ConversionException` (possible attempt/success) or `ForbiddenClassException` (blocked attempt on patched servers); unexpected unauthorized agents (in-the-wild agents used names starting with `scan`); unexpected `.jspws`/`.jsp` files under `webapps/ROOT`. - The correct fix pattern is exactly what JetBrains shipped: never deserialize the polling protocol with `AnyTypePermission.ANY`; use `NoTypePermission.NONE` + a strict allowlist. ## Additional Notes - Idempotency: the script recreates both containers from pinned image digests on every run and uses fresh random markers/tokens, so consecutive runs are independent. - The exploit does not depend on the `TeamCity-AgentCommandId` value (deserialization happens before the header is parsed); any integer suffices. - On the vulnerable server the `/commands/error` request returns HTTP 500 *after* the gadget side effects have executed — the 500 is expected and matches the in-the-wild capture. - Exploit helper provenance: `bundle/repro/exploit_cve_2026_63077.py` is the public Rapid7 PoC (github.com/sfewer-r7/CVE-2026-63077), used unmodified; the same chain was independently captured in the wild (pcap in `bundle/repro/analysis/`). - The default `--webroot-relative ../webapps/ROOT` is correct for the official Linux Docker image (JVM working directory `/opt/teamcity/bin`). ## Reproduction Details Reproduced: 2026-08-23T15:38:48.280Z Duration: 6055 seconds Tool calls: 347 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00329 pruva-verify CVE-2026-63077 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00329&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00329/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-63077 - Source: https://blog.jetbrains.com/teamcity/2026/07/cve-2026-63077 ## Artifacts - bundle/repro/rca_report.md (analysis, 11197 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 12046 bytes) - bundle/logs/exploit_fixed.log (log, 1006 bytes) - bundle/logs/reproduction_steps.log (log, 3256 bytes) - bundle/logs/teamcity_fixed_server.log (log, 33226 bytes) - bundle/repro/analysis/CVE-2026-63077-itw.pcap (other, 25913 bytes) - bundle/repro/analysis/fix_CVE_2026_63077.zip (other, 14722 bytes) - bundle/repro/exploit_cve_2026_63077.py (script, 16448 bytes) - bundle/repro/payload_fixed.xml (other, 5379 bytes) - bundle/repro/runtime_manifest.json (other, 1417 bytes) - bundle/repro/validation_verdict.json (other, 1213 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00329 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00329/artifacts/bundle/repro/reproduction_steps.sh - Web: https://www.pruva.dev/reproductions/REPRO-2026-00329 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://www.pruva.dev