reassembly: the newer package has the same SYN-payload substitution as tcpassembly, by a different mechanism #14

Open
opened 2026-08-26 10:15:30 +00:00 by claude · 0 comments
Collaborator

Severity: high · reassembly/tcpassembly.go:691-698,724

Follow-up to #3. I noted there that "the same code shape is present in reassembly/". That turned out to be half right in a way worth writing down: reassembly fixed the sequence arithmetic that tcpassembly gets wrong, and still has the identical attacker primitive.

This matters because reassembly is the newer package and the one new code is steered toward.

What reassembly gets right

if half.nextSeq == invalidSequence {
        if t.SYN {
                seq = seq.Add(1)          // correct: a SYN consumes exactly one sequence number
                half.nextSeq = seq
                action.queue = false
        }

Compare tcpassembly/assembly.go:579, which does conn.nextSeq = seq.Add(len(bytes) + 1) — advancing by the payload length as well. reassembly is right and tcpassembly is wrong.

What it still gets wrong

Twenty-six lines later:

action = a.handleBytes(bytes, seq, half, t.SYN, t.RST || t.FIN, action, ac)

bytes is the SYN's payload and seq has already been advanced to ISN+1. So the SYN's payload is emitted as stream data at exactly the offset where the real request will arrive. The genuine segment at ISN+1 is then contiguous with data already delivered and is trimmed as an overlap.

The net effect on the reconstructed stream is the same as #3: analyser_stream = SYN_payload || real_request[N:].

Reproduction

reassembly/ (the newer package):
  plain SYN         -> "GET /flag?a=1 HTTP/1.1\r\nUser-Agent: sqlmap\r\n\r\n"
  SYN + "XXXX"      -> "XXXX/flag?a=1 HTTP/1.1\r\nUser-Agent: sqlmap\r\n\r\n"
  SYN + 45 B cover  -> "GET /healthz HTTP/1.1\r\nUser-Agent: kube-probe\r\n"

  server actually receives: "GET /flag?a=1 HTTP/1.1\r\nUser-Agent: sqlmap\r\n\r\n"

The third line is the whole attack: the analyser reconstructs a complete, well-formed request to /healthz from kube-probe, and the real sqlmap request against /flag never appears in the stream at all — the cover is longer than the request, so nothing of the original survives the overlap trim. A Linux listener that has not negotiated TCP Fast Open discards the SYN's data entirely and serves the real request normally.

Cost: bytes carried on the SYN, which the attacker was sending anyway.

Fix

Do not treat a SYN's payload as stream data:

if t.SYN {
        seq = seq.Add(1)
        half.nextSeq = seq
        action.queue = false
        bytes = nil        // a non-TFO listener discards data on a SYN
}

or, if TFO support is wanted, gate it behind an explicit AssemblerOptions flag rather than making it the default for every SYN.

tcpcheck.go is a natural second place to enforce this — it already inspects SYN flags and options for the state machine, and a TCPOptionCheck that saw no TFO option in the handshake knows the SYN payload cannot have been delivered.

Related: #3 (the tcpassembly variant, plus the !ACK insertion primitive, which should be checked here too).


Verified against b7d9dbd on Go 1.24.4. PoC: reasm_syn on branch pentest/2026-08-poc.

**Severity: high** · `reassembly/tcpassembly.go:691-698,724` Follow-up to #3. I noted there that "the same code shape is present in `reassembly/`". That turned out to be half right in a way worth writing down: `reassembly` fixed the sequence arithmetic that `tcpassembly` gets wrong, and **still has the identical attacker primitive**. This matters because `reassembly` is the newer package and the one new code is steered toward. ## What reassembly gets right ```go if half.nextSeq == invalidSequence { if t.SYN { seq = seq.Add(1) // correct: a SYN consumes exactly one sequence number half.nextSeq = seq action.queue = false } ``` Compare `tcpassembly/assembly.go:579`, which does `conn.nextSeq = seq.Add(len(bytes) + 1)` — advancing by the payload length as well. `reassembly` is right and `tcpassembly` is wrong. ## What it still gets wrong Twenty-six lines later: ```go action = a.handleBytes(bytes, seq, half, t.SYN, t.RST || t.FIN, action, ac) ``` `bytes` is the SYN's payload and `seq` has already been advanced to `ISN+1`. So the SYN's payload is emitted as stream data **at exactly the offset where the real request will arrive**. The genuine segment at `ISN+1` is then contiguous with data already delivered and is trimmed as an overlap. The net effect on the reconstructed stream is the same as #3: `analyser_stream = SYN_payload || real_request[N:]`. ## Reproduction ``` reassembly/ (the newer package): plain SYN -> "GET /flag?a=1 HTTP/1.1\r\nUser-Agent: sqlmap\r\n\r\n" SYN + "XXXX" -> "XXXX/flag?a=1 HTTP/1.1\r\nUser-Agent: sqlmap\r\n\r\n" SYN + 45 B cover -> "GET /healthz HTTP/1.1\r\nUser-Agent: kube-probe\r\n" server actually receives: "GET /flag?a=1 HTTP/1.1\r\nUser-Agent: sqlmap\r\n\r\n" ``` The third line is the whole attack: the analyser reconstructs a complete, well-formed request to `/healthz` from `kube-probe`, and the real sqlmap request against `/flag` never appears in the stream at all — the cover is longer than the request, so nothing of the original survives the overlap trim. A Linux listener that has not negotiated TCP Fast Open discards the SYN's data entirely and serves the real request normally. Cost: bytes carried on the SYN, which the attacker was sending anyway. ## Fix Do not treat a SYN's payload as stream data: ```go if t.SYN { seq = seq.Add(1) half.nextSeq = seq action.queue = false bytes = nil // a non-TFO listener discards data on a SYN } ``` or, if TFO support is wanted, gate it behind an explicit `AssemblerOptions` flag rather than making it the default for every SYN. `tcpcheck.go` is a natural second place to enforce this — it already inspects SYN flags and options for the state machine, and a `TCPOptionCheck` that saw no TFO option in the handshake knows the SYN payload cannot have been delivered. Related: #3 (the `tcpassembly` variant, plus the `!ACK` insertion primitive, which should be checked here too). --- *Verified against `b7d9dbd` on Go 1.24.4. PoC: `reasm_syn` on branch `pentest/2026-08-poc`.*
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
noi/gopacket#14
No description provided.