1、前言 最近研究RPC在内网中的一些攻击面,主要是以红队视角来看,使用RPC协议有时候BypassEDR等设备会有较好的效果,那么什么是RPC呢,RPC代表远程过程调用,它不是Windows特定的概念。RPC的第一个实现是在80年代在UNIX系统上实现的。这允许机器在网络上相互通信,它甚至被用作网络文件系统(NFS)的基础,其实简单的说就是它允许请求另一台计算机上的服务,本节内容主要是依靠microsoft官方文档进行学习(https:learn。microsoft。comzhcnwindowswin32rpcrpcstartpage)。2、RPC结构相关概念 1、首先我们要理解RPC是如何进行通信的首先需要知道几个概念IDL文件,UUID,ACF文件 IDL文件:为了统一客户端与服务端不同平台处理不同的实现,于是有了IDL语言。IDL文件由一个或多个接口定义组成,每一个接口定义都有一个接口头和一个接口体,接口头包含了使用此接口的信息(UUID和接口版本),接口体包含了接口函数的原型相关细节查看https:learn。microsoft。comzhcnwindowswin32rpctheinterfacedefinitionlanguageidlfile。 UUID:通常为一个16长度的标识符,具有唯一性,在Rpc通信模型中,UUID提供对接口、管理器入口点向量或客户端对象等对象的唯一指定。 ACF:(ACF)的应用程序配置文件有两个部分:接口标头,类似于IDL文件中的接口标头,以及一个正文,其中包含适用于IDL文件的接口正文中定义的类型和函数的配置属性https:learn。microsoft。comzhcnwindowswin32rpctheapplicationconfigurationfileacf。 2、调用过程 RpcStringBindingCompose:需要先创建一个绑定句柄字符串。https:learn。microsoft。comzhcnwindowswin32apirpcdcenfrpcdcerpcstringbindingcompose。 RpcBindingFromStringBinding:通过绑定句柄字符串返回绑定句柄https:learn。microsoft。comzhcnwindowswin32apirpcdcenfrpcdcerpcstringbindingcompose。 3、存根分配和释放内存 在编写RPC调用的时候,必须将函数MIDLuserallocate和MIDLuserfree在项目的中定义。 3、相关攻击面1、IOXIDResolver探测内网多网卡主机 我们发送一个IOXID的传输包,这个发送方式有很多种,我这里用的K8师傅的工具,用Wireshark抓包。 上图中TCP的三个包就不用看了,就是很常见的TCP的三次握手,后四个包中可以如图看,主要关注的是最后一个包,前三个都是固定的,就是交互中用来协商版本之类的参数。 1、先来构造第一个数据包,由于这个包是固定的可以直接CopyWireshark中的,如下图05000b03100000004800000001000000b810b810000000000100000000000100c4fefc9960521b10bbcb00aa0021347a00000000045d888aeb1cc9119fe808002b10486002000000 2、后续第二个是接受的数据包,直接将第三个包复制就可以050000031000000018000000010000000000000000000500 3、主要就是看我们如何剖析最后一个包,将他接收过来并且进行一个分割输出,首先我们是想要枚举他的多网卡信息,和主机信息。我们对数据包进行一个分割。是从0x070x00进行分割。 结束的是在0x090x000xff这一块结束的,把我们接受的数据进行一个分割。 相关代码:https:github。comM0nster3RpcsDemoblobmainOXIDINterkanetworkcardOXID。go 效果图 2、RPCSMB RPC还可以通过不同的协议进行一个访问,例如通过SMB协议传输的RPC服务就可以通过管道进行访问,加入在做项目的时候又有个域凭证就可以进行一写RPC借口的一个调用,比较好用的一个工具是rpcclient,它是执行客户端MSRPC功能的工具。 相关命令的一些总结我发在了https:github。comM0nster3RpcsDemoblobmainRPC20over20SMBMSRPC。md中,大家有需要可以去提取。3、MSSAMR的那些事 该协议支持包含用户和组的帐户存储或目录的管理功能,简单来说就是该协议主要是对Windows用户以及用户组的一些相应操作,例如添加用户,用户组等操作。官方参考:https:learn。microsoft。comzhcnopenspecswindowsprotocolsmssamr4df07fab1bbc452f8e927853a3c7e3801)添加本地用户 调用的APISamrCreateUser2InDomain()可以创建一个用户。longSamrCreateUser2InDomain(〔in〕SAMPRHANDLEDomainHandle,〔in〕PRPCUNICODESTRINGName,〔in〕unsignedlongAccountType,〔in〕unsignedlongDesiredAccess,〔out〕SAMPRHANDLEUserHandle,〔out〕unsignedlongGrantedAccess,〔out〕unsignedlongRelativeId); 在创建用户的时候通过分档来看,不能直接创建到内置域(Builtin)中,需要先创建到账户域(账户)中,如下图。 关于内置域和账户域的相关内容可以参考官方链接:https:learn。microsoft。comzhcnwindowswin32secmgmtbuiltinandaccountdomains 其实简单来说就是,账户域内的用户只能访问该账户所在计算机的资源,而内置域中的账户可以访问域的资源。 由于使用SamrCreateUser2InDomain创建的账户存在禁用标识位,我们先需要为它Set一个属性,来清除禁用标识位。然后才可以将其加入到所在的内置域中。 使用SamrSetInformationUser()这个API为它设置。longSamrSetInformationUser(〔in〕SAMPRHANDLEUserHandle,〔in〕USERINFORMATIONCLASSUserInformationClass,〔in,switchis(UserInformationClass)〕PSAMPRUSERINFOBUFFERBuffer); 编写脚本有两种方式一种是直接调用MSSAMR协议去直接创建一个用户,微软官方给了IDLhttps:learn。microsoft。comenusopenspecswindowsprotocolsmssamr1cd138b9cc1b4706b11549e53189e32e,将其编译,然后构造,这种方式调用起来比较麻烦,另一种是使用神器mimikatz打包好的包,samlib来进行调用,调用的时候将前面的samr改成sam就可以。 参考微软给的官方例子:https:learn。microsoft。comenusopenspecswindowsprotocolsmssamr3d8e23d8d9df481f83b39175f980294c 可以按照这个例子依次构造 首先先求出来账户域Account和内置域的Builts的SID为后续添加账户以及加入到内置域中做准备。 然后获取域对象的句柄,然后为域对象添加用户,并且清除禁用标识位,关键代码。 到这里创建用户的准备工作就结束了,接下来,就是将用户添加到组里面,用到SamAddMemberToAlias这个APIhttps:learn。microsoft。comenusopenspecswindowsprotocolsmssamr9a5d2c35e84b4e59b7b096c6fa0fc8d7longSamrAddMemberToAlias(〔in〕SAMPRHANDLEAliasHandle,〔in〕PRPCSIDMemberId); 相应的Demo:https:github。comM0nster3RpcsDemoblobmainMSSAMRAddUserAddUsermain。c 2)ChangeNtlm 调用的关键API在SamrChangePasswordUser()https:learn。microsoft。comenusopenspecswindowsprotocolsmssamr9699d8cae1a4433ca8c3d7bebeb01476, 当我们获取到了用户名,以及密码NTLMhash,则可以是用这个API将用户的密码修改了。longSamrChangePasswordUser(〔in〕SAMPRHANDLEUserHandle,〔in〕unsignedcharLmPresent,〔in,unique〕PENCRYPTEDLMOWFPASSWORDOldLmEncryptedWithNewLm,〔in,unique〕PENCRYPTEDLMOWFPASSWORDNewLmEncryptedWithOldLm,〔in〕unsignedcharNtPresent,〔in,unique〕PENCRYPTEDNTOWFPASSWORDOldNtEncryptedWithNewNt,〔in,unique〕PENCRYPTEDNTOWFPASSWORDNewNtEncryptedWithOldNt,〔in〕unsignedcharNtCrossEncryptionPresent,〔in,unique〕PENCRYPTEDNTOWFPASSWORDNewNtEncryptedWithNewLm,〔in〕unsignedcharLmCrossEncryptionPresent,〔in,unique〕PENCRYPTEDLMOWFPASSWORDNewLmEncryptedWithNewNt); 这这里遇到了一个坑,就是只用旧的Ntlm就行修改而不对LmCrossEncryptionPresent和NewLmEncryptedWithNewNt进行传参,则会输出一个C000017F的错误,如下图。 我去查看一下这个错误发现是客户端使用当前密码LMhash作为加密密钥请求返回,不清楚为什么不能用当前的密码LMhash,就改了一个其他的LMhash,关键代码。 接下来就是编写POC,我在这里使用微软官方的提供的IDL进行编译https:learn。microsoft。comenusopenspecswindowsprotocolsmssamr1cd138b9cc1b4706b11549e53189e32e,提供了我们需要的所有包,在我们编译好,生成exe的时候会有很多错误,直接将其都注释就好。 根据RPC的调用过程首先需要进行RPC的绑定RPCSTATUSRpcStringBindingComposeW(RPCWSTRObjUuid,RPCWSTRProtSeq,RPCWSTRNetworkAddr,RPCWSTREndpoint,RPCWSTROptions,RPCWSTRStringBinding); 其中的ObjUuid可以直接在提供的IDL中找到,如下图,但是发现这个例子有没有这个都可以,最主要的必须定义一个命名管道端点PIPEsamr。https:learn。microsoft。comenusopenspecswindowsprotocolsmswkst13e9ee5d41254492bcc79a0061f2bbe7关键代码 绑定了之后接下来就是构造SamrChangePasswordUser,https:learn。microsoft。comenusopenspecswindowsprotocolsmssamr9699d8cae1a4433ca8c3d7bebeb01476如果我们不熟悉MSSAMR我们可以倒着堆整个调用流程。longSamrChangePasswordUser(〔in〕SAMPRHANDLEUserHandle,〔in〕unsignedcharLmPresent,〔in,unique〕PENCRYPTEDLMOWFPASSWORDOldLmEncryptedWithNewLm,〔in,unique〕PENCRYPTEDLMOWFPASSWORDNewLmEncryptedWithOldLm,〔in〕unsignedcharNtPresent,〔in,unique〕PENCRYPTEDNTOWFPASSWORDOldNtEncryptedWithNewNt,〔in,unique〕PENCRYPTEDNTOWFPASSWORDNewNtEncryptedWithOldNt,〔in〕unsignedcharNtCrossEncryptionPresent,〔in,unique〕PENCRYPTEDNTOWFPASSWORDNewNtEncryptedWithNewLm,〔in〕unsignedcharLmCrossEncryptionPresent,〔in,unique〕PENCRYPTEDLMOWFPASSWORDNewLmEncryptedWithNewNt); 根据上面的图,以及相关的官方文档,我们发现我们现在就需要传入一个UserHandle用户句柄,其他的就是我们需要输入的NThash,以及我们需要修改的新的NThash,那么这个UserHandle需要从哪里获取呢。这时候可以翻看官方文档。发现一个APISamrOpenUser()https:learn。microsoft。comenusopenspecswindowsprotocolsmssamr0aee1c31ec404633bb560cf8429093c0如下,可以为我们提供我们需要的Userhandle, 这个API意思就是通过RID来获取用户句柄。longSamrOpenUser(〔in〕SAMPRHANDLEDomainHandle,〔in〕unsignedlongDesiredAccess,〔in〕unsignedlongUserId,〔out〕SAMPRHANDLEUserHandle); 继续查看这个API需要什么参数,需要一个域的句柄,所需要的访问权限查看文档https:learn。microsoft。comenusopenspecswindowsprotocolsmssamrc0be3f43bcf943eeb0273d02ab372c53,如下图,由于我们是要实现修改密码,所以我们需要一个指定修改用户密码的能力USERCHANGEPASSWORD,最后还需要一个RID。 通过上面的分析,我们现在好需要两个参数,一个参数是DomainHandle,另一个就是UserId。 继续翻看文档发现这样一个APISamrLookupNamesInDomain(),https:learn。microsoft。comenusopenspecswindowsprotocolsmssamrd91271c67b2e419499278fabfa429f90如下 就是将我们输入的用户名转化为RID,输出一个RID号,到这里我们上面所需要的两个参数中的UserId就找到了。 这里需要的两个参数就是我们输入的用户名,还有和上面SamrOpenUser通向需要的的DomainHandle。longSamrLookupNamesInDomain(〔in〕SAMPRHANDLEDomainHandle,〔in,range(0,1000)〕unsignedlongCount,〔in,sizeis(1000),lengthis(Count)〕RPCUNICODESTRINGNames〔〕,〔out〕PSAMPRULONGARRAYRelativeIds,〔out〕PSAMPRULONGARRAYUse); 我们继续找返现SamrOpenDomainhttps:learn。microsoft。comenusopenspecswindowsprotocolsmssamrba710c905b1242f89e5ad4aacc1329fa这个API,通过SID号可以输出我们需要的域对象句柄。longSamrOpenDomain(〔in〕SAMPRHANDLEServerHandle,〔in〕unsignedlongDesiredAccess,〔in〕PRPCSIDDomainId,〔out〕SAMPRHANDLEDomainHandle); 到这里SamrOpenUser这个API所需要的条件就找全了。 我们需要继续为SamrOpenDomain寻找它所需要输入的内容,服务器句柄,SID号 这一块可以使用SamrLookupDomainInSamServer来获取我们需要的SID。 这个需要一个内置域的名称,也就是上面上面添加本地用户中提到的获取内置域的名称就可以,这里填写Builtin以及一个服务器句柄。longSamrLookupDomainInSamServer(〔in〕SAMPRHANDLEServerHandle,〔in〕PRPCUNICODESTRINGName,〔out〕PRPCSIDDomainId); 获取服务器对象的句柄使用到的APISamrConnect5。https:learn。microsoft。comenusopenspecswindowsprotocolsmssamrc842a8970a424ca5a6072afd05271dae 这个API会返回服务器对象的句柄,需要我们填入我们的服务器,直接填写机器名称就可以。longSamrConnect5(〔in,unique,string〕PSAMPRSERVERNAMEServerName,〔in〕unsignedlongDesiredAccess,〔in〕unsignedlongInVersion,〔in〕〔switchis(InVersion)〕SAMPRREVISIONINFOInRevisionInfo,〔out〕unsignedlongOutVersion,〔out,switchis(OutVersion)〕SAMPRREVISIONINFOOutRevisionInfo,〔out〕SAMPRHANDLEServerHandle); 总结一下: 1、我们首先利用SamrConnect5获取服务器句柄。 2、利用获取到的服务器句柄经过SamrLookupDomainInSamServer获取服务器SID,。 3、接着利用对一步中获取的服务器句柄以及第二步中的SID利用SamrOpenDomain获取域句柄 4、接下来利用获取到的域句柄利用SamrLookupNamesInDomain获取RID号 5、接着利用第四步中的RID以及第三步中的域句柄利用SamrOpenUserAPI获取用户句柄 6、最后利用用户句柄以及之前的NThash和需要修改的NtHash调用SamrChangePasswordUser修改密码。 想要修改的Nthash可以使用python2。importhashlib,binasciiprintbinascii。hexlify(hashlib。new(md4,123456。encode(utf16le))。digest()) 效果图: 完整的Demo: https:github。comM0nster3RpcsDemoblobmainMSSAMRChangeNTLMChangePassmain。c4、MSTSCH 〔MSTSCH〕:任务计划程序服务远程协议,用于注册和配置任务以及查询远程计算机上运行的任务的状态。顾名思义就是利用这个API可以操纵计划任务。https:learn。microsoft。comzhcnopenspecswindowsprotocolsmstschd1058a287e0249488b8d4a347fa64931 直接来看相关APISchRpcRegisterTask()https:learn。microsoft。comzhcnopenspecswindowsprotocolsmstsch849c131a64e446efb0159d4c599c5167 直接向服务器注册一个任务,关键的两个参数一个是我们创建服务的路径,另一个就是定义计划任务的xml。HRESULTSchRpcRegisterTask(〔in,string,unique〕constwchartpath,〔in,string〕constwchartxml,〔in〕DWORDflags,〔in,string,unique〕constwchartsddl,〔in〕DWORDlogonType,〔in〕DWORDcCreds,〔in,sizeis(cCreds),unique〕constTASKUSERCREDpCreds,〔out,string〕wchartpActualPath,〔out〕PTASKXMLERRORINFOpErrorInfo); 奇怪的是我们在编写的时候总是提示我们缺少参数,如下图,我们缺少一个句柄,这个句柄就是我们写RPC时候的一个绑定句柄,这个Demo写起来就简单多了,不需要之前那么多要求,只要配置一个RPC绑定就可以了。 本来以为很简单直接写一个绑定就可以,没想到调用之前的绑定,发现总是失败,后来查找github别人的源码发现需要多一步验证,需要实现RpcBindingSetAuthInfoExA,真是吐了。RPCSTATUSRpcBindingSetAuthInfoExA(RPCBINDINGHANDLEBinding,RPCCSTRServerPrincName,unsignedlongAuthnLevel,unsignedlongAuthnSvc,RPCAUTHIDENTITYHANDLEAuthIdentity,unsignedlongAuthzSvc,RPCSECURITYQOSSecurityQos); 关键代码 效果图: 相关代码: https:github。comM0nster3RpcsDemoblobmainMSTSCHDESKRPCDESKRPCDESKmain。c5、MSSCMR 〔MSSCMR〕:服务控制管理器远程协议https:learn。microsoft。comenusopenspecswindowsprotocolsmsscmr705b624a13de43ccb8a299573da3635f 指定服务控制管理器远程协议,用于远程管理服务控制管理器(SCM),这是一个启用服务配置和服务程序控制的RPC服务器。其实就是一个管理服务的一个RPC协议。 需要调用ROpenSCManagerA、RCreateServiceA也可以创建服务,除了这个之外还可以查看很多文档,还有许多API来使用。DWORDROpenSCManagerA(〔in,string,unique,range(0,SCMAXCOMPUTERNAMELENGTH)〕SVCCTLHANDLEAlpMachineName,〔in,string,unique,range(0,SCMAXNAMELENGTH)〕LPSTRlpDatabaseName,〔in〕DWORDdwDesiredAccess,〔out〕LPSCRPCHANDLElpScHandle);DWORDRCreateServiceA(〔in〕SCRPCHANDLEhSCManager,〔in,string,range(0,SCMAXNAMELENGTH)〕LPSTRlpServiceName,〔in,string,unique,range(0,SCMAXNAMELENGTH)〕LPSTRlpDisplayName,〔in〕DWORDdwDesiredAccess,〔in〕DWORDdwServiceType,〔in〕DWORDdwStartType,〔in〕DWORDdwErrorControl,〔in,string,range(0,SCMAXPATHLENGTH)〕LPSTRlpBinaryPathName,〔in,string,unique,range(0,SCMAXNAMELENGTH)〕LPSTRlpLoadOrderGroup,〔in,out,unique〕LPDWORDlpdwTagId,〔in,unique,sizeis(dwDependSize)〕LPBYTElpDependencies,〔in,range(0,SCMAXDEPENDSIZE)〕DWORDdwDependSize,〔in,string,unique,range(0,SCMAXACCOUNTNAMELENGTH)〕LPSTRlpServiceStartName,〔in,unique,sizeis(dwPwSize)〕LPBYTElpPassword,〔in,range(0,SCMAXPWDSIZE)〕DWORDdwPwSize,〔out〕LPSCRPCHANDLElpServiceHandle); 通过创建的服务是没有开启的,这个时候我们就需要一个开启的APIRStartServiceA,准备好了所有的东西,就可以开始编写Demo。 相关Demo和之前的一样哪些搞就可以了,这里写几个注意的点。 1、当我们使用官方给的IDL编写的时候有很多重命名,我们直接注释就可以,还有一些我们代码中可能用不到的方法,但是由于是使用官方的IDL编译的,所以需要我们实现一下。 2、创建服务的时候只能直接将我们的EXE作为服务启动,因为不是所有程序都可以作为服务的方式运行,作为服务运行需要能返回运行情况等信息,所以有的程序添加后会,这里我提供一个方法,就是使用微软官方的程序srvany。exe 1)首先将srvany。exe添加到服务中并且启动。 2)将我们要执行的内容路径放入到注册表中regaddHKEYLOCALMACHINESYSTEMCurrentControlSetServicesServiceNameParametersvAppDirectorytREGSZdc:f 3)然后将程序放入注册表regaddHKEYLOCALMACHINESYSTEMCurrentControlSetServicesServiceNameParametersvApplicationtREGSZdc:xxx。exefregaddHKEYLOCALMACHINESYSTEMCurrentControlSetServicesServiceNameParametersvAppParameterstREGSZd如果程序需要参数则填在这里,如果不需要,清空这段文字或者整行f 效果图: ng) 这里我们将我们的shellcode执行一下,添加注册表的时候需要将servicesname改为你添加任务的名字。 而且这里还是system权限。 6、SeclogonDumpLsass 这个是splintercode这个师傅发现的https:splintercod3。blogspot。compthehiddensideofseclogonpart2。html 它的原理主要是,不直接调用OpenProcess去打开进程对象,而是利用已经打开的Lsass进程句柄,从而绕过检测,然后利用RpcImpersonateClient尝试使用PID做一个调用者的伪造。 关键细节可以看这个师傅的博客说的很详细了: 效果图: 需要将我们的第一步t1的提取出来,不然直接使用t2解密之后会被杀软杀了。