docs(research): TeamCity deserialization, coverage as implemented (#3189) - #3481
Conversation
|
One numeric detail worth pinning down before merge, so the count is reproducible rather than a number someone has to trust. I counted the rows in
The four
Row 4 is also a case where the TeamCity class must not win, so if "negatives" means every row the TeamCity class does not claim, the total is 11, not 10. The doc currently says 10 and explains it as "7 quoted in #3444's commit message, plus 3 added by #3464's rebase". That is a coherent account, and the three named rows (XML-RPC raw stream, bare container, base64 at offset zero) are all present and real. Could the doc state explicitly which definition it uses — negatives as empty-class assertions only (7), versus all rows the class does not claim (11) — and give the number under that definition? As written a reader counting rows in the test gets 11 and the doc says 10, with no way to tell whether that is an error or a different definition. Not blocking my merge decision; the substance is right and everything else I checked (the dispatch position claim, the two file:line citation sets, the raw-vs-lowered reasoning) is accurate. But a research doc whose value is being checkable should not hand the reader a number they cannot reproduce. |
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
…etector The issue body's STATUS block says the implementation PRs are open and awaiting merge. That is stale: the classifier shipped in #3444 and survived #3464's move of the cases out of main.go. The dispatch line in classify.go is on main, not behind a PR. This adds the missing research document and nothing else. No new detector, no re-implementation, no source change. The document describes the classifier as it actually exists, read from source: a conjunction of a serialization container (raw AC ED 00 05, its base64 form rO0AB anchored to a token start, or a gadget class name) and TeamCity's agent-protocol shape (a product-qualified marker, or one of eight call names as a whole value), at classify.go:78 -- second in a first-match-wins dispatch and ahead of the generic serialized-object case, which would otherwise claim the raw forms and leave the base64 and query forms unlabelled. Every claim is cited to file:line. Where the shipped implementation is narrower than the issue asked for, the gap is stated in Known gaps rather than glossed: the class labels an attempt and not an exploitation; the AWS/S3/backup chain, the Cadence connection and the attributed JetBrains advice all remain UNVERIFIED in the issue's own research and none of them is covered here; there is no persona, no bait endpoint and no path category. CVE facts are transcribed from the issue's primary-source verification matrix and carry its verdicts rather than a fresh reading. The matrix's negative findings are restated as negatives, and the 404 advisory URL is listed only so it is not re-derived as a citation. Elasticsearch field names beyond honeypot.payload_class are flagged as unmapped rather than invented, and no detection or false-positive rate is claimed: the pinned corpus has no entry of this class. tests/docs/ 659 passed, 1 xfailed, 17 subtests, before and after. check-docs-reachable and check-public-leaks both pass.
12d945e to
3658e7c
Compare
Closes #3189.
Deliverable
docs/research/3189-teamcity-deserialization.md— the research record for CVE-2026-63077 JetBrains TeamCity deserialization RCE, documenting the detector as actually shipped rather than as proposed.Docs only. Verified before dispatch:
classify_teamcity.go:49and the dispatch entry atclassify.go:78are already onmain; the issue body's "awaiting merge" status block is stale.Why dispatch position is load-bearing, not cosmetic
The class is second in a first-match-wins dispatch, ahead of the generic
serialized-objectcase. Below it, it would be dead code.wantPayloadClassOrder(classify_order_3464_test.go:422) pins that position andTestPayloadClassOrderIsPinnedcompares the dispatch against it row by row. The doc explains what each side of the boundary would claim if the order changed.The raw-vs-lowered split inside the predicate is documented as load-bearing with the reason:
strings.ToLowermaps every non-UTF-8 byte to U+FFFD, so the lowered copy of a rawAC ED 00 05header has lost the signature. The container is read from the raw strings, the ASCII product names from the lowered copies — which is also why the hot path allocates nothing.Honesty
## Known gapsstates the class labels an attempt, not an exploitation; the AWS chain and Cadence are uncovered; recognition is limited to the eight call names and four markers actually listed. No detection or false-positive rate is claimed — and the doc says the pinned corpus having no entry of this class is not evidence of absence.payload_class(main.go:125, read atevents.rs:206) andhoneypot.payload_classvia the Filebeat namespace. Everything past that is explicitly "to be mapped to the real schema".One correction to raise in review
The doc records the negative-case count as 10 (7 asserting the empty class, plus 3 asserting
serialized-object). My own count ofteamcity_deser_test.gorows finds 4 rows assertingserialized-object, not 3 — the fourth beinga neighbouring call name is not TeamCity's, which is also a case where the TeamCity class must not win. Depending on how "negative" is defined the figure is 10 or 11. Either is defensible, but the doc should state which definition it uses so the number is reproducible. Raised as a review comment rather than fixed silently.Verification
One file added, nothing else touched.
go test ./...inhttp-honeypotpasses on the branch.tests/docs/gate run before and after, no new failures. Author and committer bothXore <Xore@users.noreply.github.com>, no AI attribution trailers.