最近研究RPC在内网中的一些攻击面,主要是以红队视角来看,使用RPC协议有时候Bypass EDR等设备会有较好的效果,那么什么是RPC呢,RPC 代表“远程过程调用”,它不是 Windows 特定的概念。RPC 的第一个实现是在80年代在UNIX系统上实现的。这允许机器在网络上相互通信,它甚至被“用作网络文件系统(NFS)的基础”,其实简单的说就是它允许请求另一台计算机上的服务,本节内容主要是依靠Microsoft官方文档进行学习。
1、首先我们要理解RPC是如何进行通信的首先需要知道几个概念IDL文件,UUID,ACF文件
IDL文件:为了统一客户端与服务端不同平台处理不同的实现,于是有了IDL语言。IDL文件由一个或多个接口定义组成,每一个接口定义都有一个接口头和一个接口体,接口头包含了使用此接口的信息(UUID和接口版本),接口体包含了接口函数的原型相关细节查看。
UUID:通常为一个16长度的标识符,具有唯一性,在Rpc通信模型中,UUID 提供对接口、管理器入口点向量或客户端对象等对象的唯一指定。
ACF:(ACF) 的应用程序配置文件有两个部分: 接口标头,类似于 IDL 文件中的接口标头,以及一个 正文,其中包含适用于 IDL 文件的接口正文中定义的类型和函数的配置属性。
2、调用过程
RpcStringBindingCompose:需要先创建一个绑定句柄字符串。。
RpcBindingFromStringBinding:通过绑定句柄字符串返回绑定句柄。
在编写RPC调用的时候,必须将函数MIDL_user_allocate和MIDL_user_free在项目的中定义。
所有的Demo都在 https://github.com/M0nster3/RpcsDemo ,大家可参考。
我们发送一个IOXID的传输包,这个发送方式有很多种,我这里用的K8师傅的工具,用Wireshark抓包。
上图中TCP的三个包就不用看了,就是很常见的TCP的三次握手,后四个包中可以如图看,主要关注的是最后一个包,前三个都是固定的,就是交互中用来协商版本之类的参数。
1、先来构造第一个数据包,由于这个包是固定的可以直接Copy Wireshark中的,如下图
05000b03100000004800000001000000b810b810000000000100000000000100c4fefc9960521b10bbcb00aa0021347a00000000045d888aeb1cc9119fe808002b10486002000000
2、后续第二个是接受的数据包,直接将第三个包复制就可以
050000031000000018000000010000000000000000000500
3、主要就是看我们如何剖析最后一个包,将他接收过来并且进行一个分割输出,首先我们是想要枚举他的多网卡信息,和主机信息。我们对数据包进行一个分割。是从/0x07/0x00/进行分割。
结束的是在0x09/0x00/0xff这一块结束的,把我们接受的数据进行一个分割。
相关代码:https://github.com/M0nster3/RpcsDemo/blob/main/OXIDINterka_network_card/OXID.go
效果图
RPC还可以通过不同的协议进行一个访问,例如通过SMB协议传输的RPC服务就可以通过管道进行访问,加入在做项目的时候又有个域凭证就可以进行一写RPC借口的一个调用,比较好用的一个工具是rpcclient,它是执行客户端 MS-RPC 功能的工具。 相关命令的一些总结我发在了https://github.com/M0nster3/RpcsDemo/blob/main/RPC%20over%20SMB/MS-RPC.md中,大家有需要可以去提取。
该协议支持包含用户和组的帐户存储或目录的管理功能,简单来说就是该协议主要是对Windows用户以及用户组的一些相应操作,例如添加用户,用户组等操作。官方参考.
1)添加本地用户
调用的API SamrCreateUser2InDomain()可以创建一个用户.
long SamrCreateUser2InDomain(
[in] SAMPR_HANDLE DomainHandle,
[in] PRPC_UNICODE_STRING Name,
[in] unsigned long AccountType,
[in] unsigned long DesiredAccess,
[out] SAMPR_HANDLE* UserHandle,
[out] unsigned long* GrantedAccess,
[out] unsigned long* RelativeId
);
在创建用户的时候通过分档来看,不能直接创建到内置域(Builtin)中,需要先创建到账户域(账户)中,如下图。
关于内置域和账户域的相关内容可以参考官方链接.
其实简单来说就是,账户域内的用户只能访问该账户所在计算机的资源,而内置域中的账户可以访问域的资源。
由于使用SamrCreateUser2InDomain创建的账户存在禁用标识位,我们先需要为它Set一个属性,来清除禁用标识位。然后才可以将其加入到所在的内置域中。
使用SamrSetInformationUser() 这个API为它设置。
long SamrSetInformationUser(
[in] SAMPR_HANDLE UserHandle,
[in] USER_INFORMATION_CLASS UserInformationClass,
[in, switch_is(UserInformationClass)]
PSAMPR_USER_INFO_BUFFER Buffer
);
编写脚本有两种方式一种是直接调用MS-SAMR协议去直接创建一个用户,微软官方给了IDL,将其编译,然后构造,这种方式调用起来比较麻烦,另一种是使用神器mimikatz打包好的包,samlib来进行调用,调用的时候将前面的samr改成sam就可以.
参考微软给的官方例子.
可以按照这个例子依次构造
首先先求出来账户域Account和内置域的Builts的SID为后续添加账户以及加入到内置域中做准备。
然后获取域对象的句柄,然后为域对象添加用户,并且清除禁用标识位,关键代码。
到这里创建用户的准备工作就结束了,接下来,就是将用户添加到组里面,用到SamAddMemberToAlias.
long SamrAddMemberToAlias(
[in] SAMPR_HANDLE AliasHandle,
[in] PRPC_SID MemberId
);
相应的Demo:https://github.com/M0nster3/RpcsDemo/blob/main/MS-SAMR/AddUser/AddUser/main.c
2) Change Ntlm
调用的关键API在SamrChangePasswordUser .
当我们获取到了用户名,以及密码NTLMhash,则可以是用这个API将用户的密码修改了。
long SamrChangePasswordUser(
[in] SAMPR_HANDLE UserHandle,
[in] unsigned char LmPresent,
[in, unique] PENCRYPTED_LM_OWF_PASSWORD OldLmEncryptedWithNewLm,
[in, unique] PENCRYPTED_LM_OWF_PASSWORD NewLmEncryptedWithOldLm,
[in] unsigned char NtPresent,
[in, unique] PENCRYPTED_NT_OWF_PASSWORD OldNtEncryptedWithNewNt,
[in, unique] PENCRYPTED_NT_OWF_PASSWORD NewNtEncryptedWithOldNt,
[in] unsigned char NtCrossEncryptionPresent,
[in, unique] PENCRYPTED_NT_OWF_PASSWORD NewNtEncryptedWithNewLm,
[in] unsigned char LmCrossEncryptionPresent,
[in, unique] PENCRYPTED_LM_OWF_PASSWORD NewLmEncryptedWithNewNt
);
这这里遇到了一个坑,就是只用旧的Ntlm就行修改而不对LmCrossEncryptionPresent和NewLmEncryptedWithNewNt进行传参,则会输出一个C000017F的错误,如下图。
我去查看一下这个错误发现是客户端使用当前密码LM hash作为加密密钥请求返回,不清楚为什么不能用当前的密码LM hash,就改了一个其他的LM hash,关键代码。
接下来就是编写POC,我在这里使用微软官方的提供的IDL进行编译,提供了我们需要的所有包,在我们编译好,生成exe的时候会有很多错误,直接将其都注释就好。
根据RPC的调用过程首先需要进行RPC的绑定
RPC_STATUS RpcStringBindingComposeW(
RPC_WSTR ObjUuid,
RPC_WSTR ProtSeq,
RPC_WSTR NetworkAddr,
RPC_WSTR Endpoint,
RPC_WSTR Options,
RPC_WSTR *StringBinding
);
其中的ObjUuid可以直接在提供的IDL中找到,如下图,但是发现这个例子有没有这个都可以,最主要的必须定义一个命名管道端点 \PIPE\samr。 关键代码
绑定了之后接下来就是构造SamrChangePasswordUser,如果我们不熟悉MS-SAMR我们可以倒着堆整个调用流程。
long SamrChangePasswordUser(
[in] SAMPR_HANDLE UserHandle,
[in] unsigned char LmPresent,
[in, unique] PENCRYPTED_LM_OWF_PASSWORD OldLmEncryptedWithNewLm,
[in, unique] PENCRYPTED_LM_OWF_PASSWORD NewLmEncryptedWithOldLm,
[in] unsigned char NtPresent,
[in, unique] PENCRYPTED_NT_OWF_PASSWORD OldNtEncryptedWithNewNt,
[in, unique] PENCRYPTED_NT_OWF_PASSWORD NewNtEncryptedWithOldNt,
[in] unsigned char NtCrossEncryptionPresent,
[in, unique] PENCRYPTED_NT_OWF_PASSWORD NewNtEncryptedWithNewLm,
[in] unsigned char LmCrossEncryptionPresent,
[in, unique] PENCRYPTED_LM_OWF_PASSWORD NewLmEncryptedWithNewNt
);
根据上面的图,以及相关的官方文档,我们发现我们现在就需要传入一个UserHandle用户句柄,其他的就是我们需要输入的NT hash,以及我们需要修改的新的NT hash,那么这个UserHandle需要从哪里获取呢。这时候可以翻看官方文档。发现一个API SamrOpenUser()如下,可以为我们提供我们需要的Userhandle,
这个API意思就是通过RID来获取用户句柄。
long SamrOpenUser(
[in] SAMPR_HANDLE DomainHandle,
[in] unsigned long DesiredAccess,
[in] unsigned long UserId,
[out] SAMPR_HANDLE* UserHandle
);
继续查看这个API需要什么参数,需要一个域的句柄,所需要的访问权限查看文档,如下图,由于我们是要实现修改密码,所以我们需要一个指定修改用户密码的能力USER_CHANGE_PASSWORD,最后还需要一个RID。
通过上面的分析,我们现在好需要两个参数,一个参数是DomainHandle,另一个就是UserId.
继续翻看文档发现这样一个API SamrLookupNamesInDomain如下
就是将我们输入的用户名转化为RID,输出一个RID号,到这里我们上面所需要的两个参数中的UserId就找到了。
这里需要的两个参数就是我们输入的用户名,还有和上面SamrOpenUser通向需要的的 DomainHandle。
long SamrLookupNamesInDomain(
[in] SAMPR_HANDLE DomainHandle,
[in, range(0,1000)] unsigned long Count,
[in, size_is(1000), length_is(Count)]
RPC_UNICODE_STRING Names<li>,
[out] PSAMPR_ULONG_ARRAY RelativeIds,
[out] PSAMPR_ULONG_ARRAY Use
);
我们继续找返现 SamrOpenDomain这个API,通过SID号可以输出我们需要的域对象句柄。
long SamrOpenDomain(
[in] SAMPR_HANDLE ServerHandle,
[in] unsigned long DesiredAccess,
[in] PRPC_SID DomainId,
[out] SAMPR_HANDLE* DomainHandle
);
到这里SamrOpenUser这个API所需要的条件就找全了。
我们需要继续为SamrOpenDomain寻找它所需要输入的内容,服务器句柄,SID号
这一块可以使用SamrLookupDomainInSamServer来获取我们需要的SID.
这个需要一个内置域的名称,也就是上面上面添加本地用户中提到的获取内置域的名称就可以,这里填写“Builtin”以及一个服务器句柄。
long SamrLookupDomainInSamServer(
[in] SAMPR_HANDLE ServerHandle,
[in] PRPC_UNICODE_STRING Name,
[out] PRPC_SID* DomainId
);
获取服务器对象的句柄使用到的API SamrConnect5。
这个API 会返回服务器对象的句柄,需要我们填入我们的服务器,直接填写机器名称就可以。
long SamrConnect5(
[in, unique, string] PSAMPR_SERVER_NAME ServerName,
[in] unsigned long DesiredAccess,
[in] unsigned long InVersion,
[in] [switch_is(InVersion)] SAMPR_REVISION_INFO* InRevisionInfo,
[out] unsigned long* OutVersion,
[out, switch_is(*OutVersion)] SAMPR_REVISION_INFO* OutRevisionInfo,
[out] SAMPR_HANDLE* ServerHandle
);
总结一下:
1、我们首先利用 SamrConnect5 获取服务器句柄。
2、利用获取到的服务器句柄经过SamrLookupDomainInSamServer获取服务器SID,。
3、接着利用对一步中获取的服务器句柄以及第二步中的SID利用SamrOpenDomain获取域句柄
4、接下来利用获取到的域句柄利用SamrLookupNamesInDomain获取RID号
5、接着利用第四步中的RID以及第三步中的域句柄利用SamrOpenUser API获取用户句柄
6、最后利用用户句柄以及之前的NT hash和需要修改的Nt Hash调用SamrChangePasswordUser修改密码。
想要修改的Nt hash 可以使用 python2 。
import hashlib,binascii
print binascii.hexlify(hashlib.new("md4", "123456".encode("utf-16le")).digest())
效果图:
完整的Demo:
https://github.com/M0nster3/RpcsDemo/blob/main/MS-SAMR/ChangeNTLM/ChangePass/main.c
[MS - TSCH]:任务计划程序服务远程协议,用于注册和配置任务以及查询远程计算机上运行的任务的状态。顾名思义就是利用这个API可以操纵计划任务。https://learn.microsoft.com/zh-cn/openspecs/windows_protocols/ms-tsch/d1058a28-7e02-4948-8b8d-4a347fa64931
直接来看相关API SchRpcRegisterTask
直接向服务器注册一个任务,关键的两个参数一个是我们创建服务的路径,另一个就是定义计划任务的xml。
HRESULT SchRpcRegisterTask(
[in, string, unique] const wchar_t* path,
[in, string] const wchar_t* xml,
[in] DWORD flags,
[in, string, unique] const wchar_t* sddl,
[in] DWORD logonType,
[in] DWORD cCreds,
[in, size_is(cCreds), unique] const TASK_USER_CRED* pCreds,
[out, string] wchar_t** pActualPath,
[out] PTASK_XML_ERROR_INFO* pErrorInfo
);
奇怪的是我们在编写的时候总是提示我们缺少参数,如下图,我们缺少一个句柄,这个句柄就是我们写RPC时候的一个绑定句柄,这个Demo写起来就简单多了,不需要之前那么多要求,只要配置一个RPC绑定就可以了。
本来以为很简单直接写一个绑定就可以,没想到调用之前的绑定,发现总是失败,后来查找github别人的源码发现需要多一步验证,需要实现RpcBindingSetAuthInfoExA,真是吐了。
RPC_STATUS RpcBindingSetAuthInfoExA(
RPC_BINDING_HANDLE Binding,
RPC_CSTR ServerPrincName,
unsigned long AuthnLevel,
unsigned long AuthnSvc,
RPC_AUTH_IDENTITY_HANDLE AuthIdentity,
unsigned long AuthzSvc,
RPC_SECURITY_QOS *SecurityQos
);
关键代码
效果图:
相关代码:
https://github.com/M0nster3/RpcsDemo/blob/main/MS-TSCH_DESK/RPCDESK/RPCDESK/main.c
[MS - SCMR]:服务控制管理器远程协议.
指定服务控制管理器远程协议,用于远程管理服务控制管理器 (SCM),这是一个启用服务配置和服务程序控制的 RPC 服务器。其实就是一个管理服务的一个RPC协议。
需要调用ROpenSCManagerA、RCreateServiceA也可以创建服务,除了这个之外还可以查看很多文档,还有许多API来使用。
DWORD ROpenSCManagerA(
[in, string, unique, range(0, SC_MAX_COMPUTER_NAME_LENGTH)]
SVCCTL_HANDLEA lpMachineName,
[in, string, unique, range(0, SC_MAX_NAME_LENGTH)]
LPSTR lpDatabaseName,
[in] DWORD dwDesiredAccess,
[out] LPSC_RPC_HANDLE lpScHandle
);
DWORD RCreateServiceA(
[in] SC_RPC_HANDLE hSCManager,
[in, string, range(0, SC_MAX_NAME_LENGTH)]
LPSTR lpServiceName,
[in, string, unique, range(0, SC_MAX_NAME_LENGTH)]
LPSTR lpDisplayName,
[in] DWORD dwDesiredAccess,
[in] DWORD dwServiceType,
[in] DWORD dwStartType,
[in] DWORD dwErrorControl,
[in, string, range(0, SC_MAX_PATH_LENGTH)]
LPSTR lpBinaryPathName,
[in, string, unique, range(0, SC_MAX_NAME_LENGTH)]
LPSTR lpLoadOrderGroup,
[in, out, unique] LPDWORD lpdwTagId,
[in, unique, size_is(dwDependSize)]
LPBYTE lpDependencies,
[in, range(0, SC_MAX_DEPEND_SIZE)]
DWORD dwDependSize,
[in, string, unique, range(0, SC_MAX_ACCOUNT_NAME_LENGTH)]
LPSTR lpServiceStartName,
[in, unique, size_is(dwPwSize)]
LPBYTE lpPassword,
[in, range(0, SC_MAX_PWD_SIZE)]
DWORD dwPwSize,
[out] LPSC_RPC_HANDLE lpServiceHandle
);
通过创建的服务是没有开启的,这个时候我们就需要一个开启的API RStartServiceA,准备好了所有的东西,就可以开始编写Demo。
相关Demo和之前的一样哪些搞就可以了,这里写几个注意的点。
1、当我们使用官方给的IDL编写的时候有很多重命名,我们直接注释就可以,还有一些我们代码中可能用不到的方法,但是由于是使用官方的IDL编译的,所以需要我们实现一下。
2、创建服务的时候只能直接将我们的EXE作为服务启动,因为不是所有程序都可以作为服务的方式运行,作为服务运行需要能返回运行情况等信息,所以有的程序添加后会,这里我提供一个方法,就是使用微软官方的程序srvany.exe
1)首先将srvany.exe添加到服务中并且启动。
2)将我们要执行的内容路径放入到注册表中
reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\ServiceName\Parameters /v AppDirectory /t REG_SZ /d "c:\" /f
3)然后将程序放入注册表
reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\ServiceName\Parameters /v Application /t REG_SZ /d "c:\xxx.exe" /f
reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\ServiceName\Parameters /v AppParameters /t REG_SZ /d "如果程序需要参数则填在这里,如果不需要,清空这段文字或者整行" /f
效果图:
这里我们将我们的shellcode执行一下,添加注册表的时候需要将servicesname改为你添加任务的名字。
而且这里还是system权限。
这个是splinter_code 这个师傅发现的.
它的原理主要是,不直接调用OpenProcess去打开进程对象,而是利用已经打开的Lsass进程句柄,从而绕过检测,然后利用RpcImpersonateClient尝试使用PID做一个调用者的伪造。
关键细节可以看这个师傅的博客说的很详细了:
效果图:
需要将我们的第一步-t 1的提取出来,不然直接使用-t 2解密之后会被杀软杀了。
参考
https://splintercod3.blogspot.com/p/the-hidden-side-of-seclogon-part-3.html https://splintercod3.blogspot.com/p/the-hidden-side-of-seclogon-part-2.html https://www.anquanke.com/post/id/245482#h3-5 https://mp.weixin.qq.com/s/ByW_tsipzLGKa9mUBImCqA