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のリンク解決に関する悪用済みの欠陥です。影響バージョン、修正、サービスの安全なファイル処理を解説。[1], [2]

💡 わかりやすい解説 (ELI5)

マスターキーを持つ夜間の管理人が、壁の矢印に従って「廊下の突き当たりの倉庫」へ荷物を届けますが、自分がどの部屋に入ったかは確認しません。入居者はその矢印を動かせます。ある入居者が倉庫を指す矢印を支配人室に向け、どの扉も開くマスターキーを持つ管理人は中へ入り、支配人の机に荷物を置きます。その扉を入居者自身は決して開けられません。管理人のキーが代わりに開けたのです。対策は、管理人が実際に開けた扉の番号を読み取り、入居者が動かせる矢印には従わないことです。[5], [9]

主要な概念と専門用語

Windows Update スタック
更新プログラムのスキャン、ダウンロード、ステージング、インストールを担う Windows のコンポーネント群。更新の適用には OS の保護された領域への書き込みが必要なため、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

マイクロソフトが CVE-2026-81963 を予約する。[2]

2026-09-08

9 月の Patch Tuesday:マイクロソフトが修正を公開し、この欠陥を「Exploitation Detected」かつ非公開扱いと表示する。[1]

2026-09-08

CISA が CVE-2026-81963 を Known Exploited Vulnerabilities カタログに追加。米連邦機関の修正期限は 2026 年 9 月 22 日。[4]

2026-09-15

マイクロソフトが勧告の改訂版を公開する。[1]

ステップ・バイ・ステップの攻撃フロー

ステップ 1

SYSTEM のサービスがユーザーの変更できるパスで動作する

Update スタックのコンポーネントは SYSTEM として動作し、ステージング用や作業用のディレクトリでファイルを扱います。標準ユーザーがそのパス上のどこかでエントリを作成・削除・改名できれば、パスの解決先を決めるのはそのユーザーです。今回どのパスが影響を受けたかをマイクロソフトは明らかにしていません。[1]

ステップ 2

パスがすり替えられる

ジャンクションやシンボリックリンクにより、ディレクトリ名が保護されたシステムフォルダーなど別の場所へ解決されます。自分が管理するフォルダーにジャンクションを作るのに管理者権限は不要です。[5], [9]

ステップ 3

サービスがリンクを追う

Windows はパスを開く際、通常は再解析ポイントを解決します。FILE_FLAG_OPEN_REPARSE_POINT が対象にするのは最後の要素で、それ以前の要素は引き続き解決される場合があります。その後、未検証のパスでサービスが書き込むと、アクセスチェックにはサービスの SYSTEM トークンが使われます。ディレクトリハンドルを検証し、そのハンドルを基点に子ファイルを作成してください。[6], [8]

ステップ 4

保護された場所が変更される

SYSTEM や管理者だけが扱うべき場所で、サービスがファイルを作成・上書き・移動・削除します。マイクロソフトの FAQ は、悪用により攻撃者が SYSTEM 権限を得ると認めています。[1], [2]

ソースコード比較:脆弱 vs 堅牢化

Microsoft は影響を受けた関数やパッチを公開していません。この防御例は Microsoft のコードではありません。所有者だけを確認しても不十分です。オブジェクトの所有者であっても、DACL が一般ユーザーの書き込みを拒否するとは限りません。このパターンは、インストーラーが固定のステージングパスとすべての親ディレクトリを、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 のソースコードやパッチではない。インストーラーはこのパスとすべての親ディレクトリを作成し、SYSTEM と
 * Administrators だけに書き込みを許可する明示的な DACL を設定する必要がある。
 */
#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;
}

エンジニアリング&システム堅牢化チェックリスト

参考資料