FlawOpen

flawopen.com/Incidents/windows-update-stack-symlink-eop-september-2026

● Microsoft · CVE-2026-81963 · September 2026 · CVSS 7.8 · 높음
보안 연구 · FlawOpen

CVE-2026-81963: Windows Update 링크 권한 상승

CVE-2026-81963은 악용된 Windows Update 링크 확인 취약점입니다. 영향 버전, Microsoft 수정 사항, 안전한 서비스 파일 처리를 확인하세요.[1], [2]

💡 알기 쉬운 설명 (ELI5)

마스터키를 가진 야간 관리인이 벽의 화살표를 따라 「복도 끝 창고」로 소포를 배달하면서 자기가 어느 방에 들어왔는지는 확인하지 않습니다. 입주자들은 그 화살표를 옮길 수 있습니다. 한 입주자가 창고를 가리키는 화살표를 관리자 사무실 쪽으로 돌려놓자, 어느 문이든 여는 마스터키를 가진 관리인은 안으로 들어가 관리자 책상에 소포를 놓습니다. 그 문은 입주자 스스로는 결코 열 수 없고, 관리인의 키가 대신 열어 준 것입니다. 해결책은 관리인이 실제로 연 문의 번호를 확인하고, 입주자가 옮길 수 있는 화살표는 따르지 않는 것입니다.[5], [9]

핵심 개념 및 용어

Windows 업데이트 스택
업데이트를 검색·다운로드·스테이징·설치하는 Windows 구성 요소. 업데이트 설치에는 운영 체제의 보호된 영역에 대한 쓰기가 필요하므로 SYSTEM으로 실행됩니다.[1]
SYSTEM(NT AUTHORITY\SYSTEM)
Windows 서비스가 실행되는 로컬 계정. 로컬 컴퓨터에서 관리자보다 강한 권한을 가져, 이 계정의 파일 작업은 거의 거부되지 않습니다.[1]
정션 / 심볼릭 링크(재분석 지점)
한 경로를 다른 위치로 확인되게 하는 NTFS 개체. 표준 사용자는 자신이 통제하는 폴더에 정션을 만들어 그 이름을 자신이 쓸 수 없는 디렉터리로 돌릴 수 있습니다.[9]
링크 추적(CWE-59)
프로그램이 이름으로 파일을 열거나 쓰거나 옮기거나 삭제하면서 그 이름이 예상한 위치로 확인되는지 검사하지 않아, 남이 심어 둔 링크가 작업 대상을 바꿔 버리는 것.[5], [2]
FILE_FLAG_OPEN_REPARSE_POINT
경로의 마지막 구성 요소가 재분석 지점일 때 마지막 링크를 따라가지 않고 해당 지점 자체를 여는 CreateFileW 플래그입니다. 이 플래그만으로 앞선 경로 구성 요소의 재분석 처리가 차단되지는 않습니다.[6]
TOCTOU(CWE-367)
검사 시점과 사용 시점의 차이. 검사와 그에 의존하는 작업 사이에 파일 시스템이 바뀌는 것. 경로 문자열을 검사한 뒤 이름으로 여는 방식이 바로 그 틈을 남깁니다.[12]
CWE-284 (부적절한 접근 제어)
Microsoft는 이 CVE에 CWE-59와 CWE-284를 모두 지정했습니다. CWE-59는 구체적인 링크 확인 취약점을 나타냅니다. CWE-284는 포괄적인 Pillar CWE이며, MITRE는 실제 취약점 매핑에 사용하는 것을 명시적으로 권장하지 않습니다. 이는 같은 보고 문제에 대한 Microsoft의 더 넓은 분류로 보아야 하며, 두 번째 버그의 증거는 아닙니다.[2], [11], [5]

프레임워크 매핑

사고 타임라인

2026-08-27

Microsoft가 CVE-2026-81963을 예약한다.[2]

2026-09-08

9월 Patch Tuesday: Microsoft가 수정을 공개하고 결함을 「Exploitation Detected」이자 비공개로 표시한다.[1]

2026-09-08

CISA가 CVE-2026-81963을 Known Exploited Vulnerabilities 목록에 추가하며, 미국 연방 기관의 수정 기한을 2026년 9월 22일로 정한다.[4]

2026-09-15

Microsoft가 권고의 개정판을 공개한다.[1]

단계별 공격 실행 흐름

단계 1

SYSTEM 서비스가 사용자가 바꿀 수 있는 경로에서 동작한다

업데이트 스택 구성 요소는 SYSTEM으로 실행되며 스테이징·작업 디렉터리에서 파일을 다룹니다. 표준 사용자가 그 경로의 어느 지점에서든 항목을 만들거나 삭제하거나 이름을 바꿀 수 있으면 경로가 어디로 확인될지는 그 사용자가 정합니다. 이번에 어떤 경로가 영향을 받았는지 Microsoft는 밝히지 않았습니다.[1]

단계 2

경로가 바뀐다

정션이나 심볼릭 링크가 디렉터리 이름을 보호된 시스템 폴더 같은 다른 위치로 확인되게 합니다. 자신이 통제하는 폴더에 정션을 만드는 데는 관리자 권한이 필요하지 않습니다.[5], [9]

단계 3

서비스가 링크를 따라간다

Windows는 경로를 열 때 일반적으로 재분석 지점을 처리합니다. FILE_FLAG_OPEN_REPARSE_POINT는 마지막 구성 요소에 적용되며, 앞선 구성 요소는 계속 해석될 수 있습니다. 이후 서비스가 검증되지 않은 경로로 쓰면 접근 검사는 서비스의 SYSTEM 토큰으로 수행됩니다. 디렉터리 핸들을 검증하고 그 핸들을 기준으로 하위 파일을 만드세요.[6], [8]

단계 4

보호된 위치가 바뀐다

서비스가 SYSTEM이나 관리자만 다뤄야 할 위치에서 파일을 만들거나 덮어쓰거나 옮기거나 삭제합니다. Microsoft의 FAQ는 악용 시 공격자가 SYSTEM 권한을 얻는다고 확인합니다.[1], [2]

소스 코드 비교: 취약한 구현 vs 보안 패치

Microsoft는 영향을 받은 함수나 패치를 공개하지 않았습니다. 이 방어 예시는 Microsoft 코드가 아닙니다. 소유자만 확인하는 것으로는 충분하지 않습니다. 객체의 소유자라는 사실만으로 DACL이 일반 사용자의 쓰기를 거부한다고 볼 수 없습니다. 이 패턴은 설치 프로그램이 고정된 staging 경로와 모든 상위 디렉터리를 SYSTEM 및 Administrators만 쓰도록 허용하는 명시적 DACL로 만든다고 가정합니다. 이 함수는 DACL을 읽거나 검증하지 않습니다. 설치 프로그램이 미리 설정한 권한은 배포 전제 조건입니다. 열린 디렉터리 핸들을 검증한 뒤 NtCreateFile과 FILE_CREATE를 사용해 그 핸들 기준으로 단일 하위 파일 이름을 생성하며, 이어 붙인 경로를 다시 열지 않습니다. 지원되는 배포 환경에서 DACL과 파일 시스템 동작을 검증하세요. CVE-2026-81963을 해결하려면 Microsoft 업데이트를 설치하세요.[5], [6], [7], [8], [9], [10]

소스 코드 발췌: Windows 경로 처리 예시이며 Microsoft 소스 코드는 아닙니다

취약한 구현
/* 설명용 예시이며 Microsoft 소스 코드가 아닙니다. 영향을 받은 함수는 공개되지 않았습니다. SYSTEM 권한 서비스가 일반 사용자가 수정할 수
 * 있는 staging 디렉터리에 파일을 씁니다.
 */
BOOL WriteStagedFile(LPCWSTR stagingDir, LPCWSTR name,
                     const BYTE *data, DWORD size)
{
    WCHAR path[MAX_PATH];
    if (FAILED(StringCchPrintfW(path, MAX_PATH, L"%s\\%s", stagingDir, name)))
        return FALSE;

    /* CWE-59: CreateFileW는 경로의 각 구성 요소에서 junction과 심볼릭 링크를 해석하며, 접근 검사는 SYSTEM 토큰으로
     * 수행됩니다. 사용자가 stagingDir를 junction으로 바꾸면 쓰기가 해당 대상으로 향합니다.
     */
    HANDLE h = CreateFileW(path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
                           FILE_ATTRIBUTE_NORMAL, NULL);
    if (h == INVALID_HANDLE_VALUE)
        return FALSE;

    DWORD written = 0;
    BOOL ok = WriteFile(h, data, size, &written, NULL) && written == size;
    CloseHandle(h);
    return ok;
}
보안 강화 패치
/* 방어 패턴 예시이며 Microsoft 소스 코드나 패치가 아닙니다. 설치 프로그램은 이 경로와 모든 상위 디렉터리를 명시적 DACL로 만들고 SYSTEM
 * 및 Administrators만 쓰도록 허용해야 합니다.
 */
#include <windows.h>
#include <winternl.h>

static const WCHAR STAGING_DIR[] =
    L"C:\\ProgramData\\Vendor\\Update\\Staging";

BOOL WriteStagedFile(LPCWSTR name, const BYTE *data, DWORD size)
{
    if (name == NULL || data == NULL || name[0] == L'\0' ||
        wcspbrk(name, L"\\/:") != NULL || wcscmp(name, L".") == 0 ||
        wcscmp(name, L"..") == 0)
        return FALSE;

    /* 디렉터리 객체를 고정합니다. FILE_SHARE_DELETE를 생략하면 이름 변경이 차단됩니다. */
    HANDLE dir = CreateFileW(STAGING_DIR,
                             FILE_LIST_DIRECTORY | FILE_TRAVERSE | FILE_READ_ATTRIBUTES,
                             FILE_SHARE_READ | FILE_SHARE_WRITE,
                             NULL, OPEN_EXISTING,
                             FILE_FLAG_BACKUP_SEMANTICS |
                                 FILE_FLAG_OPEN_REPARSE_POINT,
                             NULL);
    if (dir == INVALID_HANDLE_VALUE)
        return FALSE;

    FILE_ATTRIBUTE_TAG_INFO tag;
    WCHAR actual[MAX_PATH];
    BOOL ok = FALSE;
    DWORD n;
    if (!GetFileInformationByHandleEx(dir, FileAttributeTagInfo,
                                      &tag, sizeof tag) ||
        (tag.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT))
        goto done;

    n = GetFinalPathNameByHandleW(dir, actual, MAX_PATH,
                                  FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
    if (n == 0 || n >= MAX_PATH ||
        _wcsicmp(actual, L"\\\\?\\C:\\ProgramData\\Vendor\\Update\\Staging") != 0)
        goto done;

    /* 경로를 다시 만들지 말고 이 핸들을 기준으로 새 하위 항목을 엽니다. */
    size_t nameChars = wcslen(name);
    if (nameChars == 0 || nameChars > 255)
        goto done;
    USHORT nameBytes = (USHORT)(nameChars * sizeof(WCHAR));
    UNICODE_STRING relativeName = { nameBytes, nameBytes, (PWSTR)name };
    OBJECT_ATTRIBUTES oa = {0};
    oa.Length = sizeof oa;
    oa.RootDirectory = dir;
    oa.ObjectName = &relativeName;
    oa.Attributes = OBJ_CASE_INSENSITIVE;

    IO_STATUS_BLOCK iosb = {0};
    HANDLE file = INVALID_HANDLE_VALUE;
    NTSTATUS status = NtCreateFile(
        &file, FILE_WRITE_DATA | SYNCHRONIZE, &oa, &iosb, NULL,
        FILE_ATTRIBUTE_NORMAL, 0, FILE_CREATE,
        FILE_NON_DIRECTORY_FILE | FILE_OPEN_REPARSE_POINT, NULL, 0);
    if (!NT_SUCCESS(status))
        goto done; /* 항목이 이미 있으면 FILE_CREATE가 실패합니다. */

    DWORD written = 0;
    ok = WriteFile(file, data, size, &written, NULL) && written == size;
    CloseHandle(file);
done:
    CloseHandle(dir);
    return ok;
}

엔지니어링 및 시스템 보안 강화 체크리스트

출처