最近在研究与Windows本地提权相关的安全问题,发现其实就本地提权而言,不局限于BypassUAC和通过内核漏洞,提权还存在非常多的其他手段,在这里主要是想结合Windows中的RPC机制,讲述笔者是如何通过对Windows中的RPC方法进行Fuzz,从而挖掘出Windows尚未进行修复和改进的一些漏洞,进而利用这些漏洞实现本地提权的目的,值得一提的是,和pipepotato
类似,由于利用条件需要拥有Impersonate
权限以及微软对于提权漏洞的判定来说,微软并不认为这是一个提权漏洞,因此相关披露已经得到了微软的许可
相关实现:
1.https://github.com/crisprss/magicAzureAttestService
2.https://github.com/crisprss/PetitPotam
这里就需要介绍本文的主角--RPC
RPC 代表“远程过程调用”,它不是 Windows 特定的概念。RPC 的第一个实现是在80年代在UNIX系统上实现的。这允许机器在网络上相互通信,它甚至被“用作网络文件系统(NFS)的基础”
由微软开发并在Windows上使用的RPC实现是DCE/RPC,它是“分布式计算环境/远程过程调用”的缩写。DCE/RPC只是Windows中使用的众多 IPC(进程间通信)机制之一。例如,它用于允许本地进程甚至网络上的远程客户端与本地或远程机器上的另一个进程或服务交互。
因此,这种协议的安全含义特别有趣。RPC服务器中的漏洞可能会产生各种结果,从拒绝服务 (DoS) 到远程代码执行 (RCE),包括本地权限提升 (LPE)。由于历史遗留问题,Windows上遗留RPC服务器的代码通常很旧,如果我们排除更新的 (D)COM模型,这使它成为一个非常有趣的模糊测试目标。
当我们在使用Impact工具来在域环境中进行横向或者纵向的信息收集或攻击中,或多或少都会使用Windows RPC但可能并不总是完全了解它。
如果你想知道这些工具是如何工作的,或者你想自己寻找Windows RPC
中的bug,我认为主要有两种方法。第一种方法在于寻找文档中有趣的关键词,然后通过impacket库去进行尝试,并且在Github中不乏有记录Windows大量RPC接口的文档,但是它仍然有一些限制,主要的问题是并非所有的RPC接口都被记录在案,甚至现有的文档也不总是完整的。
因此,第二种方法是使用RpcView等工具直接在Windows机器上枚举RPC服务器
在Rpcview的官网中给出了详细介绍,我们可以在Github中下载,由于RPC接口和rpcrt4.dll
相联系,在这里并不建议使用最近更新后的机器下载和运行RPCview,原因是rpcrt4.dll
新的版本可能还未在工具中记录
在这里可以看到对应的版本信息,如果没有覆盖,可以通过手工编译的方式来编译生成一个适配本地环境的RPCview,在这里就不再赘述。
当下载或者编译成功后我们以管理员身份成功打开后,应该是如下图这样:
工具优化
在截图中的右下方我们可以看到有一个部分应该列出通过RPC服务器公开的所有过程或函数,但它实际上只包含地址。
这样并不能够直接准确的定位方法的内容或者信息,但是在Windows的可执行文件中中我们知道微软都会发布它们关联的PDB(程序数据库)
文件
PDB 是一种专有文件格式(由 Microsoft 开发),用于存储有关程序(或通常的程序模块,如 DLL 或 EXE)的调试信息
如果我们事先下载Windows10 SDK
那么这一步应该很简单。SDK包含一个名为的工具symchk.exe
,它允许直接从Microsoft的服务器获取几乎所有EXE或 DLL 的PDB文件。
例如,以下命令允许您下载.dll文件中所有DLL的符号
cd "C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\"
symchk /s srv*c:\SYMBOLS*https://msdl.microsoft.com/download/symbols C:\Windows\System32\*.dll
当下载完所有对应的符号表后,在RPCview中通过Options > Configure Symbols
菜单项进行配置
再次重新启动RPCview后可以看到几乎每个函数的名称都会解析
从上文当我们了解和使用了RPCview后,通过对PetitPotam
的研究,我们先着手自己实现利用PetitPotam
进行本地提权的工具
我们知道在PetitPotam的实现过程中本质也是基于对RPC组件或者说是某个接口的实现,因此在此之前应该需要对Windows中RPC(远程过程调用)做一个简单的介绍
所谓过程调用,就是将控制从一个过程 A 传递到另一个过程 B, 返回时过程 B 将控制进程交给过程 A。目前大多数系统中, 调用者和被调用者都在给定主机系统中的一个进程中, 它们是在生成可执行文件时由链接器连接起来的, 这类过程调用称为本地过程调用。
Rpc(远程过程调用):基于网络端口而实现,支持协议:https://docs.microsoft.com/en-us/windows/win32/rpc/string-binding
Rpc是广义的,RPC可以发生在不同的主机之间,也可以发生在同一台主机上,发生在同一台主机上就是LPC。
而本地过程调用(LPC):是压栈直接调函数,远程过程调用也是调函数,但是在调用另一个进程的函数,而为了区分调用哪个函数设置了一些标识,这些表示则对应两个进程的对应的函数,所以客户端传给服务端不仅仅需要传递函数的参数还需要给那些标识表示调用哪个函数。
MSDN中给出了一张关于RPC体系结构图:
了解了这些之后我们应该还认识到如下一些问题:
为此我们还需要了解在实现RPC通信过程中需要进行如下的配置:
IDL文件:为了统一客户端与服务端不同平台处理不同的实现,于是有了IDL语言。IDL文件由一个或多个接口定义组成,每一个接口定义都有一个接口头和一个接口体,接口头包含了使用此接口的信息(UUID和接口版本),接口体包含了接口函数的原型
UUID:通常为一个16长度的标识符,具有唯一性,在Rpc通信模型中,Rpc运行时库使用UUID来检查客户端和服务器之间的兼容性,也使用它在注册表中配置自身。
在vs 2019中可以在工具->创建UUID中生成唯一的UUID
ACF文件(RPC 应用程序配置文件):Rpc应用程序使用 ACF 文件来描述特定于硬件和操作系统的接口的特性,和IDL文件一起由MIDL编译,所以MIDL编译器(vs自带)可以为不同的平台和系统版本生成代码,这并非是必须的
由它们(ACF/IDL)编译生成后的文件用于描述调用方和被调用过程之间的数据交换和函数原型和参数传递机制。
传递机制其实是和Java中RPC的传递是类似的,将传递给服务端的参数转换为二进制流,服务端接受参数后进行反序列化,这些实现在RpcNDR
引擎,而RpcNDR
引擎发送的数据依赖于idl/.acf
文件编译后生成的存根文件。
另外RPC技术发送Local请求时使用ncalrpc协议,发送Remote请求时使用ncacn_ip_tcp或者ncacn_np协议,前者微软更推荐。
当使用ncacn_np时,DCE/RPC请求被封装在SMB数据包中并发送到远程命名管道。另一方面,当使用ncacn_ip_tcp时,DCE/RPC请求直接通过TCP发送
我们以ncacn_ip_tcp
为例,来看看整个过程中的TCP数据报文
这里可以看到调用一次方法的过程中,首先进行TCP三次握手,然后通过DCERPC协议会有4个数据包,前两次可能是获取接口列表,后两次对应的是客户端传参和调用函数,以及服务端接受序列化的数据后进行的响应包
在这里以一个demo为例来尝试自己编写一个RPC接口供客户端调用服务端的接口函数
首先通过编写接口定义语言(IDL)文件:
确定唯一的UUID以及方法的接口体,这里定义两个方法,在acf配置文件中定义好我们的序列化句柄:
进行编译后会生成3个文件分别对应rpcDemo_c.c、rpcDemo_s.c、rpcDemo_h.h
其中包含一个头文件和分别对应客户端和服务端的文件,不论是在编写客户端还是服务端我们都需要包含头文件,而在对应编写中我们只需要导入对应的客户端或者服务端的c源文件即可
服务端调用过程:
RpcServerUseProtseqEp 函数告诉 RPC 运行时库使用指定的协议序列与指定的终结点组合来接收远程过程调用。
RpcServerRegisterIfEx函数使用 RPC 运行时库注册接口。
RpcServerListen函数向 RPC 运行时库发出信号,以监听远程过程调用。
注意:这里为了能够抓取数据包,选择ncacn_ip_tcp
协议,监听23333端口
客户端调用过程:
RpcStringBindingCompose 生成绑定句柄的字符串。
RpcBindingFromStringBinding 绑定函数从绑定句柄的字符串表示形式返回绑定句柄。
这里一般将调用RPC接口函数的过程放在RPCTryExcept
中:
注意,在这里避免链接错误,我们需要做两个操作
1.包含函数
函数MIDL_user_allocate
和MIDL_user_free
用于为RPC存根分配和释放内存。
MIDL_user_allocate和MIDL_user_free
在实现RPC应用程序时,它们必须在应用程序的某个地方定义,这里直接在主文件定义即可
//在客户端和服务端实现中都要包含这两个函数 void __RPC_FAR* __RPC_USER midl_user_allocate(size_t len) { return(malloc(len)); } void __RPC_USER midl_user_free(void __RPC_FAR *ptr) { free(ptr); }
2.引入rpcrt4.lib依赖包
可以在vs 2019项目属性-链接器中引入,也可以手动通过#pragma comment(lib,"RpcRT4.lib")
进行引入
当服务端和客户端全部编译完成后,就可以远程或者本地过程调用printHello方法,效果如前文图
不同的厂商实现了不同的Rpc协议,这里主要针对windows内部使用的Rpc,对用户来说,它是隐藏的,你并不知道这个调用的方法是部署哪里,不过这并不代表我们无法让它们暴露出来,这里便可以使用RpcView来反编译idl接口
https://github.com/silverf0x/RpcView
由于PpcView无法自动下载对应的PDB,如果我们想进一步利用和探索RPC过程则需要手动下载对应的所有PDB
使用RPCview可以得到进程对应的interface接口和使用的协议,最为关键的是可以反编译IDL文件,该IDL文件不太准确,但是基本上反编译出来还是比较正确的。
请注意,反编译后重要的不是接口函数名字,重要的是函数参数和uuid以及版本,端口协议信息,因为名字和通信是否成功没有任何关系。
我们回到正文,PetitPotam利用基于EFSR接口的加密文件系统远程协议 (EFSRPC),该协议在MSDN中也被十分详细的介绍,EFSRPC协议对远程存储和通过网络访问的加密数据执行维护和管理操作
其中在文档中描述了MS-EFSR接口的调用
在官方文档里面MS-EFSR
的调用有\pipe\lsarpc
和\pipe\efsrpc
两种方法,其中:
\pipe\lsarpc的服务器接口必须是UUID [c681d488-d850-11d0-8c52-00c04fd90f7e]
\pipe\efsrpc的服务器接口必须是UUID [df1941c5-fe89-4e79-bf10-463657acf44d]
当使用Rpcview来查看lsass.exe
进程时我们可以发现:
在这里值得引起我们的注意,因为lsass进程由低特权用户启动,而该进程为特权进程(具有SYSTEM权限)并且能通过命名管道的方式执行相关远程文件操作,很容易联想起Pringbug也是存在类似操作,我们是有可能通过模拟RPC客户端来模拟特权进程创建相关进程的。
这里我们可以选择将其反编译成IDL文件后,生成对应的头文件和源文件,在此基础上实现客户端
这里用反编译得到的IDL实现了客户端,当我们使用接口的UUID位df1941c5-fe89-4e79-bf10-463657acf44d
对应端点名称\pipe\efsrpc
却发现抛出异常:RPC_S_SERVER_UNAVAILABLE代表RPC服务器不可用
这个接口行不通,别忘了MSDN还给出了另外一种调用的方法,那就是通过\pipe\lsarpc
这个端点名称,幸运的是这个特权进程的确也使用了该种方式:
因此我们反编译IDL后重新生成对应的头文件和源文件,运行编译完成的Demo程序:
#pragma comment(lib, "RpcRT4.lib") #include "efsr_h.h" #include <iostream> #include <tchar.h> #include <strsafe.h> int wmain(int argc, wchar_t* agrv[]) { RPC_STATUS status; RPC_WSTR pszStringBinding; RPC_BINDING_HANDLE BindingHandle; status = RpcStringBindingCompose( NULL, (RPC_WSTR)L"ncacn_np", (RPC_WSTR)L"\\\\127.0.0.1",//这里取NULL也能代表本地连接 (RPC_WSTR)L"\\pipe\\lsarpc", NULL, &pszStringBinding ); wprintf(L"[+]RpcStringBindingCompose status: %d\n", status); wprintf(L"[*] String binding: %ws\r\n", pszStringBinding); //绑定接口 status = RpcBindingFromStringBinding(pszStringBinding, &BindingHandle); wprintf(L"[+]RpcBindingFromStringBinding status: %d\n",status); //释放资源 status = RpcStringFree(&pszStringBinding); wprintf(L"RpcStringFree code:%d\n", status); RpcTryExcept{ PVOID pContent; LPWSTR pwszFileName; pwszFileName = (LPWSTR)LocalAlloc(LPTR, MAX_PATH * sizeof(WCHAR)); StringCchPrintf(pwszFileName, MAX_PATH, L"\\\\127.0.0.1\\C$\\tmp\\test.txt"); long result; wprintf(L"[*] Invoking EfsRpcOpenFileRaw with target path: %ws\r\n", pwszFileName); result = Proc0_EfsRpcOpenFileRaw_Downlevel( BindingHandle, &pContent, pwszFileName, 0 ); wprintf(L"[*] EfsRpcOpenFileRaw status code: %d\r\n", result); status = RpcBindingFree( &BindingHandle // Reference to the opened binding handle ); } RpcExcept(1) { wprintf(L"RpcExcetionCode: %d\n", RpcExceptionCode()); }RpcEndExcept } // 下面的函数是为了满足链接需要而写的,没有的话会出现链接错误 void __RPC_FAR* __RPC_USER midl_user_allocate(size_t len) { return(malloc(len)); } void __RPC_USER midl_user_free(void __RPC_FAR* ptr) { free(ptr); }
运行编译后的程序我们发现此时的EfsRpcOpenFileRaw Status Code
为2,对应:ERROR_FILE_NOT_FOUND
,说明该接口函数调用成功了,只是我们指定的文件并不存在
不仅如此,我们可以看到lsass.exe确实尝试访问\\127.0.0.1\C$\tmp\test.txt
不存在的文件,因此出现"找不到文件"错误。
并且我们注意到在lsass.exe
打开了管道\pipe\srvsvc
,这和此前的PrintSpoofer
利用是异曲同工的,我们可以尝试让lsass.exe
访问另外一个恶意的管道,而欺骗手段也是类似,利用/
来绕过路径的检测,欺骗进程访问\\pipe\\xxx\pipe\srvsvc
管道
经过尝试。我们可以通过这样的UNC路径来欺骗进程访问管道:
\\\\127.0.0.1/pipe/crispr\\C$\\x
通过Process Monitor
查看一下是否成功:
可以看到,通过利用特定UNC路径成功欺骗lsass.exe
进程去连接指定的管道,因此为了实现本地提权,我们只需要创建这样一个特定管道去模拟RPC客户端安全上下文即可
管道模拟RPC安全上下文需要SecurityImpersonation权限,因此适用于Service服务用户提权至SYSTEM用户
编写的Petitpotam本地提权工具提供了如下几种接口函数用于本地提权:
1.EfsRpcOpenFileRaw (fixed with CVE-2021-36942)
2: EfsRpcEncryptFileSrv_Downlevel
3: EfsRpcDecryptFileSrv_Downlevel
4: EfsRpcQueryUsersOnFile_Downlevel
5: EfsRpcQueryRecoveryAgents_Downlevel
6: EfsRpcRemoveUsersFromFile_Downlevel
7: EfsRpcAddUsersToFile_Downlevel
Usage:Petitpotam \<EFS-API-to-use> //选择对应的索引即可
部分利用截图:
但是笔者注意到近期微软似乎已经修复了PetitPotam,再利用过程中无法再通过UNC欺骗的方式来骗取特权进程,即lsass.exe
对“自定义命名管道”进行相关通信
可以看到此前的UNC Spoofing已经不再能够使用,这意味着特权进程不再会和“自定义的命名管道”进行通信,但是在虚拟机未修复版本中还是可以正常使用
在对PetitPotam的原理以及利用方式说明之后,我想可能对于如何去寻找和利用现有的RPC方法来实现提权已经有了基本的思路,在这里笔者将披露近期挖掘的两个提权Trick,至少在写文章期间还没有在互联网上能够搜索到相关资料,之所以不叫做0Day,会因为事实上微软认为Impersonation权限已经是高权限用户,这和Potato提权是一个道理,并且事实上只有其中之一能够实现服务用户到系统用户的提权,因此旨在分享思路和利用方式
诊断跟踪服务(又名连接用户体验和遥测服务)可能是最具争议的Windows功能之一,以收集用户和系统数据而闻名,并且值得注意的是该服务是默认开启的服务
于是笔者使用RPCview来查看该对应进程开启的RPC接口:
这里出现了以Download为关键词的一系列API方法,根据接口方法名可以推测出该方法可能与文件操作相关或者是与某些连接访问相关,并且这里是特权进程,如果接口对调用者没有进行身份验证或者鉴别,当我们以普通用户的身份能够充当RPC客户端时,这就意味着我们可以以一个低权限的用户来通过这些API方法来操控特权进程
因此在这里利用RPCview反编译生成了IDL文件,经过构造和尝试后我发现可以以普通用户来成功的调用这些接口方法,我将目光投向了 UtcApi_StartCustomTrace
函数,因为需要在参数中选择一个字符串,这里如果想进一步了解方法的详细操作,可以定位到对应的DLL文件,对该DLL文件进行逆向分析从而判断
早在2020年时@itm4n就在该项服务中挖掘到了一处任意文件读取漏洞,其中利用UtcApi_DownloadLatestSettings
RPC方法,结合James Forshaw的符号链接测试工具https://github.com/googleprojectzero/symboliclink-testing-tools
通过挂载的方式实现Windows任意文件读取,因此笔者也是率先从这项服务中进行探索
笔者在这里利用的是UtcApi_StartCustomTrace
RPC函数
在这里笔者构造了一个作为RPC客户端的程序,主要是将第二个参数,也就是字符串,修改为本地文件
然后笔者再使用Process Monitor
来观察进程执行的操作,如果它确实按照我们的预期访问了我们指定的文件,那么我们就可以通过修改UNC路径来使得特权进程访问恶意管道,从而模拟RPC实现提权
可以看到,我只用普通用户权限的cmd执行程序后,对应的特权进程确实访问了C:\tmp\crispr.txt
,当有了如上的结果时这里笔者尝试让进程访问一个命名管道
注意:这里我选择使用UNC路径,因为它始终适用于大多数提权
同样打开Process Monitor
来查看对应进程的相关行为操作:
特权进程确实会尝试访问命名管道,这意味着我们可以创建恶意管道来模拟RPC
安全上下文
注意:管道模拟RPC客户端需要SecurityImpersonation权限,也就是说可以将权限从服务用户(NT AUTHORITY\LOCAL SERVICE OR SQLSERVER账号等)提升到SYSTEM权限
这也就是为何微软不认为这是提权漏洞的原因,和Rotten Potato类似,均需要SecurityImpersonation
权限,而微软给出的解释认为具有该权限的用户组本身就是SYSTEM用户组,因此并不认为这是提权漏洞,但是在渗透过程中利用范围依然比较广泛,因为服务账户通常是具有该权限的,例如IIS用户和sqlserver用户组等
于是笔者简单地写了一个恶意的管道试图模拟RPC使用特权进程的token创建一个cmd进程
最终的结果是成功的使用了这个RPC接口方法,让本地服务用户能够提升到SYSTEM权限
基于以上,笔者尝试了其他接口函数,已经确认以下接口函数可以用于提权:
1.UtcApi_StartCustomTrace
2.UtcApi_SnapCustomTrace
由于该服务默认是开启的,最终实现的效果为以服务用户的身份成功提权至SYSTEM
但是正当笔者以为能够正常触发之时,我没有意识到这项服务对服务用户来说是透明的,这也意味这想要通过服务用户来模拟管道提权是不可行的,因为服务用户例如sqlserver等没有办法调用RPC接口,这也意味着通过这种方式只能从管理员用户提权到SYSTEM,因此在这里只是做一个过程分享
接上文所述,同样是对服务进行相关接口Fuzz时发现了一个有趣的服务:
与其他服务不同,该服务没有任何的描述,经过相关搜索后发现系统在安装SQL Server 2019后会默认安装和启动这项服务
该服务与微软公司Azure
证明相关,回到RPCview中,通过对该接口的定位发现反编译生成的IDL文件只有一个接口方法:
因为它只实现了一种接口方法,并且注意端点名称是固定的,意味着只要安装了SQL Server 2019,普通用户完全可以充当RPC客户端
笔者开始在本地实现RPC客户端来调用这个接口函数,首先用Rpcview反编译生成对应的IDL文件:
[
uuid(78224520-4978-4616-8075-3d514ad9896f),
version(1.0),
]
interface DefaultIfName
{
long Proc0(
[in]handle_t arg_0,
[in][string] wchar_t* arg_1,
[in][unique][size_is(arg_3)]char* arg_2,
[in]long arg_3,
[out][ref][string][size_is(, *arg_5)] wchar_t** arg_4,
[out]long* arg_5);
}
吸引笔者的是它的字符串参数,所以在这里尝试使用本地路径作为字符串参数,最后调用了函数:
随后打开记录进程的相关操作:
观察结果,特权进程去访问我们指定的UNC格式的本地路径。当然,这个文件在这里是不存在的,自然的结果就是没有找到。
后续操作便和前面的是一致的,因为IRP_MJ_CREATE
是支持UNC路径的
这里直接让RPC服务端访问恶意的管道:
特权进程确实会尝试访问命名管道,这意味着我们可以创建恶意管道来模拟 RPC 客户端安全上下文,从而恶意的管道试图模拟RPC客户端使用特权进程的token创建一个cmd进程
最终的结果是成功的使用了这个RPC接口方法,让本地服务用户能够提升到SYSTEM权限
这里RPC协议是ncalrpc,端点名称是AzureAttestService,都是固定已知,不过利用范围而言,安装SQL Server 2019会默认开启这个服务,笔者并没有对其他SQL Server版本进行检查,这也是局限性所在,因为该项服务并不是默认服务
主要原理就是监听恶意管道,通过SecurityImpersonation权限
可以模拟RPC安全上下文的权限,模拟特权进程的令牌,最终调用CreateProcessWithTokenW
创建进程实现提权
笔者在发现这个安全风险之后也第一时间和MSRC联系,针对这个问题也是进行了修复
本文主要通过Windows中远程过程调用(RPC)来扩展本地提权的思路,分享笔者是如何利用RPCview对RPC接口方法实现Fuzz,最终利用某些存在缺陷的接口方法实现本地提权,目前分享的两种方法在互联网并未搜集到相关提权利用信息,在得到相关许可后决定对其进行披露,当然本文所述思路以及相关经历分享只是抛砖引玉,感谢师傅们的阅读