任意文件移动漏洞是近几年来Windows漏洞中一类非常特殊的存在,相对于内存破坏漏洞而言,这类漏洞不会导致目标主机蓝屏,在执行时有较高的稳定性和隐蔽性,是在进行Red Team行动时的首选。
这类漏洞中一个比较典型的例子就是CVE-2020-0787,为了对它有一个更加深刻的理解,对其漏洞利用代码进行调试分析。
首先从网站上下载项目文件导入VS2019当中,并双击文件夹当中的sln项目打开。
整个工程分为了五个部分,分别是 BitsArbitraryFileMove
、 BitsArbitraryFileMoveExploit
、 BitsArbitraryFileMovePoc
、CommonUtils
以及 UsoDllLoader
。编译生的Release文件夹中包含有两个可执行文件, BitsArbitraryFileMoveExploit.exe
和 BitsArbitraryFileMovePoc.exe
需要注意的是,原项目直接进行编译可能不会通过,要在 BitsArbitraryFileMove.cpp
文件中 include
包含 CommonUtils.h
的头文件
#include <CommonUtils.h>
否则会产生CreateSymlink定义不存在的错误。
创建目录和挂载
将 "C:\Windows\System32\WindowsCoreDeviceInfo.dll"
作为参数传入了 BitsArbitraryFileMove::Run
函数中
- 首先检查了
WindowsCoreDeviceInfo.dll
是否已经在System32
文件夹当中存在。如果存在的话直接退出 - 调用
BitsArbitraryFileMove.cpp
中实现的PrepareWorkspace()
函数生成一个工作空间- 通过GetTempPath获取当前用户的Temp目录,一般而言这个目录为
C:UsersxxxAppDataLocalTemp
- 通过CreateDirectory在Temp目录下创建名为workspace的目录
- 通过CreateDirectory在workspace目录下创建名为mountpoint的目录
- 通过CreateDirectory在workspace目录下创建名为bait的目录
0) Prepare workspace Create %Temp%workspace Create %Temp%workspacemountpoint Create %Temp%workspacebait <DIR> %Temp%workspace |__ <DIR> mountpoint |__ <DIR> redir
- 通过GetTempPath获取当前用户的Temp目录,一般而言这个目录为
- 如果提供了自己的恶意dll文件就直接使用提供的恶意dll文件地址,否则自己生成一个dll文件,生成的文件内容随意
BOOL BitsArbitraryFileMove::WriteSourceFile() { HANDLE hFile; BOOL bErrorFlag = FALSE; const char* fileContent = "foo123rn"; DWORD dwBytesToWrite = (DWORD)strlen(fileContent); DWORD dwBytesWritten = 0; hFile = CreateFile(m_wszSourceFilePath, GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL); [...]
- 调用
ReparsePoint::CreateMountPoint
生成一个挂载,将mountpoint目录挂载到bait目录上。(ReparsePoint挂载了用户定义的数据)
调用BITS服务
首先在mountpoint文件夹下面创建了一个 test.txt
文件,然后调用了 CbitsCom.cpp
文件当中的 PrepareJob
函数。函数的具体工作如下:
- CoCreateInstance通过UUID创建
BackgroundCopyQMgr
实例 - 调用
StringFromCLSID
根据BITSCOM_GUID_GROUP
生成String类型的groupGuidStr
- 调用
GetGroup
获得m_pBackgroundCopyGroup
-
m_pBackgroundCopyGroup->CancelGroup()
取消CopyGroup当中的group -
m_pBackgroundCopyQMgr->CreateGroup()
生成CopyQMgr对应的group -
m_pBackgroundCopyGroup->CreateJob
创建一个job,把这个job对应的实例保存到m_pBackgroundCopyJob1
当中去 - 调用
m_pBackgroundCopyJob1->AddFiles
将本地的\\127.0.0.1\C$\Windows\System32\drivers\etc\hosts
文件和在mountpoint文件夹下面创建的test.txt
文件作为参数传入
这样AddFiles就OK了。创建了一个BIT传输的任务,任务的意思是将\\127.0.0.1\C$\Windows\System32\drivers\etc\hosts
传输到 test.txt
当中
查找BITS创建的TMP文件
因为之前已经将mountpoint当中的bait文件夹挂载成了bait文件夹的链接,因此可以在这个文件夹当中找到名为 BIT*.tmp
的文件。
调用了FindFirstFile函数,将搜索到的变量信息保存到 structWin32FindData
变量当中。
BOOL BitsArbitraryFileMove::FindBitsTempFile()
{
WIN32_FIND_DATA structWin32FindData;
[...]
ZeroMemory(wszSearchPath, MAX_PATH * sizeof(WCHAR));
StringCchCat(wszSearchPath, MAX_PATH, m_wszBaitDirPath);
StringCchCat(wszSearchPath, MAX_PATH, L"BIT*.tmp");
hRes = FindFirstFile(wszSearchPath, &structWin32FindData);
搜索到的名字保存在 m_wszBitsTempFileName
变量当中。
给查找到的文件设置oplock
oplock全称为Opportunistic Locks。是客户端给放置在服务器上文件上的锁,在大多数情况下,客户端请求一个oplock,以便它可以在本地缓存数据,从而减少了网络流量并缩短了明显的响应时间。 oplock由具有远程服务器的客户端上的网络重定向器以及本地服务器上的客户端应用程序使用。
oplock协调客户端和服务器之间以及多个客户端之间的数据缓存和一致性。一致的数据是整个网络上相同的数据。换句话说,如果数据是连贯的,则服务器和所有客户端上的数据将同步。
这里给查找到的BITS创建的TMP文件上了一个oplock,这样就会把bait文件夹下面的TMP文件和对应的workload文件夹下面的TMP文件进行同步。
设置oplock通常通过 DeviceIoControl 的系统调用来进行,设置调用的状态 dwIoControlCode 为对应的OPLOCK值即可。
if (exclusive)
{
DeviceIoControl(g_hFile,
FSCTL_REQUEST_OPLOCK_LEVEL_1,
NULL, 0,
NULL, 0,
&bytesReturned,
&g_o);
}
else
{
DeviceIoControl(g_hFile, FSCTL_REQUEST_OPLOCK,
&g_inputBuffer, sizeof(g_inputBuffer),
&g_outputBuffer, sizeof(g_outputBuffer),
nullptr, &g_o);
}
调用Resume函数进行同步
调用 CBitsCom::ResumeJob()
函数之后,就会开始进行传输,将 \\127.0.0.1\C$\Windows\System32\drivers\etc\hosts
文件写到TMP文件当中,这个过程仍然是模拟用户进行的。
从上图中可以看出,在开始往tmp文件中传输的时候依然还是模拟的user3用户
// --- Resume job ---
hRes = pBackgrounCopyJob->Resume();
if (FAILED(hRes))
{
wprintf(L"[-] IBackgroundCopyJob->Resume() failed. HRESULT=0x%08Xn", hRes);
return BITSCOM_ERR_RESUMEJOB;
}
创建挂载点
等待Oplock的触发,如果触发了Oplock,说明BITS服务的传输已经完成了。正准备对TMP文件进行重命名,这里是这个漏洞的关键位置所在。重命名的时候不会impersonate
当前的用户,而是以服务本身的权限去执行的。
通过procmon可以看到当创建完挂载点之后进行重命名,也就是调用 CreateFile
的时候没有模拟当前的用户。这个时候在重命名之前重新挂载文件的位置就能在重命名的时候把创建的链接文件也同时一起移动。
在进行重命名之前,我们还可以做一些其他的工作
- 首先删除了mountpoint文件夹下面的挂载点 (
ReparsePoint::DeleteMountPoint
删除了挂载点) - 重新将mountpoint挂载到
\RPC Control
上 (ReparsePoint::CreateMountPoint
创建了新的挂载点),这样在对TMP文件进行进行重命名的时候也会将dll文件从一个位置移动到另一个位置。这一点可以参考这一篇文章。
创建Symlinks
创建完挂载点之后,访问 RPC Control
就是访问我们创建的mountpoint目录。
创建这个新的挂载点的目的是为了最后在重命名的时候能够将我们自己的 FakeDll.dll
移动到 System32
下面。
C:workspacemountpoint -> RPC Control
Symlink #1: RPC ControlBIT1337.tmp -> C:workspaceFakeDll.dll
Symlink #2: RPC Controltest.txt -> C:WindowsSystem32FakeDll.dll
调用 CreateSymlink
函数生成挂载文件,首先将 RPC ControlBITxxx.tmp
挂载到全局路径的workspace的 FakeDll.dll
上面。
接着继续调用 CreateSymlink
函数生成挂载文件,将 RPCControltest.txt
文件挂载到system32目录下的 FakeDll.dll
上面。
调用Release完成文件移动
已经创建完成了链接之后,继续调用Release能够通过BITS服务的文件移动功能将文件从workspace目录下面移动到System32目录下面。
在 CBitsCom::CompleteJob()
函数中实现,复制当前job的指针并保存到 pBackgroundCopyJob
变量当中,调用GetState获取当前job的状态。直到当前的状态等于 BG_JOB_STATE_TRANSFERRED
之后退出。
[*] Job state: BG_JOB_STATE_CONNECTING
[*] Job state: BG_JOB_STATE_TRANSFERRING
[*] Job state: BG_JOB_STATE_TRANSFERRED
调用Complete函数完成当前job,即通过Symlink将Fake.dll移动到system32处。
获取一个SYSTEM权限的shell
最终使用了UsoDllLoader获得SYSTEMshell。UsoDllLoader同样也是该作者实现的一个项目,顾名思义,这个项目是用来加载Dll并实现本地权限提升的(Local Privilege Escalation)。在Windows10之后,微软提供了一种名叫 Update Session Orchestrator
的服务。通过这个服务能够作为一个普通用户去用COM和系统服务通信,并开始一个 "update scan"
去进行更新。甚至存在一个未被记载的工具 usoclient.exe
实现更新的目的。并且他在开始更新的时候还会尝试去加载一个不存在的DLL windowscoredeviceinfo.dll
。因此如果找到了一个任意文件写的漏洞,就能够将我们自己版本的 windowscoredeviceinfo.dll
复制到 C:WindowsSystem32
当中然后用USO服务加载它,以SYSTEM的权限获得任意代码执行的能力。
需要更加详细的了解的话,可以参考下面的两篇文章
版本限制
Windows10 教育版 1903.18362.30上能够成功利用漏洞,USODllLoader的漏洞也没有被补上
Windows10 教育版 1903.18362.900之后的版本中BITS传输的漏洞就已经被补上了
调用CREATE创建tmp文件的时候会模拟当前的用户,传输会失败(没有往system32文件夹下面写文件的权限)。执行最后一步的时候会出现 BG_JOB_STATE_ERROR
的错误。