regreSSHion — OpenSSH (CVE-2024-6387)

regreSSHion — OpenSSH (CVE-2024-6387)

TL;DR: A race in sshd’s SIGALRM handler calls async-signal-unsafe glibc functions, letting a remote attacker corrupt heap state and execute code as root with no credentials.

What it is

Qualys disclosed this in July 2024. It is a regression of CVE-2006-5051: a 2020 refactor reintroduced async-signal-unsafe calls inside sshd’s SIGALRM handler, which fires when a client fails to authenticate within LoginGraceTime (default 120s). The handler calls sigdie(), which lands in glibc’s syslog() path, which is not safe to invoke from a signal handler — it can hold malloc arena locks, fiddle with the FILE buffer for /dev/log, and call localtime_r. By precisely timing the connection to be interrupted while the main sshd thread is mid-malloc, an attacker corrupts the heap of a privileged child process and walks it to RCE as root.

Preconditions / where it applies

  • OpenSSH 8.5p1 through 9.7p1 with the 2020 regression (commit 752250c) on glibc-based Linux
  • LoginGraceTime non-zero (the default) so the alarm fires
  • Network reachability to TCP/22 (or whatever port sshd binds)
  • Originally demonstrated against 32-bit glibc; the 64-bit case is exploitable but takes much longer (Qualys: ~6–8h on 32-bit, weeks on 64-bit)
  • Not affected: OpenBSD (different sig handling), musl-libc systems, OpenSSH ≤ 4.4p1 or ≥ 9.8p1

Technique

The attacker opens many parallel SSH connections and lets LoginGraceTime expire on each, hoping one fires SIGALRM mid-free():

1
2
3
4
5
6
7
8
9
10
11
12
13
# Conceptual — real exploit pre-computes glibc offsets and sprays FILE structures
import socket, threading, time

def race(host, delay):
    s = socket.create_connection((host, 22))
    s.sendall(b"SSH-2.0-regreSSHion\r\n")
    # Send a partial KEX init sized to nudge sshd into malloc(), then stall
    s.sendall(KEX_PRIMER + b"A" * SPRAY_LEN)
    time.sleep(delay)           # tuned so alarm fires inside malloc()
    s.close()

for _ in range(10000):
    threading.Thread(target=race, args=(target, GRACE_TIME - 0.0007)).start()

When the race wins, syslog() reuses a half-freed _IO_FILE structure whose vtable the attacker has primed via the KEX spray. vtable->_xsputn redirects to a small ROP chain into glibc’s gadget set, ending in execve("/bin/sh", ...) as UID 0. Qualys’s lab numbers: ~10k connections per win on 32-bit Debian 12.

Detection and defence

  • Upgrade OpenSSH to 9.8p1 (released 2024-07-01) or apply distro patches (USN-6859, DSA-5724, RHSA-2024:4312)
  • Stopgap: LoginGraceTime 0 in sshd_config disables the alarm and blocks the race at the cost of letting slow clients sit forever — pair with connection limits
  • MaxStartups, PerSourceMaxStartups, fail2ban-style throttles raise the time-to-exploit dramatically
  • Detect: spikes of half-open SSH sessions from a single source, sshd children segfaulting under load (coredumpctl list sshd), or auth logs full of Timeout before authentication
  • Network layer: rate-limit TCP/22 SYNs per source; expose SSH only via VPN or bastion
  • glibc heap mitigations (tcache double-free check, safe-linking) raise difficulty but do not prevent exploitation

References

See also: linux-enumeration, kernel-exploits-linux, linux-privesc-vectors.