介绍
这篇文章主要讲述了由NCC Group研究人员所发现的两个通过COM本地服务进行非法提权的漏洞。第一个漏洞CVE-2019-1405是COM服务中的一个逻辑错误,可让本地普通用户以LOCAL SERVICE身份执行任意命令。第二个漏洞CVE-2019-1322是一个简单的服务配置错误,可让本地SERVICE组中的任何用户重新配置以SYSTEM权限运行的服务(此漏洞也被其他研究人员发现)。当以上两个漏洞结合在一起时,就允许本地普通用户以SYSTEM权限执行任意命令。
COM背景
为了帮助大家更好的理解漏洞,我们将从一些基础知识开始讲起,如果你熟悉Windows上COM的基本概念,可以跳过本节。
组件对象模型(COM)是微软于1993年推出的一套软件组件接口标准。一般来说,COM是一种让开发人员以与开发语言无关的方式使用某些软件组件的体系结构。COM的众所周知的用法包括ActiveX和OLE(例如,在Word文档中嵌入Excel电子表格)。此外COM在整个Windows生态系统中被微软官方和众多第三方应用所使用,它是真的无处不在!
COM对象是通过存储在Windows注册表中的称为CLSID的全局惟一标识符(GUID)来标识的(许多COM对象也在注册表中被命名,但是名字只是链接到相应的CLSID)。一个CLSID条目包含COM子系统在创建对象实例时使用的信息。
COM会对外公开一个或多个接口,隐藏对象的实现细节。本质上,一个接口定义了对象所支持的一系列方法,而没有规定实现方式。接口在调用方法的代码和实现方法的代码之间存在清晰的界线。每个COM对象都支持IUnknown接口,这个接口用于通过AddRef、Release和QueryInterface方法向其他受支持的接口提供引用计数和类型转换。接口通常通过Microsoft接口定义语言(MIDL)进行定义,并通过被称为IID的GUID进行标识。同样,这些都存储在Windows注册表中。但是请注意,一个COM对象所支持的特定接口并没有在注册表中列出。
一般来说,COM对象可以分为两类——一类是在调用进程内部创建的,另一类是在进程外部创建的。出于权限提升的目的,以本地服务身份所执行的进程外部COM对象就是我们的攻击点,Windows的默认安装中包括很多这样的对象。
有许多工具可用来检查在Windows实例上注册的COM对象和接口。也许其中最好的是由谷歌Project Zero的James Forshaw编写的OleViewDotNet。在这篇博客文章中,我们不会过多介绍这个工具的特性,如果你有兴趣了解更多关于COM的知识,可以从James编写的相关文章开始。
UPnP Device Host服务(CVE-2019-1405)
UPnP Device Host服务在Windows操作系统(从Windows XP到Windows 10)上默认启用,并以NT AUTHORITY\LOCAL SERVICE权限运行(该服务在众多Windows版本上默认安装,但在某些版本上默认不启动)。本服务中的许多COM对象都以本地服务权限执行,如下图由OleViewDotNet所展示的:
Windows中的任何安全对象基本都能设置COM服务的访问控制信息,而OleViewDotNet能够显示这些信息。下图显示了UPnP Device Host服务的权限设置:
可以看出,DACL阻止网络用户在此服务中启动COM对象,但是所有本地用户都可以这样做,这显然是为了防御非法提权。
这个服务托管的UPnPContainerManager和UPnPContainerManager64COM对象都实现了IUPnPContainerManager接口。虽然这个接口缺少文档说明,但是OleViewDotNet能够找到关于它方法的某些信息。此外,Microsoft还为核心操作系统组件提供调试符,从而产生了以下关于接口的初始定义(作为MIDL的一个片段):
[
object,
uuid(6d8ff8d4-730d-11d4-bf42-00b0d0118b56),
pointer_default(unique)
]
interface IUPnPContainerManager : IUnknown {
HRESULT ReferenceContainer([in] wchar_t* string1);
HRESULT UnReferenceContainer([in] wchar_t* string1);
HRESULT CreateInstance(
[in] wchar_t* string1,
[in] GUID* guid1,
[in] GUID* guid2,
IUnknown** pobject);
HRESULT CreateInstanceWithProgID(
[in] wchar_t* string1,
[in] wchar_t* guid1,
[in] GUID* guid2,
[out] IUnknown** pobject);
HRESULT Shutdown();
}
初步看,IUPnPContainerManager所公开的CreateInstance方法似乎特别有趣。对于有一些COM编程经验的人来说,这个方法的名称和API都应该与用来创建任何COM对象的标准CoCreateInstance方法有关(尽管第一个参数的目的不是很明确)。但不幸的是,使用一个众所周知的CLSID和IID以及将一个虚拟字符串作为第一个参数来调用CreateInstance会产生一个未知错误代码。
此时,我们别无选择,只能打开反汇编软件,查看实现该方法的代码。幸运的是,这个过程很简单(OleViewDotNet标出了实现这个类的模块和接口方法的偏移量),我们发现大部分运作是在upnphost.dll中的CContainerManagerbase::CreateInstance方法中进行的。
传递给CreateInstance的字符串参数(在这段代码中被标记为a2)被复制到v7,然后通过调用HrAssign来初始化Block对象。然后将Block传递给HrLookup,实际工作将由第33行中的虚拟函数调用执行。通过调试这段代码我们可以很快发现,当将一个伪字符串作为第一个参数传递给CreateInstance时,我们无法检查HrLookup的结果。显然,这个参数的内容很重要。
下一步是拆分HrLookup函数,但是这并不是易事,我们不得不慎重考虑。由于在实践中,我们通过IUPnPContainerManager接口与这段代码进行交互,因此我们决定查看其他公开的方法,看是否能得到灵感。特别是对CContainerManagerbase::ReferenceContainer方法的反编译,该方法实现了ReferenceContainer:
这个方法的开头看起来与CreateInstance非常相似,但是在这种情况下,如果调用HrLookup失败,就会转而调用HrInsertTransfer方法,将被创建的Block对象传递给ReferenceContainer。因此,我们假设在调用CreateInstance之前,我们可能需要调用ReferenceContainer,经过测试表明这确实如此。基于以上,IUPnPContainerManager接口的更进一步定义如下所示:
[
object,
uuid(6d8ff8d4-730d-11d4-bf42-00b0d0118b56),
pointer_default(unique)
]
interface IUPnPContainerManager : IUnknown {
HRESULT ReferenceContainer([in] wchar_t* containerName);
HRESULT UnReferenceContainer([in] wchar_t* containerName);
HRESULT CreateInstance(
[in] wchar_t* containerName,
[in] GUID* clsid,
[in] GUID* iid,
IUnknown** pobject);
HRESULT CreateInstanceWithProgID(
[in] wchar_t* containerName,
[in] wchar_t* progID,
[in] GUID* iid,
[out] IUnknown** pobject);
HRESULT Shutdown();
}
调用ReferenceContainer方法代入一个任意字符串,然后后将同一个字符串代入到CreateInstance方法,就将在本地服务进程中创建由clsid参数标识的COM对象的实例,并将此对象中由iid标识的接口的引用返回给pobject对象中的调用。
实际上,这就让低权限的本地用户可以使用任何已注册的进程内COM对象,就好像是用户NT AUTHORITY\LOCAL SERVICE执行进程外本地服务一样。特别是,它还可以创建一个Windows script Host Shell实例,获得该对象的IWshShell接口的引用。最后通过从这个接口调用Run方法,利用UPnP Device Host程序执行任意命令。
在Windows 10上,UPnP Device Host被设置为在没有模拟权限的情况下,以用户NT AUTHORITY\LOCAL SERVICE(其中ServiceSidType设置为SERVICE_SID_TYPE_UNRESTRICTED)执行。以下截图显示了启用了UPnP Device Host服务的svchost实例的进程属性,可以确认SeImpersonatePrivilege并没有被启用:
不幸的是,这拦截了一些众所周知的提权方法。但是,服务用户相比普通用户拥有更多的权限,比如NT AUTHORITY\SERVICE组的成员权限更大(这和第二个漏洞关联很大)。
在Windows XP中,进行这种级别的配置是不可能的,因此提权到NT AUTHORITY\SYSTEM非常简单。
Update Orchestrator(CVE-2019-1322)
在发现了上述漏洞后,我们自然想知道获得的权限能否允许我们配合另一个漏洞来完全控制主机。对可能使用了默认访问控制设置的对象进行了全面扫描,很快便找出了Update Orchestrator服务。
Update Orchestrator服务在Windows 10和Windows Server 2019上以NT AUTHORITY\SYSTEM权限默认启用。而对SysInternals工具accesschk.exe的调用显示了在Windows 10 1803至1903版本的Update Orchestrator服务所实施的访问控制。
UsoSvc
Medium Mandatory Level (Default) [No-Write-Up]
R NT AUTHORITY\Authenticated Users
SERVICE_QUERY_STATUS
SERVICE_QUERY_CONFIG
SERVICE_INTERROGATE
SERVICE_ENUMERATE_DEPENDENTS
SERVICE_START
SERVICE_USER_DEFINED_CONTROL
R BUILTIN\Administrators
SERVICE_QUERY_STATUS
SERVICE_QUERY_CONFIG
SERVICE_INTERROGATE
SERVICE_ENUMERATE_DEPENDENTS
SERVICE_START
SERVICE_STOP
SERVICE_USER_DEFINED_CONTROL
RW NT AUTHORITY\SYSTEM
SERVICE_ALL_ACCESS
RW NT AUTHORITY\SERVICE
SERVICE_ALL_ACCESS
以上输出的最后两行显示,NT AUTHORITY\SERVICE用户组中的用户可以完全访问控制该服务。特别是这个组中的用户能够停止、重新配置和启动服务,这就让用户以NT AUTHORITY\SYSTEM权限执行任意命令。
例如,以下命令(当NT AUTHORITY\SERVICE组中的用户执行时)将向目标机器添加名为_tmpAdmUser的管理用户(密码为H.jqt41Kz!a!),并将该服务恢复到默认状态:
sc stop UsoSvc
sc config UsoSvc binpath= "cmd.exe /c net user /add _tmpAdmUser H.jqt41Kz!a!