Security #8857
Updated by Jason Ish 16 days ago
Reported by Communications Security Establishment (CSE): <pre> ## Summary The TLS ClientHello extension parser passes JA3 scratch buffers into per-extension helper functions **by value** (`JA3Buffer *`). When `Ja3BufferAddValue()` hits an allocation failure inside one of those helpers, it frees the underlying `JA3Buffer` and NULLs only the helper's *local* copy of the pointer; the caller in `TLSDecodeHSHelloExtensions()` still holds the now-dangling pointer and, on the `goto end;` error path, hands it to `Ja3BufferAppendBuffer()`. That function dereferences the freed struct (use-after-free read), passes its stale `->data` pointer to `snprintf`, and finally calls `Ja3BufferFree()` on it again — a double-free of the `JA3Buffer` and a wild `SCFree` of whatever `->data` now contains. The defect is reachable from a network ClientHello whenever JA3 fingerprinting is enabled, but the corrupting branch is gated on `SCMalloc`/`SCRealloc` returning NULL, so practical exploitation requires concurrent memory pressure. ## Affected Piece of Code - **File:** `src/app-layer-ssl.c` - **Function / Location:** `TLSDecodeHSHelloExtensionEllipticCurves()` ~L1046–1100, `TLSDecodeHSHelloExtensionEllipticCurvePF()` ~L1102–1153, and the `end:` label of `TLSDecodeHSHelloExtensions()` ~L1439–1473 - **Subsystem:** al-ssl — SSL/TLS handshake parser (record layer, extensions, certificates) ```c /* src/app-layer-ssl.c */ 1046 static inline int TLSDecodeHSHelloExtensionEllipticCurves(SSLState *ssl_state, 1047 const uint8_t * const initial_input, 1048 const uint32_t input_len, 1049 JA3Buffer *ja3_elliptic_curves) /* <-- BY VALUE */ ... 1077 if (TLSDecodeValueIsGREASE(elliptic_curve) != 1) { 1078 int rc = Ja3BufferAddValue(&ja3_elliptic_curves, /* address of LOCAL copy */ 1079 elliptic_curve); 1080 if (rc != 0) 1081 return -1; /* buffer already freed */ ... 1326 case SSL_EXTENSION_ELLIPTIC_CURVES: 1327 { 1328 /* coverity[tainted_data] */ 1329 ret = TLSDecodeHSHelloExtensionEllipticCurves(ssl_state, input, 1330 ext_len, 1331 ja3_elliptic_curves); 1332 if (ret < 0) 1333 goto end; /* caller's pointer is dangling */ ... 1439 end: 1440 if (ja3) { ... 1446 if (ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) { 1447 rc = Ja3BufferAppendBuffer(&ssl_state->curr_connp->ja3_str, 1448 &ja3_elliptic_curves); /* UAF read + double-free */ ``` Supporting code in `src/util-ja3.c`: ```c 172 int Ja3BufferAddValue(JA3Buffer **buffer, uint32_t value) ... 179 if ((*buffer)->data == NULL) { 180 (*buffer)->data = SCMalloc(JA3_BUFFER_INITIAL_SIZE); 181 if ((*buffer)->data == NULL) { 182 SCLogError("Error allocating memory for JA3 data"); 183 Ja3BufferFree(buffer); /* frees struct, NULLs *buffer (the LOCAL one) */ 184 return -1; 185 } ... 191 int rc = Ja3BufferResizeIfFull(*buffer, value_len); 192 if (rc != 0) { 193 Ja3BufferFree(buffer); /* same on realloc failure */ 194 return -1; 195 } ``` ## The Bug ### Call chain from the wire A TCP stream on a TLS port (e.g. 443) is delivered by the app-layer engine to the registered TOSERVER parser: ``` RegisterSSLParsers() src/app-layer-ssl.c:3219 → SSLParseClientRecord() :2807 → SSLDecode() :2658 → SSLv3Decode() :2756 → :2379 → SSLv3ParseHandshakeProtocol() :2590 → :1688 → SSLv3ParseHandshakeType() :1607 handshake_type == SSLV3_HS_CLIENT_HELLO sets SSL_AL_FLAG_STATE_CLIENT_HELLO :1617 → TLSDecodeHandshakeHello() :1622 → :1477 → TLSDecodeHSHelloExtensions() :1529 → :1251 ``` Inside `TLSDecodeHSHelloExtensions()`, with JA3 enabled (`SC_ATOMIC_GET(ssl_config.enable_ja3) == 1`, line 1260–1261) and `SSL_AL_FLAG_STATE_CLIENT_HELLO` set, the function allocates three local `JA3Buffer *` variables via `Ja3BufferInit()` (lines 1268, 1273, 1277). When the extension loop encounters extension type `0x000a` (`SSL_EXTENSION_ELLIPTIC_CURVES`, line 1326), it calls: ```c ret = TLSDecodeHSHelloExtensionEllipticCurves(ssl_state, input, ext_len, ja3_elliptic_curves); /* :1329-1331 */ ``` The `JA3Buffer *` is passed **by value**. Inside the helper (`:1046-1049`) the parameter `ja3_elliptic_curves` is therefore an independent local copy of the caller's pointer. ### The free that the caller cannot see For each non-GREASE curve in the extension, the helper does: ```c int rc = Ja3BufferAddValue(&ja3_elliptic_curves, elliptic_curve); /* :1078 */ ``` Note `&ja3_elliptic_curves` — the address of the *local parameter*. `Ja3BufferAddValue()` (`util-ja3.c:172`) is designed so that on allocation failure it cleans up after itself: if `SCMalloc(JA3_BUFFER_INITIAL_SIZE)` at `util-ja3.c:180` returns NULL, or if `Ja3BufferResizeIfFull()` at `:191` fails its `SCRealloc`, the function executes `Ja3BufferFree(buffer)` (`:183` / `:193`). `Ja3BufferFree()` calls `SCFree((*buffer)->data)`, `SCFree(*buffer)`, and writes `*buffer = NULL`. Because `buffer` here is `&<local parameter>`, only the helper's stack slot is NULLed. The helper then returns `-1` (`:1081`). ### The dangling pointer is reused Back in `TLSDecodeHSHelloExtensions()`: ```c if (ret < 0) goto end; /* :1332-1333 -- NOT goto error */ ``` The caller's `ja3_elliptic_curves` was never updated; it still points at the `JA3Buffer` struct that was just `SCFree()`d. Control jumps to the `end:` label (`:1439`). Because `ja3` is true and `SSL_AL_FLAG_STATE_CLIENT_HELLO` is set, the cleanup-and-merge block runs: ```c rc = Ja3BufferAppendBuffer(&ssl_state->curr_connp->ja3_str, &ja3_elliptic_curves); /* :1447-1448 */ ``` `Ja3BufferAppendBuffer()` (`util-ja3.c:108`) begins with a NULL guard: ```c if (*buffer1 == NULL || *buffer2 == NULL) { ... return -1; } /* :110 */ ``` This guard **passes** — `*buffer2` is the dangling, non-NULL pointer. The function then: 1. Reads `(*buffer2)->data`, `(*buffer2)->used`, `(*buffer2)->size` (`:118-119`, `:125`, `:132`) — **use-after-free reads** of a freed heap chunk. 2. If `(*buffer1)->data != NULL`, passes the stale `(*buffer2)->data` pointer to `snprintf("%s", ...)` (`:136-138`) — a wild read of arbitrary length terminated only by a NUL byte in whatever memory now occupies that address. 3. Finally calls `Ja3BufferFree(buffer2)` (`:141`, or `SCFree(*buffer2)` at `:121` on the copy path) — a **double-free** of the `JA3Buffer` struct, plus an `SCFree()` of the stale `->data` field (a **wild free** of an attacker-influenceable pointer if the freed chunk was reallocated in the interim). Note that even if step 1 had failed the NULL guard, the alternative `goto error;` path at `:1466-1472` would *also* re-free the dangling pointer via `Ja3BufferFree(&ja3_elliptic_curves)` (`:1469-1470`), since that block only checks `!= NULL`. There is no safe exit once the helper has freed the buffer. ### Twin instance: ec_point_formats The identical pattern exists for extension type `0x000b` (`SSL_EXTENSION_EC_POINT_FORMATS`, `:1340`) via `TLSDecodeHSHelloExtensionEllipticCurvePF()` (`:1102-1105`, by-value parameter; `Ja3BufferAddValue(&ja3_elliptic_curves_pf, ...)` at `:1131`; `return -1` at `:1134`; `goto end;` at `:1346-1347`; reuse at `:1452-1453`). ### Required input To reach the vulnerable call site the attacker must send a TLS record with content-type `0x16` (Handshake), record version `0x0301`–`0x0303`, handshake type `0x01` (ClientHello), at least one cipher suite, and an extensions block containing type `0x000a` with at least one non-GREASE curve ID (e.g. `0x0017`). JA3 fingerprinting must be enabled in the sensor configuration. To reach the *defective branch*, `SCMalloc(128)` or the subsequent `SCRealloc` must return NULL while that curve list is being processed — a process-level memory-exhaustion condition, not a value the packet itself can encode. **Vulnerability class:** network-reachable use-after-free / double-free (CWE-416 / CWE-415), conditional on allocator failure. ## Reproduction Results 1. **Configuration prerequisite.** Enable JA3 fingerprinting so that `SC_ATOMIC_GET(ssl_config.enable_ja3) == 1`. Either set in `suricata.yaml`: ```yaml app-layer: protocols: tls: ja3-fingerprints: yes ``` or load any rule that uses `ja3.hash` / `ja3.string`, which auto-enables JA3. 2. **Drive the parser to the vulnerable site.** Send a single TCP segment (client → server, e.g. dst port 443) carrying a TLS 1.2 ClientHello with a `supported_groups` (elliptic_curves) extension. Hex layout of the TCP payload: ``` 16 03 01 00 47 ; TLS record: Handshake, v1.0, length 0x47 (71) 01 00 00 43 ; Handshake: ClientHello, length 0x43 (67) 03 03 ; client_version TLS 1.2 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; 32-byte random 00 ; session_id length = 0 00 02 c0 2c ; cipher_suites_len=2, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 01 00 ; compression_methods_len=1, null 00 18 ; extensions_len = 24 00 0a 00 14 ; ext_type=0x000a (elliptic_curves), ext_len=20 00 12 ; elliptic_curves_len = 18 (9 curves) 00 17 00 18 00 19 00 1d 00 1e 00 1f 00 20 00 21 00 22 ; 9 non-GREASE curve IDs ``` This deterministically reaches `TLSDecodeHSHelloExtensionEllipticCurves()` and invokes `Ja3BufferAddValue()` once per curve. 3. **Force the allocation-failure branch.** This is the non-deterministic gate: `SCMalloc(128)` at `util-ja3.c:180` (first curve) or `SCRealloc` inside `Ja3BufferResizeIfFull` (subsequent curves) must return NULL while step 2 is being parsed. Practical lab approximations: - **(a)** Run Suricata under a tight cgroup memory limit / `ulimit -v` and flood it with many concurrent large flows so the heap is exhausted at the moment the ClientHello arrives; or - **(b)** Use fault injection: build with `-Wl,--wrap=malloc` (or `LD_PRELOAD` a failmalloc shim) configured to fail the N-th allocation while replaying the pcap from step 2. 4. **Observed effect when the allocation fails.** `Ja3BufferAddValue()` frees the `JA3Buffer` and the helper returns `-1`; control reaches `end:` and `Ja3BufferAppendBuffer()` dereferences and re-frees the dangling pointer → glibc `double free or corruption` abort, or under AddressSanitizer a `heap-use-after-free` followed by `attempting double-free` report pointing at `util-ja3.c:118` / `util-ja3.c:141`. **Status: analytical only.** A deterministic network-only trigger could not be constructed because the defective branch is gated on `SCMalloc`/`SCRealloc` returning NULL (`util-ja3.c:180-184` / `191-194`), which depends on process memory state rather than on any byte in the ClientHello. The packet in step 2 reliably reaches the vulnerable call site; step 3 (allocation failure) is the blocker for a pure-pcap PoC and requires external memory pressure or fault injection to reproduce. ## Severity **MEDIUM** This is a use-after-free read of a freed `JA3Buffer` struct followed by a double-free of that struct — and a wild `SCFree` of its stale `->data` field — inside the TLS app-layer parser, which runs on untrusted network input. On glibc the double-free typically aborts the Suricata process: denial of service of the IDS/IPS sensor, and therefore a detection-bypass window while it is down or restarting. Because the freed chunk's contents are re-read and passed both to `snprintf("%s", ...)` and to `SCFree()`, an attacker who can (a) induce memory pressure to hit the failing `SCMalloc`/`SCRealloc` and (b) groom the heap so that the freed `JA3Buffer` slot is reallocated with controlled bytes between the free and the reuse could in principle escalate to an arbitrary-free / heap-metadata-corruption primitive, raising the theoretical ceiling toward remote code execution. In practice, exploitation is heavily constrained by the very small window between free and reuse (same packet-processing call frame) and by the OOM precondition. Net realistic impact: the bug turns a recoverable allocation failure into a hard crash of the sensor. ## Suggested Fix Pass the JA3 buffers to the per-extension helpers **by reference** (`JA3Buffer **`) so that when `Ja3BufferAddValue()` frees-and-NULLs on allocation failure, the caller's variable is NULLed as well. The existing NULL checks in `Ja3BufferAppendBuffer()` (`util-ja3.c:110`) and in the `error:` cleanup (`app-layer-ssl.c:1467-1472`) then make the path safe. Apply the same change to the `EllipticCurvePF` helper. ```diff --- a/src/app-layer-ssl.c +++ b/src/app-layer-ssl.c @@ -1046,7 +1046,7 @@ static inline int TLSDecodeHSHelloExtensionEllipticCurves(SSLState *ssl_state, const uint8_t * const initial_input, const uint32_t input_len, - JA3Buffer *ja3_elliptic_curves) + JA3Buffer **ja3_elliptic_curves) { @@ -1066,7 +1066,7 @@ - if ((ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) && ja3_elliptic_curves) { + if ((ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) && *ja3_elliptic_curves) { @@ -1078,7 +1078,7 @@ - int rc = Ja3BufferAddValue(&ja3_elliptic_curves, + int rc = Ja3BufferAddValue(ja3_elliptic_curves, elliptic_curve); @@ -1102,7 +1102,7 @@ static inline int TLSDecodeHSHelloExtensionEllipticCurvePF(SSLState *ssl_state, const uint8_t * const initial_input, const uint32_t input_len, - JA3Buffer *ja3_elliptic_curves_pf) + JA3Buffer **ja3_elliptic_curves_pf) { @@ -1122,7 +1122,7 @@ - if ((ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) && ja3_elliptic_curves_pf) { + if ((ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) && *ja3_elliptic_curves_pf) { @@ -1131,7 +1131,7 @@ - int rc = Ja3BufferAddValue(&ja3_elliptic_curves_pf, + int rc = Ja3BufferAddValue(ja3_elliptic_curves_pf, elliptic_curve_pf); @@ -1329,7 +1329,7 @@ ret = TLSDecodeHSHelloExtensionEllipticCurves(ssl_state, input, ext_len, - ja3_elliptic_curves); + &ja3_elliptic_curves); @@ -1343,7 +1343,7 @@ ret = TLSDecodeHSHelloExtensionEllipticCurvePF(ssl_state, input, ext_len, - ja3_elliptic_curves_pf); + &ja3_elliptic_curves_pf); ``` **Alternative minimal fix:** change `goto end;` at lines 1333 and 1347 to `goto error;` **and** remove the `Ja3BufferFree()` calls inside `Ja3BufferAddValue()` so that ownership stays with the caller on failure. The double-pointer approach above is preferred because it preserves the existing free-on-failure contract of `Ja3BufferAddValue()` for every other call site that already relies on it. </pre>