Project

General

Profile

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>

Back