Project

General

Profile

Actions

Security #8853

open
SB VJ

ssl: integer underflow due to miscalculation in SSLv2 CLIENT_HELLO

Security #8853: ssl: integer underflow due to miscalculation in SSLv2 CLIENT_HELLO

Added by Shivani Bhardwaj about 1 month ago. Updated about 9 hours ago.

Status:
In Review
Priority:
Normal
Assignee:
Target version:
Affected Versions:
Label:
CVE:
Git IDs:
Severity:
LOW
Disclosure Date:
11/02/2026

Description

Reported by Communications Security Establishment (CSE):

## Summary

The SSLv2 record parser in `SSLv2Decode()` consumes six fixed `CLIENT_HELLO` body bytes
guarded only by the *buffer*-remaining length, not by the *record*-remaining length.
When an attacker sends an SSLv2 record whose declared `record_length` is between 1 and
6, `bytes_processed` overruns `record_length + record_lengths_length`, and the
subsequent `uint32_t diff = record_length + record_lengths_length - bytes_processed`
underflows to a value near `0xFFFFFFFA`. The wild `diff` is added to the `input` pointer
— undefined behaviour per C11 §6.5.6/8 — and the truncated return value causes
`SSLDecode()` to rewind and re-parse already-consumed body bytes as a fresh record
header. The bug is remotely reachable with a single 9-byte client→server TCP segment on
any TLS-enabled port and requires no special configuration.

## Affected Piece of Code

- **File:** `src/app-layer-ssl.c`
- **Function / Location:** `SSLv2Decode()` ~L2262-2376 (esp. L2263-2278 and L2359-2367)
- **Subsystem:** al-ssl — SSL/TLS handshake parser (record layer, extensions,
  certificates)

```c
/* src/app-layer-ssl.c */

2262        case SSLV2_MT_CLIENT_HELLO:
2263            if (input_len < 6) {
2264                SSLSetEvent(ssl_state, TLS_DECODER_EVENT_INVALID_SSL_RECORD);
2265                return SSL_DECODER_ERROR(-1);
2266            }
...
2275            uint16_t session_id_length = (input[5]) | (uint16_t)(input[4] << 8);
2276            input += 6;
2277            input_len -= 6;
2278            ssl_state->curr_connp->bytes_processed += 6;
...
2359    if (input_len + ssl_state->curr_connp->bytes_processed >=
2360            (ssl_state->curr_connp->record_length +
2361            ssl_state->curr_connp->record_lengths_length)) {
2362
2363        /* looks like we have another record after this */
2364        uint32_t diff = ssl_state->curr_connp->record_length +
2365                ssl_state->curr_connp->record_lengths_length + -
2366                ssl_state->curr_connp->bytes_processed;
2367        input += diff;
2368        SSLParserReset(ssl_state);
...
2376    return SSL_DECODER_OK((input - initial_input));
```

## The Bug

### Root cause

When an SSLv2 record carries `msg_type == SSLV2_MT_CLIENT_HELLO`, the parser at
L2262-2278 unconditionally reads six additional body bytes (client `version` (2),
`cipher_spec_length` (2), `session_id_length` (2)) and advances `input`, `input_len` and
`bytes_processed` by 6. The only guard is `if (input_len < 6)` at L2263, where
`input_len` is the number of bytes remaining in the **stream-slice buffer**, not the
number of bytes remaining inside the **current SSLv2 record**. Nothing checks that the
record itself is large enough to contain those six bytes.

After the header has been parsed by `SSLv2ParseRecord()` (L2098),
`bytes_processed == record_lengths_length + 1` (the length header plus the one
`msg_type` byte). Adding the six `CLIENT_HELLO` bytes yields
`bytes_processed == record_lengths_length + 7`. Control then falls through to L2359,
which is intended to advance `input` to the end of the current record so the next loop
iteration can start cleanly:

```c
uint32_t diff = record_length + record_lengths_length - bytes_processed;
input += diff;
```

This expression silently assumes
`bytes_processed <= record_length + record_lengths_length`. If the attacker sets
`record_length` to any value in `1..6` (e.g., bytes `80 01 01 ...` giving
`record_length = 1`), the right-hand side becomes negative before the implicit
conversion to `uint32_t`. With `record_length = 1`, `record_lengths_length = 2`,
`bytes_processed = 9`, we get `diff = 1 + 2 - 9 = (uint32_t)-6 = 0xFFFFFFFA`. The
statement `input += diff;` then forms a pointer roughly 4 GiB beyond the 9-byte input
buffer — undefined behaviour under C11 §6.5.6/8 even though the pointer is never
dereferenced.

The existing sanity checks do **not** catch this:

- The "have full record" check at L2227 compares `record_lengths_length + record_length`
  (`2 + 1 = 3`) against `input_len + bytes_processed` (`6 + 3 = 9`); `3 > 9` is false,
  so parsing continues.
- The `record_length == 0` check at L2241 is bypassed because `record_length == 1`.
- Neither check compares `record_length` against the six bytes about to be consumed in
  the `CLIENT_HELLO` case.

### Downstream effect — parser desynchronisation

`SSLv2Decode()` returns via `SSL_DECODER_OK((input - initial_input))` at L2376. On a
64-bit target the pointer subtraction yields a `ptrdiff_t` of `0x100000003` (the genuine
9 bytes consumed plus the wild `0xFFFFFFFA` advance). `SSL_DECODER_OK` stores this into
a `uint32_t retval`, truncating it to `0x00000003`. Back in `SSLDecode()` (L2730) the
guard `r.retval < 0 || r.retval > input_len` evaluates `3 > 9` → false, so the caller
believes only **3** bytes were consumed, advances `input` by 3, and re-enters the
`while (input_len > 0)` loop with `input` now pointing at offset 3 of the original
payload. The six bytes that were already parsed as the `CLIENT_HELLO` body
(`00 02 00 00 00 00`) are now re-interpreted as the start of a brand-new SSLv2/TLS
record header. Suricata's view of the SSLv2 record boundaries has diverged from the wire
by six bytes — a deterministic parser desync that an attacker can use to feed Suricata a
different record stream from the one the peer endpoint sees.

### Call chain to the sink

A single client→server TCP segment on any port where `ALPROTO_TLS` is detected (default
`443`, plus the probing-parser port set) drives the following chain:

1. `RegisterSSLParsers()` (`src/app-layer-ssl.c:3219`) registers `SSLParseClientRecord`
   as the `STREAM_TOSERVER` handler.
2. `SSLParseClientRecord()` (L2807) → `SSLDecode()` (L2658).
3. The dispatch loop at L2696 inspects `input[0]`; because `input[0] & 0x80` is set
   (L2710), it assigns `curr_connp->version = SSL_VERSION_2` and calls `SSLv2Decode()`
   (L2728-2729) → `SSLv2Decode()` (L2182).
4. `SSLv2Decode()` calls `SSLv2ParseRecord()` (L2208 → L2098).

With the 9-byte payload `80 01 01 00 02 00 00 00 00`:

- `SSLv2ParseRecord()` sees `input[0] & 0x80`, sets `record_lengths_length = 2`,
  computes `record_length = (0x7F & 0x80) << 8 | 0x01 = 1`, sets `content_type = 0x01`
  (`SSLV2_MT_CLIENT_HELLO`), `bytes_processed = 3`, returns `3`. `input` advances to
  offset 3, `input_len = 6`.
- L2227: `2 + 1 > 6 + 3` → false → continue.
- L2241: `record_length == 0` → false → continue.
- L2262 `case SSLV2_MT_CLIENT_HELLO`: L2263 tests `input_len < 6` (buffer-remaining),
  `6 < 6` → false. Reads `version = 0x0002`, `session_id_length = 0`, then `input += 6`,
  `input_len = 0`, `bytes_processed = 9`.
- L2359: `0 + 9 >= 1 + 2` → true.
- L2364: `diff = 1 + 2 - 9 = 0xFFFFFFFA`; L2367: `input += 0xFFFFFFFA` (**UB**); L2368:
  `SSLParserReset()`.
- L2376: returns `SSL_DECODER_OK(0x100000003)` → `retval = 3`.
- Caller L2730: `3 <= 9`, accepted; `input += 3`, `input_len -= 3`; loop iteration 2 now
  parses offsets `3..8` as a new record.

Protocol detection is satisfied without any prior traffic: `SSLProbingParser()` (L2941)
accepts the input because `(input[0] & 0x80) && input[2] == 0x01`, and the
pattern-matcher signature `|01 00 02|` registered at offset 2 (L3012) also matches
`bytes[2..4] = 01 00 02`.

**Vulnerability class:** network-reachable integer underflow → undefined-behaviour
pointer arithmetic and record-stream desynchronisation.

## Reproduction Results

No special configuration or rule is needed; the default `suricata.yaml` with the TLS
app-layer parser enabled (the default) suffices.

1. Establish a TCP three-way handshake from any client IP/port to
   `<sensor-monitored-host>:443` (or any port in the configured `tls` detection-ports
   list).
2. Send one client→server TCP data segment whose payload is exactly these 9 bytes (hex):

   ```
   80 01 01 00 02 00 00 00 00
   ```

   Field meaning as Suricata parses it:

   | Bytes  | Meaning |
   |--------|---------|
   | `80 01` | SSLv2 2-byte length header, high bit set, `record_length = 0x0001` |
   | `01`    | `msg_type = SSLV2_MT_CLIENT_HELLO` |
   | `00 02` | client version (read at L2272; value `0x0002` also satisfies PM pattern &#124;01 00 02&#124;) |
   | `00 00` | `cipher_spec_length` (unused on this path) |
   | `00 00` | `session_id_length = 0` |

	

3. Suricata's app-layer dispatch calls `SSLParseClientRecord` → `SSLDecode` →
`SSLv2Decode` with `input_len = 9`.
4. Inside `SSLv2Decode()` the underflow occurs at L2364-2367:
`diff = 1 + 2 - 9 = 0xFFFFFFFA` (`uint32_t`), then `input += 0xFFFFFFFA`.
5. Observable evidence without a debugger:
- On a build compiled with `-fsanitize=undefined` / `-fsanitize=pointer-overflow`,
the process aborts at `app-layer-ssl.c:2367` with a diagnostic of the form
"pointer index expression ... overflowed".
- On a release build, `SSLv2Decode()` returns `retval = 3` (not `9`); `SSLDecode()`
then re-enters the loop with `input` pointing at offset 3 (`00 02 00 00 00 00`) and
parses those body bytes as a fresh record — i.e., the SSLv2 record stream is
desynchronised by six bytes.

Variant: any `record_length` in `1..6` triggers the same underflow; e.g.,
`80 03 01 00 02 00 00 00 00` gives `diff = 3 + 2 - 9 = 0xFFFFFFFC` and `retval = 5`.

Pcap/scapy one-liner for the step-2 payload (after completing the handshake):

```python
send(IP/TCP/bytes.fromhex('800101000200000000'))
```

or simply:

```sh
printf '\x80\x01\x01\x00\x02\x00\x00\x00\x00' | ncat TARGET 443
```

Status of reproduction: This is an analytically-derived trigger. Every value in
the call chain above was traced statically through the current source; the arithmetic is
deterministic and depends on no compiler-defined behaviour prior to the UB at L2367. The
trigger has not been exercised against a live Suricata instance as part of this audit,
but no guard in the code path can reject the payload before the underflow is reached.

  1. Severity

LOW

Two effects flow from the bug:

1. Undefined behaviour. `input += 0xFFFFFFFA` forms a pointer roughly 4 GiB outside
the `StreamSlice` buffer. The pointer is never dereferenced, but its mere formation
is UB; a UBSan build aborts here, and an aggressively-optimising compiler is
permitted to assume the subtraction at L2364 cannot underflow, which could in
principle license unexpected transformations of the surrounding code.
2. Detection bypass / parser desync. Because the truncated `retval`
(`record_length + record_lengths_length`, e.g. `3`) is smaller than the 9 bytes
actually consumed, `SSLDecode()` rewinds and re-parses six attacker-controlled body
bytes as a new SSLv2/TLS record header. Suricata's view of record boundaries diverges
from the wire, so subsequent record-level inspection (frames, events, keyword
matching) operates on a stream the peer never sent.

Mitigating factors keep this at LOW:

- The dispatch loop in `SSLDecode()` is bounded by `max_records` (L2695-2703), so there
is no infinite loop or hang.
- No out-of-bounds read or write occurs; the wild pointer is used only for a difference
computation.
- Practical evasion value is limited: a real TLS server would reject an SSLv2 record
with `record_length = 1` that carries 7+ body bytes, so this cannot cloak a working
handshake. The primary concern is the formal UB and localised intra-slice parser
confusion rather than a viable end-to-end TLS-inspection bypass.

  1. Suggested Fix

Validate that the SSLv2 record actually contains the six fixed `CLIENT_HELLO` bytes
before consuming them. The `msg_type` byte has already been counted against
`record_length`, so at least 7 record bytes are required. This guarantees
`bytes_processed` can never exceed `record_length + record_lengths_length`, which in
turn makes the subtraction at L2364 safe.

```diff
--- a/src/app-layer-ssl.c
+++ b/src/app-layer-ssl.c
@ -2260,7 +2260,9 @ static struct SSLDecoderResult SSLv2Decode(uint8_t direction, SSLState *ssl_sta
break;

case SSLV2_MT_CLIENT_HELLO:
- if (input_len < 6) {
+ /* we already consumed 1 byte (msg_type) of record_length; need 6 more
+ * (version(2) + cipher_spec_len(2) + session_id_len(2)) inside the record */
+ if (input_len < 6 || ssl_state->curr_connp->record_length < 7) {
SSLSetEvent(ssl_state, TLS_DECODER_EVENT_INVALID_SSL_RECORD);
return SSL_DECODER_ERROR(-1);
}
```

Optional defence-in-depth at the sink: before computing `diff` at L2364, bail out if
`bytes_processed > record_length + record_lengths_length` (set
`TLS_DECODER_EVENT_INVALID_SSL_RECORD` and return `SSL_DECODER_ERROR(-1)`). This ensures
any future code path that over-consumes record bytes cannot reintroduce the underflow.


Subtasks 1 (1 open0 closed)

Security #8927: ssl: integer underflow due to miscalculation in SSLv2 CLIENT_HELLO (8.0.x backport)AssignedVictor JulienActions

VJ Updated by Victor Julien about 1 month ago Actions #1

  • Subject changed from Integer underflow in SSLv2 CLIENT_HELLO record-end calculation causes UB pointer arithmetic and parser desync to ssl: Integer underflow in SSLv2 CLIENT_HELLO record-end calculation causes UB pointer arithmetic and parser desync

VJ Updated by Victor Julien 30 days ago Actions #2

  • Status changed from New to In Progress
  • Assignee changed from OISF Dev to Victor Julien

JI Updated by Jason Ish 16 days ago Actions #3

  • Description updated (diff)

JI Updated by Jason Ish 16 days ago Actions #4

  • GHSA set to GHSA-gqq5-xc2p-x395

JI Updated by Jason Ish 15 days ago Actions #5

  • Status changed from In Progress to In Review
  • Label Needs backport to 8.0 added

MR on GL for review.

OT Updated by OISF Ticketbot 15 days ago Actions #6

  • Subtask #8927 added

OT Updated by OISF Ticketbot 15 days ago Actions #7

  • Label deleted (Needs backport to 8.0)

PA Updated by Philippe Antoine 10 days ago Actions #8

  • Severity set to LOW

I propose LOW severity as SSLv2 is obsolete and it is just parser desync

SB Updated by Shivani Bhardwaj 5 days ago Actions #9

  • Subject changed from ssl: Integer underflow in SSLv2 CLIENT_HELLO record-end calculation causes UB pointer arithmetic and parser desync to ssl: integer underflow due to miscalculation in SSLv2 CLIENT_HELLO

VJ Updated by Victor Julien 1 day ago Actions #10

  • Status changed from In Review to Resolved

Staged.

JI Updated by Jason Ish 1 day ago Actions #12

  • Disclosure Date set to 11/02/2026

VJ Updated by Victor Julien about 9 hours ago Actions #13

  • Status changed from Resolved to In Review
  • Private changed from Yes to No
Actions

Also available in: PDF Atom