CVE-2020-0787-Windows本地提权漏洞分析
Author:剑残雪飘@深蓝攻防实验室
itm4n在Background Intelligent Transfer Service中发现了任意文件移动漏洞,这里学习一下其中的思路。
BITS
参考链接
程序员和系统管理员使用后台智能传输服务(BITS)从HTTP Web服务器和SMB文件共享下载文件或将文件上传到HTTP Web服务器和SMB文件共享。BITS将考虑传输成本以及网络使用情况,以便用户的前台工作影响尽可能小。即使重新启动后,BITS也可以处理网络中断,暂停并自动恢复传输。
这个服务公开了几个COM对象,这些对象是“控件类”的不同迭代,并且还有一个“旧版控件类”。“旧版控件类”可以获取指向IBackgroundCopyGroup
的指针,有两个未记录的方法QueryNewJobInterface()
和SetNotificationPointer()
。
如果用户调用IBackgroundCopyGroup
接口的CreateJob()
方法,就可以获得一个指向IBackgroundCopyJob1
接口的指针,如果调用QueryNewJobInterface()
方法,就可以获得一个指向新IBackgroundCopyJob
接口的指针。
此调用是由服务处理的without impersonation
,意味着用户可以在 NT AUTHORITY\SYSTEM
的context中获得一个指向IBackgroundCopyJob
接口的指针
创建并将文件添加到队列后,将创建一个临时文件,服务完成文件写入后,通过MoveFileEx()
重命名,漏洞点为用QueryNewJobInterface()
返回的指针是without impersonation
,可以获取NT AUTHORITY\SYSTEM
权限。
BITS COM类如何工作?
后台智能传输服务公开了几个COM对象,可以使用OleViewDotNet列出这些对象
重点关注后台智能传输(BIT)控制类1.0和旧版BIT控制类及其主要接口,分别为IBackgroundCopyManager
和IBackgroundCopyMgr
。
“new” BIT Control Class
BIT 控制类1.0的工作方式如下:
- 创建BIT控制类(CLSID:
4991D34B-80A1-4291-83B6-3328366B9097
)的实例,需要一个指向IBackgroundCopyQMgr
的指针,接口为CoCreateInstance()
。 - 然后,创建一个“作业”,并调用
IBackgroundCopyManager::CreateJob()
获取指向该IBackgroundCopyJob
接口的指针。 - 然后,调用将文件添加到作业的
IBackgroundCopyJob::AddFile()
。这需要两个参数:URL和本地文件路径。该URL也可以是UNC路径。 - 最后,由于作业中创建一个
SUSPENDED
状态,调用IBackgroundCopyJob::Resume()
,并在工作的状态TRANSFERRED
时调用IBackgroundCopyJob::Complete()
。
CoCreateInstance(CLSID_4991D34B-80A1-4291-83B6-3328366B9097) -> IBackgroundCopyManager*
|__ IBackgroundCopyManager::CreateJob() -> IBackgroundCopyJob*
|__ IBackgroundCopyJob::AddFile(URL LOCAL_FILE)
|__ IBackgroundCopyJob::Resume()
|__ IBackgroundCopyJob::Complete()
尽管BIT服务的运行方式为NT AUTHORITY\SYSTEM
,但所有这些操作都是在模拟RPC客户端执行的,因此此处无法提权。
Legacy Control Class
在传统控制类的工作方式有些不同。在过程开始时需要一个额外的步骤。
- 创建传统BIT控制类(CLSID:
69AD4AEE-51BE-439B-A92C-86AE490E8B30
)的实例,需要一个指向IBackgroundCopyQMgr
的指针,接口为CoCreateInstance()
。 - 然后,创建调用
IBackgroundCopyQMgr::CreateGroup()
的组,获取指向该IBackgroundCopyGroup
接口的指针 - 然后,创建“作业”,并调用
IBackgroundCopyGroup::CreateJob()
以获取指向该IBackgroundCopyJob1
接口的指针。 - 然后,将文件添加到“作业”中,该调用
IBackgroundCopyJob1::AddFiles()
将FILESETINFO
结构作为参数。 - 最后,由于作业中创建一个
SUSPENDED
状态,调用IBackgroundCopyJob1::Resume()
,并在工作的状态TRANSFERRED
时调用IBackgroundCopyJob1::Complete()
。
CoCreateInstance(CLSID_69AD4AEE-51BE-439B-A92C-86AE490E8B30) -> IBackgroundCopyQMgr*
|__ IBackgroundCopyQMgr::CreateGroup() -> IBackgroundCopyGroup*
|__ IBackgroundCopyGroup::CreateJob() -> IBackgroundCopyJob1*
|__ IBackgroundCopyJob1::AddFiles(FILESETINFO)
|__ IBackgroundCopyJob1::Resume()
|__ IBackgroundCopyJob1::Complete()
跟上面一样,尽管BIT服务运行为NT AUTHORITY\SYSTEM
,但所有这些操作都是在模拟RPC客户端执行的,因此在这里也不能提权。
这两个COM类及其接口的用法在MSDN有详细记录。但是,在尝试了解IBackgroundCopyGroup
接口如何工作时,注意到MSDN上列出的方法与其实际的Proxy定义有一些差异
IBackgroundCopyGroup
接口的文档可在此处获得。文档有13种方法。
但是,当使用OleViewDotNet查看此接口的代理定义时,我们可以看到它实际上有15个方法。
Proc3
以Proc15
匹配文档中的方法,但Proc16
和Proc17
不存在。
通过文档,我们知道相应的头文件是Qmgr.h
。
看到两个未记录的方法:QueryNewJobInterface()
和SetNotificationPointer()
。
未公开的方法:“ QueryNewJobInterface()”
参数结构
virtual HRESULT STDMETHODCALLTYPE QueryNewJobInterface(
/* [in] */ __RPC__in REFIID iid
/* [iid_is][out] */ __RPC__deref_out_opt IUnknown **pUnk) = 0;
打开qmgr.dll
首先,将输入GUID
(接口ID)与硬编码值37668d37-507e-4160-9316-26306d150b12
进行比较:如果不匹配,则该函数返回错误代码0x80004001
。否则,它将从调用函数CJob::GetJobExternal()
。
硬编码的GUID
值(37668d37-507e-4160-9316-26306d150b12
)可以在Bits.h
头文件中找到为IID_IBackgroundCopyJob
任意文件移动漏洞
在进一步进行分析之前,可以根据收集到的少量信息做出猜测。
- 未记录方法的名称
QueryNewJobInterface()
。 - 在传统 BIT控制类的
IBackgroundCopyGroup
接口中为exposed 。 - 在
GUID
“新”的IBackgroundCopyJob
接口为involved。
因此,可以假设此函数的目的是从Legacy Control Class获取指向“ new” IBackgroundCopyJob
接口的指针。
- 创建指向Legacy Control Class的实例,获取一个指向
IBackgroundCopyQMgr
接口的指针 - 创建一个新组调用
IBackgroundCopyQMgr::CreateGroup()
,获取一个指向IBackgroundCopyGroup
接口的指针 - 创建一个job,调用
IBackgroundCopyGroup::CreateJob()
,获取一个指向IBackgroundCopyJob1
接口的指针 - 通过
IBackgroundCopyJob1::AddFiles()
添加文件 - 调用
IBackgroundCopyGroup::QueryNewJobInterface()
方法并获得指向未知接口的指针,假定它是一个IBackgroundCopyJob
接口 - 通过调用
IBackgroundCopyJob
接口的Resume()
和Complete()
来恢复和完成job而不是IBackgroundCopyJob1
接口
目标URL为\\127.0.0.1\C$\Windows\System32\drivers\etc\hosts
。
使用Procmon分析了BIT服务
首先,可以看到该服务**在目标目录中创建了一个TMP文件。
调用该Resume()
函数,该服务将开始读取目标文件\\127.0.0.1\C$\Windows\System32\drivers\etc\hosts
并将其内容写入TMP文件,此时依然是 impersonating the current user
最后,将TMP文件重命名为test.txt
,并且调用MoveFileEx()
,不再是当前用户,这意味着文件移动是在NT AUTHORITY\SYSTEM
的context中完成的
SetRenameInformationFile
调用了win32的MoveFileEx()
函数。
漏洞原理
CreateJob()
方法在IBackgroundCopyGroup
的COldGroupInterface
类中实现
)
由于CFG的存在,看不出什么信息,这个方法调用CreateJobInternal()
方法,CreateJobInternal()
调用CLockedJobWritePointer::ValidateAccess
CLockedJobWritePointer::ValidateAccess
调用了CJob::CheckClientAccess
CheckClientAccess()
检查用户token并应用于当前线程的impersonation
这个执行流返回CreateJobInternal
方法,并调用CJob::GetOldJobExternal
返回IBackgroundCopyJob1
类型的指针
调用流程
(CLIENT) IBackgroundCopyGroup::CreateJob()
|
V
(SERVER) COldGroupInterface::CreateJob()
|__ COldGroupInterface::CreateJobInternal()
|__ CLockedJobWritePointer::ValidateAccess()
| |__ CJob::CheckClientAccess() // Client impersonation
|__ CJob::GetOldJobExternal() // IBackgroundCopyJob1* returned
了解了CreateJob()
的实现原理,回到QueryNewJobInterface()
方法,如果提供的GUID
匹配IID_IBackgroundCopyJob
调用CJob::GetJobExternal
查询新的接口指针调用并返回给客户端。
流程
(CLIENT) IBackgroundCopyGroup::QueryNewJobInterface()
|
V
(SERVER) COldGroupInterface::QueryNewJobInterface()
|__ CJob::GetJobExternal() // IBackgroundCopyJob* returned
当我们调用COldGroupInterface::QueryNewJobInterface()
获取新的指针时,客户端并没有impersonated,这样就可以获取一个NT AUTHORITY\SYSTEM
上下文的指针
实际上文件移动操作发生在调用IBackgroundCopyJob::Resume()
之后以及IBackgroundCopyJob::Complete()
之前
调用IBackgroundCopyJob::Resume()
流程
(CLIENT) IBackgroundCopyJob::Resume()
|
V
(SERVER) CJobExternal::Resume()
|__ CJobExternal::ResumeInternal()
|__ ...
|__ CJob::CheckClientAccess() // Client impersonation
|__ CJob::Resume()
|__ ...
调用IBackgroundCopyJob::Complete()
流程
(CLIENT) IBackgroundCopyJob::Complete()
|
V
(SERVER) CJobExternal::Complete()
|__ CJobExternal::CompleteInternal()
|__ ...
|__ CJob::CheckClientAccess() // Client impersonation
|__ CJob::Complete()
|__ ...
这两种情况都不能成功
当调用IBackgroundCopyGroup::QueryNewJobInterface()
获取一个IBackgroundCopyJob
类型的指针时,这个工作是由服务端完成的,而不是RPC客户端。此时可以利用成功。
MoveFileEx()
调用流程
漏洞利用
本地创建一个job用于下载,并在tmp文件上设置Oplock,恢复执行后该服务会写入TMP文件触发Oplock,然后切换挂载点到对象目录,创建符号链接,tmp文件指向我们的文件,本地文件指向system32文件夹中的dll,最后释放Oplock,写入成功
1)准备
创建一个如下的目录
<DIR> %temp%\workspace
|__ <DIR> bait
|__ <DIR> mountpoint
|__ FakeDll.dll
mountpoint
目录是从连接bait
目录,切换到RPC Control
对象,FakeDll.dll为了移动到受限位置,比如system32
2)创建挂载点
创建一个从%temp%\workspace\mountpoint
到%temp%\workspace\mountpoint
的挂载点
3)创建新的job
用Legacy Control Class接口创建一个新的job,参数如下
Target URL: \\127.0.0.1\C$\Windows\System32\drivers\etc\hosts
Local file: %temp%\workspace\mountpoint\test.txt
因为之前创建了链接,所以实际路径为%temp%\workspace\bait\test.txt
4)找到tmp文件设置Oplock
列出bait
文件夹的内容找到类似BITAA6.tmp
的临时文件,并在文件上设置设置Oplock
5)Resume并等待Oplock
恢复job时,会打开tmp文件写入触发Oplock
6)切换挂载点
切换前
TMP file = %temp%\workspace\mountpoint\BIT1337.tmp -> %temp%\workspace\bait\BITAA6.tmp
Local file = %temp%\workspace\mountpoint\test.txt -> %temp%\workspace\bait\test.txt
切换挂载点创建符号链接
%temp%\workspace\mountpoint -> \RPC Control
Sym #1: \RPC Control\BITAA6.tmp -> %temp%\workspace\FakeDll.dll
Sym #2: \RPC Control\test.txt -> C:\Windows\System32\FakeDll.dll
完成此步骤后:
TMP file = %temp%\workspace\mountpoint\BITAA6.tmp -> %temp%\workspace\FakeDll.dll
Local file = %temp%\workspace\mountpoint\test.txt -> C:\Windows\System32\FakeDll.dll
7) 释放Oplock
释放Oplock后,CreateFile
将对原始TMP文件操作,并且该服务将开始写入%temp%\workspace\bait\BITAA6.tmp
。之后,MoveFileEx()
由于符号链接,最终将被重定向。DLL将被移至该System32
文件夹
8) 提权
使用Update Session Orchestrator service
加载移动到system32文件夹
的dll文件WindowsCoreDeviceInfo.dll
提权
参考链接
- https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-0787
- https://github.com/itm4n/BitsArbitraryFileMove
- https://itm4n.github.io/cve-2020-0787-windows-bits-eop/
- https://docs.microsoft.com/en-us/windows/win32/api/bits/nn-bits-ibackgroundcopymanager
- https://docs.microsoft.com/en-us/windows/win32/api/qmgr/nn-qmgr-ibackgroundcopyqmgr