[하루한줄] CVE-2026-20841: Windows Notepad의 마크다운 렌더링 엔진 내 URI 스킴 검증 미흡으로 인한 RCE 취약점
URL
Target
- Windows 11 (마크다운 지원 메모장 앱 버전: 11.2501.x 이상)
- 2026년 2월 패치 미적용 시스템
Explain
Microsoft는 25년도에 사용자의 편의를 위해 윈도우 11의 메모장에 마크다운 미리보기 및 렌더링 기능을 도입했습니다. 이 기능은 내부적으로 마크다운 텍스트를 파싱해 리치 텍스트 또는 HTML-like 구조로 변환한 뒤 화면에 뿌려줍니다.
문제는 마크다운 내의 하이퍼링크 (Ex. [text][url])을 처리할 때 발생합니다. 메모장은 사용자가 링크를 클릭하거나 특정 조건에서 해당 링크의 안전성 검사를 해야 하는데, 이때 허용된 프로토콜 리스트를 검증하는 로직에서 취약점이 발견되었습니다. 공격자는 이를 악용해 사용자가 메모장 파일을 열고 링크를 클릭하는 것 만으로 임의 코드를 실행을 할 수 있습니다. 하지만 기본적으로 .md 확장자가 메모장으로 열리도록 되어 있지 않습니다. 따라서, 공격자는 사용자가 수동으로 마크다운 파일을 메모장으로 열도록 유도해야 합니다.
Root Cause
취약점의 근본 원인은 링크 클릭 시 호출되는 sub_140170F60 함수의 부족한 화이트리스트 검증에 있습니다. 코드는 링크가 단순한 슬래시, 백슬래시로 구성되어 있는지만 확인하고, 중요한 프로토콜 Scheme에 대한 검증은 누락되었습니다.
// sub_140170F60: 마크다운 링크 클릭 핸들러
void __fastcall sub_140170F60(link_handle *Block) {
switch ( LOWORD(Block->field_48) ) {
[생략]
case 4:
[1]
_original_string = GetLinkString(Block);
string_length = -1i64;
do ++string_length; while ( _original_string[string_length] );
strncpy((char **)&Block->link_dst, _original_string, string_length);
string_base = (int *)&Block->link_dst;
if ( Block->length > 7ui64 ) string_base = (int *)Block->link_dst;
end_of_string = (char *)string_base + 2 * Block->max_length;
[1]에서 sub_140170F60 함수는 마크다운 블록에서 공격자가 제어하는 링크 문자열 _original_string를 가져오는 걸로 시작합니다. 이후 strncpy를 통해 내부 구조체인 Block→link_dst에 복사하고 분석을 위한 시작점 string_base와 끝점 end_of_string 포인터를 설정합니다. 이 단계에서 공격자가 입력한 file:// 또는 ms-appinstaller://와 같은 악성 URI가 메모리에 로드됩니다.
[2]
string_ptr = sub_14002C820(string_base, end_of_string);
if ( string_ptr == (int *)end_of_string ) goto continue_to_processing;
do {
if ( *(_WORD *)string_ptr != '\\' && *(_WORD *)string_ptr != '/' )
break;
string_ptr = (int *)((char *)string_ptr + 2);
} while ( string_ptr != (int *)end_of_string );
if ( string_ptr == (int *)end_of_string ) goto continue_to_processing;
break;
default:
__debugbreak();
return;
}
[2]에서는 do-while 루프를 통해 링크 문자열 내에 슬래시와 백슬래시가 아닌 문자가 포함되어 있는지 확인합니다. 만약 문자가 단 한개라도 있다면 루프를 break합니다. 예를 들어 링크가 file://로 시작한다면 첫 문자인 f에서 바로 루프가 break 됩니다.
[3]
continue_to_processing:
strncpy((char **)&Block->field_80, string_base, (end_of_string - (char *)string_base) >> 1);
Block->shell_start_struct.cbSize = 112;
Block->shell_start_struct.fMask = 1088;
Block->shell_start_struct.lpVerb = L"open";
v16 = (const WCHAR *)&Block->field_28;
if ( Block->field_40 > 7ui64 ) v16 = *(const WCHAR **)v16;
Block->shell_start_struct.lpFile = v16;
[4]
if ( ShellExecuteExW(&Block->shell_start_struct) ) {
...
}
break;
[생략]
}
[2]의 검증을 통과하면 [3]에서 ShellExecuteExW 함수를 호출하기 위해 SHELLEXECUTEINFOW 구조체를 초기화 합니다.
- fMask = 1088:
SEE_MASK_FLAG_DDEWAIT(0x100)와SEE_MASK_CLASSNAME(0x800)이 결합된 값으로, 윈도우가 URI 스킴에 따라 적절한 프로토콜 핸들러를 찾아서 실행하도록 보장합니다. - lpVerb = L”open”: 윈도우 셸에 대상 파일을 “열기” 명령으로 실행할 것을 지시합니다.
- lpFile = v16: 검증되지 않은 공격자의 URI가 그대로 전달됩니다.
마지막으로 [4]에서 ShellExecuteExW()가 호출되면서 취약점이 트리거됩니다. 메모장은 기본적으로 현재 로그인한 사용자의 보안 컨텍스트 내에서 실행되기에, 공격자는 file://스킴을 사용해 원격 SMB 서버에 호스팅된 악성 파일을 즉시 실행하거나, ms-appinstaller://스킴을 통해 사용자 개입을 최소화하면서 악성 앱 패키지의 설치 및 실행을 유도할 수 있습니다.
Reference
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.