- 漏洞简介
2022 年 2 月,微软修补了 CVE-2022-21999 漏洞。Windows Print Spooler 存在权限提升漏洞,经过身份认证的本地攻击者可通过在目标系统上运行特制程序来利用此漏洞,成功利用此漏洞的攻击者可在目标系统上以 SYSTEM 权限执行任意代码。漏洞公开当天,国外安全研究员在网上公开了此漏洞的细节及 PoC,此漏洞为 CVE-2020-1030 漏洞补丁的绕过。经验证,此 PoC 可在 Windows 主机上稳定利用。
通过阅读 CVE-2020-1030 技术文章,我们可以得知:
a. 可通过配置 copyfile 下的 module 值来指定 Point and Print dll,使 Spooler 自动加载这个库,但这个库的路径存在一定限制
b. Spooler 会尝试从系统目录、Spooler 驱动程序目录、环境和驱动程序版本等目录去加载 Point and Print DLL,加载 Point and Print DLL 时,Spooler 会搜索以下路径:
1
2
|
i. * * % SYSTEMROOT % \\\\System32 * * ii. * * % SYSTEMROOT % \\\\System32\\\\spool\\\\drivers\\\\<ENVIRONMENT>\\\\<DRIVERVERSION> * * |
c. 通过在打印机上配置SpoolDirectory
属性可创建任意且可写的目录,利用这点来创建任意用户可写的 ii. 目录
d. 利用 a. 先后加载 AppVTerminator.dll、目标.dll。其中,加载 AppVTerminator.dll 是为了在配置SpoolDirectory
后可以终止 Spooler,等待 Spooler 重启并创建相应目录后将目标 DLL 写入该目录,然后 a. 利用加载该 DLL
- 漏洞分析
建议先看一下这两篇相关研究文章:
https://www.accenture.com/us-en/blogs/cyber-defense/discovering-exploiting-shutting-down-dangerous-windows-print-spooler-vulnerability
https://research.ifcr.dk/spoolfool-windows-print-spooler-privilege-escalation-cve-2022-22718-bf7752b68d81
由于 CVE-2020-1030 漏洞的 PoC 已公开,我们以 CVE-2020-1030 漏洞为基础开始对新的漏洞进行分析。微软在修补 CVE-2020-1030 漏洞时加入了一些安全验证,使用 CVE-2020-1030 的 PoC 已经无法将 SpoolDirectory 直接设置为 C:\\Windows\\System32\\spool\\drivers\\x64\\4 了,SplSetPrinterDataEx 函数中已添加了通过 IsValidSpoolDirectory 函数校验即将要设置的 SpoolDirectory 的路径的逻辑。
IsValidSpoolDirectory 函数通过调用 AdjustFileName 函数将路径转换为规范路径,然后检查当前用户是否可以以GENERIC_WRITE
访问权限打开或创建目标目录,最终检查目录的链接数是否不大于 1,如果校验通过则返回 True。由于我们对 C:\\Windows\\System32\\spool\\drivers\\x64\\4 没有可写权限所以校验失败。
根据公开的技术文章,下面我们将使用重解析点绕过 IsValidSpoolDirectory 函数的校验成功设置 SpoolDirectory。我们先将 SpoolDirectory 设置为当前用户可操作的路径,如 C:\\Users\\test\\AppData\\Local\\Temp\\testtest\\4,这样就通过 IsValidSpoolDirectory 函数的校验成功设置 SpoolDirectory。然后再创建重解析点,将 C:\\Users\\test\\AppData\\Local\\Temp\\testtest\\ 链接到 C:\\Windows\\System32\\spool\\drivers\\x64,等待 Spooler 创建 Everyone 具有可写权限的目录 C:\\Windows\\System32\\spool\\drivers\\x64\\4。
Print Spooler 需要重启才能初始化 SpoolDirectory 指定的目录,也就是 C:\\Users\\test\\AppData\\Local\\Temp\\testtest\\4 -> C:\\Windows\\System32\\spool\\drivers\\x64\\4。通过配置 Point and Print dll 为 AppVTerminator.dll 来停止 Print Spooler ,等待它重启调用 BuildPrinterInfo 函数(另外,调用 EnumPrinters 函数可加速调用 BuildPrinterInfo)来设置目标文件夹。但在这里又出现一次校验,在 BuildPrinterInfo 函数调用 CreateEverybodySecurityDescriptor、GetSecurityDescriptorDacl、SetSecurityInfo 等函数为目标路径设置 Everyone 可以访问的安全描述符之前,会调用 IsPathALink 和 IsModuleFilePathAllowed 函数进行校验。
如下图所示,IsModuleFilePathAllowed 函数会判断要目标路径是否在 SystemDirectory 或 DriverDirectory 路径下,如果满足条件就返回 True。其中,SystemDirectory 为 C:\\Windows\\system32,DriverDirectory 为 C:\\Windows\\system32\\spool\\DRIVERS\\x64。Print Spooler 在设置目录前需要保证要目标不在系统目录或驱动目录中,满足条件才会去设置目录。由于我们想要创建的目录是 C:\\Windows\\System32\\spool\\drivers\\x64\\4,并不满足条件,也就不能通过 IsModuleFilePathAllowed 函数处的校验。
为了绕过此处的限制,我们将 SpoolDirectory 设置为 UNC 路径,即 \\\\localhost\\C\\$\\Users\\test\\AppData\\Local\\Temp\\testtest\\4,而不是 C:\\Windows\\System32\\spool\\drivers\\x64\\4。这样在IsModuleFilePathAllowed 函数中校验的路径就是 UNC\\localhost\\C\\$\\Windows\\System32\\spool\\drivers\\x64\\printers\\x64\\4,而不是 C:\\Windows\\System32\\spool\\drivers\\x64\\4,就不能通过 _wcsnicmp 函数和 SystemDirectory 或 DriverDirectory 匹配上,从而绕过这个判断。
当我们有权限向 C:\\Windows\\System32\\spool\\drivers\\x64\\4 写入之后,将想要加载的 DLL 复制到该目录,然后通过 SetPrinterDataEx 函数配置 Point and Print dll 为 目标 DLL。Print Spooler 服务会调用 localspl!SplSetPrinterDataEx 函数进行处理,最终会在 SplLoadLibraryTheCopyFileModule 函数中调用 LoadLibraryW 函数来加载相应模块,在调用 LoadLibraryW 函数之前会通过 IsModuleFilePathAllowed 函数来判断目标 DLL 是否在 C:\\Windows\\system32、C:\\Windows\\system32\\spool\\DRIVERS\\x64 以及 C:\\Windows\\system32\\spool\\DRIVERS\\x64\\4 目录下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
/ / SplSetPrinterDataEx if ( !v11 && !wcsncmp(pKeyName, L \"CopyFiles\\\\\" , 0xAui64 ) && !( * (_DWORD * )( * ((_QWORD * )hPrinter + 0x15 ) + 168i64 ) & 0x4000000 ) ) { SplCopyFileEvent(hPrinter, pKeyName); } / / SplCopyFileEvent if ( !(unsigned int )SplGetPrinterDataEx( hPrinter, v7, L \"Module\" , (unsigned int * )&v20, (unsigned __int8 * )pszModule, v19, &v19) && v20 = = 1 ) { v3 = CreateFullyQualifiedNameFromPSpool(hPrinter, &v18); if ( v3 ) { v10 = SplLoadLibraryTheCopyFileModule((__int64)hPrinter, pszModule); / / SplLoadLibraryTheCopyFileModule if ( (unsigned int )GetDriverDirectory(&DriverDirectory, MaxLength, pIniEnvironment, 0i64 , pIniSpooler) ) { if ( !(unsigned int )MakeCanonicalPath(pszModule, &LibFileName) || !(unsigned int )IsModuleFilePathAllowed(&LibFileName, &DriverDirectory) || (hModule = LoadLibraryW(&LibFileName)) = = 0i64 ) { if ( (unsigned int )GetIniDriverAndDirForThisMachineEx( * (_QWORD * )(hPrinter + 0x40 ), MaxLength, &IniDriverAndDir, (struct _INIDRIVER * * )&hPrinter, pIniEnvironment) ) { v12 = StringCchCopyW(&IniDriverModulePath, 0x104i64 , &IniDriverAndDir); / / / / 0 : 004 > du beddc0 / / IniDriverAndDir / / 00000000 ` 00beddc0 \"C:\\Windows\\system32\\spool\\DRIVER\" / / 00000000 ` 00bede00 \"S\\x64\\4\\\" v6 = StatusFromHResult(v12); if ( !v6 ) { v13 = StringCchCatW(&IniDriverModulePath, 0x104i64 , pszModule); v6 = StatusFromHResult(v13); if ( !v6 ) { if ( (unsigned int )MakeCanonicalPath(&IniDriverModulePath, &LibFileName) ) { if ( (unsigned int )IsModuleFilePathAllowed(&LibFileName, &IniDriverAndDir) ) { hModule = LoadLibraryExW(&LibFileName, 0i64 , v6 + 8 ); if ( !hModule ) v6 = GetLastError(); } } } |
IsModuleFilePathAllowed 函数中判断目标是否在 C:\\Windows\\System32 目录或 DriverDirectory/IniDriverAndDir 目录或子目录下,所以我们可以利用漏洞在 C:\\Windows\\system32\\spool\\DRIVERS\\x64 目录下创建可写任意文件夹(如 C:\\Windows\\system32\\spool\\DRIVERS\\x64\\5 ),然后将目标 DLL 复制过去并利用 Point and Print 来加载,如下图所示:
- 补丁分析
以下为补丁前后版本的修改情况,BuildPrinterInfo 函数发生了变化。
补丁前,在 BuildPrinterInfo 函数中会先尝试创建目标目录,同时共享 FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE 权限(即 FILE_SHARE_VALID_FLAGS),成功执行后获得句柄(hObject)。如果创建失败的话会尝试以 FILE_SHARE_VALID_FLAGS 权限打开该目录,如果当前用户没有相应权限也会打开失败。在通过IsPathALink 函数及 IsModuleFilePathAllowed 函数校验之后,会调用 SetSecurityInfo(hObject, SE_FILE_OBJECT, 4u, 0i64, 0i64, pDacl, 0i64) 函数来设置该目录。
补丁后的 BuildPrinterInfo 函数只是尝试以 FILE_SHARE_READ 权限打开目标目录,并且删掉了之前使用 CreateEverybodySecurityDescriptor、GetSecurityDescriptorDacl、SetSecurityInfo 等函数为目标路径设置 Everyone 读写访问、执行等所有可能的访问权限,也就是不会再去创建新的 SpoolDirectory 了,也不会修改原本目录的权限。
暂无评论内容