{"repro_id":"REPRO-2026-00329","version":6,"title":"JetBrains TeamCity On-Premises unauthenticated RCE via agent polling protocol","repro_type":"security","status":"published","severity":"critical","description":"JetBrains TeamCity On-Premises is vulnerable to unauthenticated remote code execution via the agent polling protocol. An attacker with HTTP(S) access to a TeamCity server can bypass authentication checks and execute arbitrary operating system commands with the privileges of the TeamCity server process. JetBrains states the issue affects all TeamCity On-Premises versions and is fixed in 2025.11.7 and 2026.1.3. A security patch plugin is available for 2017.1+ installations. TeamCity Cloud is not affected.","root_cause":"# CVE-2026-63077 — Root Cause Analysis\n\n## Summary\n\nJetBrains TeamCity On-Premises is vulnerable to unauthenticated remote code execution\n(CWE-502, deserialization of untrusted data) in its **agent polling protocol**. The\nserver-side handler `jetbrains.buildServer.agentServer.polling.Error.fromXml()` (and the\nsibling `XStreamHolder`s in `PollingRemoteAgentConnection`, `RunBuildCommandResult`, and\n`NodesAwareLogMessagePersister`) deserializes attacker-controlled HTTP request bodies with\nan XStream instance configured with `AnyTypePermission.ANY` and only a small denylist.\nAn unauthenticated attacker first registers a synthetic build agent via\n`POST /app/agents/v1/register` (which issues a valid `TeamCity-AgentSessionId` without any\ncredentials), then posts a crafted XStream XML document to\n`POST /app/agents/v1/commands/error`. The embedded gadget chain starts an HSQLDB\nconnection whose `connectionInitSqls` drop a self-deleting `.jspws` webshell into the\nTeamCity webroot; a single GET to that file executes an arbitrary OS command with the\nprivileges of the TeamCity server process.\n\n## Impact\n\n- Package/component: JetBrains TeamCity On-Premises server (`webapps/ROOT` webapp,\n  classes in `server-core.jar`, `common-impl.jar`, `messages.jar`, `web-core.jar`).\n- Affected versions: all TeamCity On-Premises versions before 2025.11.7 / 2026.1.3\n  (verified vulnerable: 2025.11.6, build 208214; verified fixed: 2025.11.7).\n- Risk: CVSS 3.1 9.8 Critical (AV:N/AC:L/PR:N/UI:N). Listed in CISA KEV\n  (added 2026-08-05) with confirmed in-the-wild exploitation. Full server compromise:\n  arbitrary OS command execution as the TeamCity server user, access to build secrets,\n  source code, CI/CD pipeline integrity.\n\n## Impact Parity\n\n- Disclosed/claimed maximum impact: unauthenticated remote code execution.\n- Reproduced impact from this run: unauthenticated remote OS command execution\n  (`touch <marker>` executed as `tcuser`, the TeamCity server process user, inside the\n  official `jetbrains/teamcity-server:2025.11.6-linux` container), proven by the\n  command-created marker file and by the one-shot JSPWS response token.\n- Parity: **full**.\n- Not demonstrated: nothing material — the claim is unauthenticated RCE and exactly that\n  was demonstrated, twice, through the real HTTP surface.\n\n## Root Cause\n\nThe agent polling protocol is served by\n`jetbrains.buildServer.controllers.agentServer.AgentPollingProtocolController`\n(`web-core.jar`), reachable under `/app/agents/v1/...` with **no servlet-level\nauthentication**: agent identity is established only by the `TeamCity-AgentSessionId`\nheader (`<agentId>:<authorizationToken>`), and a fresh valid session is handed out by the\nunauthenticated `register` action to any caller\n(`createRegisteredAgentWithPollingConnection` → `registerAgent` → session id in the\n`TeamCity-AgentSessionId` response header).\n\nFor the `commands/error` sub-path, `AbstractAgentCommandsRequestsProcessor.\nhandleCommandIsFailedRequest` executes:\n\n```java\nError error = Error.fromXml(StreamUtil.readTextFrom(request.getReader()));  // <- sink\nint n = Integer.parseInt(request.getHeader(\"TeamCity-AgentCommandId\"));\n```\n\n`Error.fromXml` → `XStreamWrapper.deserializeObject(xml, ourXStreamHolder)`.\n`jetbrains.buildServer.messages.XStreamHolder` (messages.jar) configures its XStream as:\n\n```java\nxstream.addPermission(AnyTypePermission.ANY);\nxstream.denyTypes(new String[]{ \"java.beans.EventHandler\", \"java.lang.ProcessBuilder\",\n    \"javax.imageio.ImageIO$ContainsFilter\", \"jdk.nashorn.internal.objects.NativeString\",\n    \"com.sun.corba.se.impl.activation.ServerTableEntry\",\n    \"com.sun.tools.javac.processing.JavacProcessingEnvironment$NameProcessIterator\",\n    \"sun.awt.datatransfer.DataTransferer$IndexOrderComparator\", \"sun.swing.SwingLazyValue\"});\nxstream.denyTypesByRegExp(/* LazyIterator, LazyEnumeration, GetterSetterReflection,\n    PrivilegedGetter, java.rmi, javax.crypto, ServiceNameIterator, JavaFX, BCEL */);\n```\n\ni.e. an \"allow everything except a 2016-era blacklist\" configuration. Bundled libraries\n(commons-collections 3.2.2, freemarker 2.3.31, commons-dbcp2/pool2, hsqldb, plus\nTeamCity's own classes) provide all the gadget classes needed for code execution.\n\nThe exploit gadget chain (identical to the in-the-wild chain captured by honeypots and\ndocumented by Rapid7):\n\n1. `linked-hash-map` entry value typed as TeamCity's own\n   `jetbrains.buildServer.serverSide.metadata.impl.metadata.HSQLMetadataStorage$SchemaMismatchException`\n   (a `Throwable`, so it passes XStream 1.4.20's default hierarchy permission). Its\n   declared fields instantiate `HSQLStorage` with a DBCP2 `BasicDataSource` whose\n   `driverClassName=org.hsqldb.jdbc.JDBCDriver`, `url=jdbc:hsqldb:mem:<rand>`, and three\n   attacker-controlled `connectionInitSqls`.\n2. A `freemarker.ext.beans.HashAdapter` whose `falseModel.object` is an XStream\n   `reference=` to that `BasicDataSource`, giving a `Map` view whose `get(\"connection\")`\n   invokes `BasicDataSource.getConnection()` via FreeMarker bean introspection.\n3. A `set` containing `org.apache.commons.collections.keyvalue.TiedMapEntry` (not covered\n   by commons-collections 3.2.2's `readObject` serialization guard) bound to that map with\n   key `\"connection\"`. During `HashSet` population, `TiedMapEntry.hashCode()` →\n   `getValue()` → `map.get(\"connection\")` → `BasicDataSource.getConnection()` → DBCP runs\n   the three init SQL statements against the in-memory HSQLDB:\n   `CREATE TABLE`, `INSERT '<JSP scriptlet>'`, and `SCRIPT '../webapps/ROOT/<rand>.jspws'`,\n   which writes a polyglot SQL/JSP webshell into the TeamCity webroot.\n4. `GET /<rand>.jspws` compiles and runs the scriptlet, which deletes itself and calls\n   `java.lang.Runtime.getRuntime().exec(<attacker command>)`, printing a per-run token.\n\nFix (confirmed by decompiling the official `fix_CVE_2026_63077.zip` security patch plugin,\nbuild limit `max-build=\"222648\"`): the patch reflectively replaces every\n`XStreamHolder` used by the polling protocol (`PollingRemoteAgentConnection.myXStreamHolder`,\n`Error.xStreamHolder`, `RunBuildCommandResult.ourXStreamHolder`,\n`NodesAwareLogMessagePersister.xStreamHolder`) with a wrapper whose `getXStream()` adds\n`NoTypePermission.NONE` plus an explicit allowlist of ~100 `jetbrains.buildServer.*` data\nclasses. It also installs an `AddToQueuePreprocessor` that strips queued builds carrying\nthe `teamcity.agent.internal.passwords.values` parameter. Fixed releases 2025.11.7 /\n2026.1.3 ship the same allowlist natively.\n\n## Reproduction Steps\n\n1. `bundle/repro/reproduction_steps.sh` (self-contained; requires docker, python3, curl).\n2. The script:\n   - pulls the pinned official images `jetbrains/teamcity-server@sha256:a435d8…4176`\n     (2025.11.6, vulnerable) and `…@sha256:d3875b…56d8` (2025.11.7, fixed);\n   - starts both servers and drives the real first-run setup wizard over HTTP\n     (`/mnt/do/goNewInstallation` → `/mnt/do/goNewDatabase` (internal HSQLDB) →\n     `/mnt/do/acceptLicenseAgreement`) until the server leaves maintenance mode;\n   - health-checks the attack surface by registering an agent **without credentials** and\n     verifying a `TeamCity-AgentSessionId` header is issued;\n   - runs the exploit (`bundle/repro/exploit_cve_2026_63077.py`, vendored Rapid7 PoC)\n     twice against the vulnerable server and twice against the fixed server, with\n     per-run random markers;\n   - requires, on the vulnerable server: exploit exit 0 **and** the marker file present\n     inside the container (created by the TeamCity server process);\n   - requires, on the fixed server: exploit failure, no marker file, and\n     `com.thoughtworks.xstream.security.ForbiddenClassException` in the server log\n     (the exact IoC JetBrains names for a blocked exploit attempt).\n3. Expected evidence: `[+] Command executed: touch /tmp/CVE_2026_63077_PWNED_<rand>` for\n   2025.11.6, `HTTP 404` for the webshell on 2025.11.7, and `RESULT: … CONFIRMED`.\n\n## Evidence\n\n- `bundle/logs/reproduction_steps.log` — full orchestration log.\n- `bundle/logs/exploit_vulnerable.log` — two successful exploit runs:\n  register → `TeamCity-AgentSessionId: <id>:<token>` → `/app/agents/v1/commands/error`\n  HTTP 500 (deserialization side effects already committed) → `GET /<rand>.jspws` HTTP 200\n  with the per-run response token.\n- `bundle/repro/marker_vulnerable.txt` — `ls -la` of the marker file (owner `tcuser`) and\n  `id` of the server process user inside the container.\n- `bundle/logs/teamcity_vuln_server.log` — vulnerable server log containing the\n  `com.thoughtworks.xstream.converters.ConversionException` IoC named in JetBrains'\n  guidance.\n- `bundle/logs/exploit_fixed.log`, `bundle/logs/teamcity_fixed_server.log` — fixed server:\n  same requests, `ForbiddenClassException` ×2, webshell GET → HTTP 404, no marker.\n- `bundle/repro/payload_vulnerable.xml` — the exact attack XML generated for the run.\n- `bundle/repro/analysis/` — patch-diff evidence: decompiled JetBrains security patch\n  plugin classes, decoded allowlist, decompiled `Error`/`AgentPollingProtocolController`/\n  `AbstractAgentCommandsRequestsProcessor`/`XStreamHolder` from 2025.11.6, and the\n  in-the-wild honeypot pcap (`CVE-2026-63077-itw.pcap`, BoredHackerBlog) showing the\n  identical request sequence.\n- Environment: official Docker images on linux/amd64; TeamCity 2025.11.6 (build 208214)\n  with bundled Tomcat 9.0.109 / JetBrains Runtime 21; no sanitizer, no instrumentation.\n\n## Recommendations / Next Steps\n\n- Upgrade to TeamCity 2025.11.7 or 2026.1.3, or install JetBrains' `fix_CVE_2026_63077`\n  security patch plugin (2017.1+; restart required on 2017.1–2018.1).\n- Restrict network access to the server (the agent polling protocol is same-port HTTP(S))\n  to trusted build-agent networks.\n- Detection: server logs containing `ConversionException` (possible attempt/success) or\n  `ForbiddenClassException` (blocked attempt on patched servers); unexpected unauthorized\n  agents (in-the-wild agents used names starting with `scan`); unexpected `.jspws`/`.jsp`\n  files under `webapps/ROOT`.\n- The correct fix pattern is exactly what JetBrains shipped: never deserialize the polling\n  protocol with `AnyTypePermission.ANY`; use `NoTypePermission.NONE` + a strict allowlist.\n\n## Additional Notes\n\n- Idempotency: the script recreates both containers from pinned image digests on every\n  run and uses fresh random markers/tokens, so consecutive runs are independent.\n- The exploit does not depend on the `TeamCity-AgentCommandId` value (deserialization\n  happens before the header is parsed); any integer suffices.\n- On the vulnerable server the `/commands/error` request returns HTTP 500 *after* the\n  gadget side effects have executed — the 500 is expected and matches the in-the-wild\n  capture.\n- Exploit helper provenance: `bundle/repro/exploit_cve_2026_63077.py` is the public\n  Rapid7 PoC (github.com/sfewer-r7/CVE-2026-63077), used unmodified; the same chain was\n  independently captured in the wild (pcap in `bundle/repro/analysis/`).\n- The default `--webroot-relative ../webapps/ROOT` is correct for the official Linux\n  Docker image (JVM working directory `/opt/teamcity/bin`).\n","cve_id":"CVE-2026-63077","cwe_id":"CWE-502 (Deserialization of Untrusted Data)","source_url":"https://blog.jetbrains.com/teamcity/2026/07/cve-2026-63077","reproduced_at":"2026-08-23T15:38:48.280866+00:00","duration_secs":6055.0,"tool_calls":347,"handoffs":2,"total_cost_usd":11.404034,"agent_costs":{"claim_matcher":0.023167,"judge":0.379977,"learning_policy":0.024283,"repro":8.5031,"support":0.092973,"vuln_variant":2.380534},"cost_breakdown":{"claim_matcher":{"gpt-5.4-mini-2026-03-17":0.023167},"judge":{"gpt-5.6-sol":0.379977},"learning_policy":{"gpt-5.4-mini-2026-03-17":0.024283},"repro":{"accounts/fireworks/models/kimi-k3":8.5031},"support":{"accounts/fireworks/models/kimi-k3":0.092973},"vuln_variant":{"accounts/fireworks/models/kimi-k3":2.380534}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"evidence":{"workflow":{"profile":"known_vulnerability","schema_version":2,"stages":["support","claim_contract","repro","judge","vuln_variant"]}},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-08-23T15:38:48.891432+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":11197,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":12046,"category":"reproduction_script"},{"path":"bundle/logs/exploit_fixed.log","filename":"exploit_fixed.log","size":1006,"category":"log"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":3256,"category":"log"},{"path":"bundle/logs/teamcity_fixed_server.log","filename":"teamcity_fixed_server.log","size":33226,"category":"log"},{"path":"bundle/repro/analysis/CVE-2026-63077-itw.pcap","filename":"CVE-2026-63077-itw.pcap","size":25913,"category":"other"},{"path":"bundle/repro/analysis/fix_CVE_2026_63077.zip","filename":"fix_CVE_2026_63077.zip","size":14722,"category":"other"},{"path":"bundle/repro/exploit_cve_2026_63077.py","filename":"exploit_cve_2026_63077.py","size":16448,"category":"script"},{"path":"bundle/repro/payload_fixed.xml","filename":"payload_fixed.xml","size":5379,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1417,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1213,"category":"other"}]}