mirror of
https://github.com/ssteelfactor-oss/NetEnum
synced 2026-08-09 13:10:29 +00:00
Refactor code and improve function implementations
Refactor code to improve readability and maintainability. Update function implementations and remove unused includes.
This commit is contained in:
@@ -1,491 +1,559 @@
|
||||
/*
|
||||
* NetEnum - Passive Active Directory Enumeration Tool
|
||||
* Uses standard ADSI/COM interfaces — generates only normal domain LDAP traffic.
|
||||
*
|
||||
* Build: VS 2022+, /analyze for SAL validation, /W4 for full diagnostics.
|
||||
* Link: activeds.lib adsiid.lib netapi32.lib
|
||||
*
|
||||
* SAL 2.0 annotations are enforced by PREfast (/analyze).
|
||||
* sal.h is pulled in automatically through windows.h.
|
||||
*/
|
||||
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
#include <activeds.h>
|
||||
#include <dsgetdc.h>
|
||||
#include <lm.h>
|
||||
|
||||
#pragma comment(lib, "activeds.lib")
|
||||
#pragma comment(lib, "adsiid.lib")
|
||||
#pragma comment(lib, "netapi32.lib")
|
||||
|
||||
// Convert FILETIME to readable string
|
||||
void FileTimeToString(LARGE_INTEGER* pLargeInt, WCHAR* buffer, size_t bufSize) {
|
||||
if (pLargeInt->QuadPart == 0) {
|
||||
wcscpy_s(buffer, bufSize, L"Never");
|
||||
|
||||
/* ── Configuration ──────────────────────────────────────────────────────── */
|
||||
|
||||
/* LDAP page size: keep moderate — large values generate bulkier responses.
|
||||
* 200 is indistinguishable from normal workstation queries. */
|
||||
#define NE_PAGE_SIZE 200
|
||||
|
||||
/* Limits for high-cardinality object classes */
|
||||
#define NE_LIMIT_USERS 20
|
||||
#define NE_LIMIT_GROUPS 15
|
||||
|
||||
/* Timestamp buffer length: "YYYY-MM-DD HH:MM:SS" = 19 chars + NUL */
|
||||
#define NE_TIMEBUF_LEN 32
|
||||
|
||||
|
||||
/* ── UAC flag table ─────────────────────────────────────────────────────── */
|
||||
/*
|
||||
* Table-driven UAC decoding replaces the original chain of 20 separate
|
||||
* if-statements. Adding or removing a flag now requires touching one line.
|
||||
*/
|
||||
typedef struct {
|
||||
DWORD mask;
|
||||
const char* name;
|
||||
} UacFlag;
|
||||
|
||||
static const UacFlag s_uacFlags[] = {
|
||||
{ 0x0000001, "SCRIPT" },
|
||||
{ 0x0000002, "ACCOUNTDISABLE" },
|
||||
{ 0x0000008, "HOMEDIR_REQUIRED" },
|
||||
{ 0x0000010, "LOCKOUT" },
|
||||
{ 0x0000020, "PASSWD_NOTREQD" },
|
||||
{ 0x0000040, "PASSWD_CANT_CHANGE" },
|
||||
{ 0x0000080, "ENCRYPTED_TEXT_PWD_ALLOWED" },
|
||||
{ 0x0000100, "TEMP_DUPLICATE_ACCOUNT" },
|
||||
{ 0x0000200, "NORMAL_ACCOUNT" },
|
||||
{ 0x0000800, "INTERDOMAIN_TRUST_ACCOUNT" },
|
||||
{ 0x0001000, "WORKSTATION_TRUST_ACCOUNT" },
|
||||
{ 0x0002000, "SERVER_TRUST_ACCOUNT" },
|
||||
{ 0x0010000, "DONT_EXPIRE_PASSWORD" },
|
||||
{ 0x0020000, "MNS_LOGON_ACCOUNT" },
|
||||
{ 0x0040000, "SMARTCARD_REQUIRED" },
|
||||
{ 0x0080000, "TRUSTED_FOR_DELEGATION" },
|
||||
{ 0x0100000, "NOT_DELEGATED" },
|
||||
{ 0x0200000, "USE_DES_KEY_ONLY" },
|
||||
{ 0x0400000, "DONT_REQ_PREAUTH" },
|
||||
{ 0x0800000, "PASSWORD_EXPIRED" },
|
||||
{ 0x1000000, "TRUSTED_TO_AUTH_FOR_DELEGATION"},
|
||||
};
|
||||
|
||||
|
||||
/* ── Forward declarations ───────────────────────────────────────────────── */
|
||||
|
||||
static void FormatLargeIntTime(_In_ const LARGE_INTEGER* pLI,
|
||||
_Out_writes_z_(bufLen) WCHAR* buf,
|
||||
_In_ size_t bufLen);
|
||||
|
||||
static void DecodeUAC(DWORD uac);
|
||||
|
||||
static void PrintColumn(_In_ IDirectorySearch* pSearch,
|
||||
_In_z_ LPCWSTR attrName,
|
||||
_Inout_ ADS_SEARCH_COLUMN* pCol);
|
||||
|
||||
static HRESULT RunQuery(_In_ IDirectorySearch* pSearch,
|
||||
_In_z_ LPCWSTR filter,
|
||||
_In_z_ LPCWSTR title,
|
||||
_In_reads_(attrCount) LPWSTR* attrs,
|
||||
_In_ DWORD attrCount,
|
||||
_In_ int limit);
|
||||
|
||||
_Must_inspect_result_
|
||||
_Success_(SUCCEEDED(return))
|
||||
static HRESULT BuildSearchObject(_Outptr_ IDirectorySearch** ppSearch);
|
||||
|
||||
_Must_inspect_result_
|
||||
_Success_(SUCCEEDED(return))
|
||||
HRESULT RunEnumeration(void);
|
||||
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* FormatLargeIntTime
|
||||
*
|
||||
* Converts a Windows FILETIME stored as LARGE_INTEGER (100-ns ticks since
|
||||
* 1601-01-01) into a human-readable "YYYY-MM-DD HH:MM:SS" wide string.
|
||||
* Writes "Never" when QuadPart == 0 (attribute not set / never occurred).
|
||||
*
|
||||
* _In_ pLI Read-only input; must not be NULL.
|
||||
* _Out_writes_z_(bufLen) buf Output wide-char buffer, null-terminated.
|
||||
* PREfast verifies buffer is large enough.
|
||||
* _In_ bufLen Buffer capacity in WCHARs.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
static void FormatLargeIntTime(
|
||||
_In_ const LARGE_INTEGER* pLI,
|
||||
_Out_writes_z_(bufLen) WCHAR* buf,
|
||||
_In_ size_t bufLen)
|
||||
{
|
||||
if (pLI->QuadPart == 0) {
|
||||
wcscpy_s(buf, bufLen, L"Never");
|
||||
return;
|
||||
}
|
||||
|
||||
FILETIME ft;
|
||||
ft.dwLowDateTime = pLargeInt->LowPart;
|
||||
ft.dwHighDateTime = pLargeInt->HighPart;
|
||||
|
||||
FILETIME ft = { (DWORD)pLI->LowPart, (DWORD)pLI->HighPart };
|
||||
SYSTEMTIME st;
|
||||
FileTimeToSystemTime(&ft, &st);
|
||||
swprintf_s(buffer, bufSize, L"%04d-%02d-%02d %02d:%02d:%02d",
|
||||
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
|
||||
|
||||
swprintf_s(buf, bufLen, L"%04u-%02u-%02u %02u:%02u:%02u",
|
||||
st.wYear, st.wMonth, st.wDay,
|
||||
st.wHour, st.wMinute, st.wSecond);
|
||||
}
|
||||
|
||||
// Decode userAccountControl flags
|
||||
void DecodeUAC(DWORD uac) {
|
||||
printf(" userAccountControl flags: 0x%X\n", uac);
|
||||
if (uac & 0x0001) printf(" - SCRIPT\n");
|
||||
if (uac & 0x0002) printf(" - ACCOUNTDISABLE\n");
|
||||
if (uac & 0x0008) printf(" - HOMEDIR_REQUIRED\n");
|
||||
if (uac & 0x0010) printf(" - LOCKOUT\n");
|
||||
if (uac & 0x0020) printf(" - PASSWD_NOTREQD\n");
|
||||
if (uac & 0x0040) printf(" - PASSWD_CANT_CHANGE\n");
|
||||
if (uac & 0x0080) printf(" - ENCRYPTED_TEXT_PWD_ALLOWED\n");
|
||||
if (uac & 0x0100) printf(" - TEMP_DUPLICATE_ACCOUNT\n");
|
||||
if (uac & 0x0200) printf(" - NORMAL_ACCOUNT\n");
|
||||
if (uac & 0x0800) printf(" - INTERDOMAIN_TRUST_ACCOUNT\n");
|
||||
if (uac & 0x1000) printf(" - WORKSTATION_TRUST_ACCOUNT\n");
|
||||
if (uac & 0x2000) printf(" - SERVER_TRUST_ACCOUNT\n");
|
||||
if (uac & 0x10000) printf(" - DONT_EXPIRE_PASSWORD\n");
|
||||
if (uac & 0x20000) printf(" - MNS_LOGON_ACCOUNT\n");
|
||||
if (uac & 0x40000) printf(" - SMARTCARD_REQUIRED\n");
|
||||
if (uac & 0x80000) printf(" - TRUSTED_FOR_DELEGATION\n");
|
||||
if (uac & 0x100000) printf(" - NOT_DELEGATED\n");
|
||||
if (uac & 0x200000) printf(" - USE_DES_KEY_ONLY\n");
|
||||
if (uac & 0x400000) printf(" - DONT_REQ_PREAUTH\n");
|
||||
if (uac & 0x800000) printf(" - PASSWORD_EXPIRED\n");
|
||||
if (uac & 0x1000000) printf(" - TRUSTED_TO_AUTH_FOR_DELEGATION\n");
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* DecodeUAC
|
||||
*
|
||||
* Prints all set bits in userAccountControl using the flag table above.
|
||||
* Pure console sink — reads uac by value, modifies nothing.
|
||||
* No SAL annotation needed on scalar-by-value parameters.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
static void DecodeUAC(DWORD uac)
|
||||
{
|
||||
printf(" UAC 0x%08X:", uac);
|
||||
|
||||
BOOL anySet = FALSE;
|
||||
for (int i = 0; i < (int)ARRAYSIZE(s_uacFlags); i++) {
|
||||
if (uac & s_uacFlags[i].mask) {
|
||||
printf("\n [+] %s", s_uacFlags[i].name);
|
||||
anySet = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
if (!anySet) printf(" (none)");
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
// Enumerate computers in the domain
|
||||
HRESULT EnumerateComputers(IADs* pDomain) {
|
||||
HRESULT hr;
|
||||
IDirectorySearch* pSearch = NULL;
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* PrintColumn
|
||||
*
|
||||
* Renders one ADS_SEARCH_COLUMN value to stdout, then releases it.
|
||||
* Centralises all attribute-specific formatting in a single place.
|
||||
*
|
||||
* _In_ pSearch COM interface needed for FreeColumn; must not be NULL.
|
||||
* _In_z_ attrName Null-terminated attribute name (dispatch key).
|
||||
* _Inout_ pCol Written by caller via GetColumn, freed here.
|
||||
* _Inout_: function both reads values and invalidates struct.
|
||||
*
|
||||
* NOTE: hSearch is intentionally NOT a parameter — FreeColumn does not need
|
||||
* it (the original code passed it but the ADSI vtable ignores it).
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
static void PrintColumn(
|
||||
_In_ IDirectorySearch* pSearch,
|
||||
_In_z_ LPCWSTR attrName,
|
||||
_Inout_ ADS_SEARCH_COLUMN* pCol)
|
||||
{
|
||||
/* ── Timestamp attributes ──────────────────────────────────────────── */
|
||||
if (wcscmp(attrName, L"lastLogon") == 0 ||
|
||||
wcscmp(attrName, L"pwdLastSet") == 0)
|
||||
{
|
||||
WCHAR ts[NE_TIMEBUF_LEN];
|
||||
if (pCol->dwNumValues > 0)
|
||||
FormatLargeIntTime(&pCol->pADsValues[0].LargeInteger,
|
||||
ts, NE_TIMEBUF_LEN);
|
||||
else
|
||||
wcscpy_s(ts, NE_TIMEBUF_LEN, L"Never");
|
||||
|
||||
wprintf(L"%s\n", ts);
|
||||
}
|
||||
|
||||
/* ── whenCreated is stored as UTC_TIME, not LargeInteger ──────────── */
|
||||
else if (wcscmp(attrName, L"whenCreated") == 0)
|
||||
{
|
||||
if (pCol->dwNumValues > 0 && pCol->dwADsType == ADSTYPE_UTC_TIME) {
|
||||
const SYSTEMTIME* s = &pCol->pADsValues[0].UTCTime;
|
||||
wprintf(L"%04u-%02u-%02u %02u:%02u:%02u UTC\n",
|
||||
s->wYear, s->wMonth, s->wDay,
|
||||
s->wHour, s->wMinute, s->wSecond);
|
||||
} else {
|
||||
wprintf(L"N/A\n");
|
||||
}
|
||||
}
|
||||
|
||||
/* ── userAccountControl — decode individual bits ───────────────────── */
|
||||
else if (wcscmp(attrName, L"userAccountControl") == 0)
|
||||
{
|
||||
if (pCol->dwNumValues > 0) {
|
||||
wprintf(L"\n");
|
||||
DecodeUAC((DWORD)pCol->pADsValues[0].Integer);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── adminCount — flag privileged accounts prominently ────────────── */
|
||||
else if (wcscmp(attrName, L"adminCount") == 0)
|
||||
{
|
||||
if (pCol->dwNumValues > 0) {
|
||||
LONG v = pCol->pADsValues[0].Integer;
|
||||
wprintf(L"%d%s\n", v, v > 0 ? L" *** PRIVILEGED ***" : L"");
|
||||
} else {
|
||||
wprintf(L"0\n");
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Multi-value string lists (memberOf, servicePrincipalName) ─────── */
|
||||
else if (wcscmp(attrName, L"memberOf") == 0 ||
|
||||
wcscmp(attrName, L"servicePrincipalName") == 0)
|
||||
{
|
||||
if (pCol->dwNumValues == 0) {
|
||||
wprintf(L"None\n");
|
||||
} else {
|
||||
wprintf(L"\n");
|
||||
for (DWORD j = 0; j < pCol->dwNumValues; j++)
|
||||
wprintf(L" [%u] %s\n", j,
|
||||
pCol->pADsValues[j].CaseIgnoreString);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── member — show count, list up to 5, truncate if more ──────────── */
|
||||
else if (wcscmp(attrName, L"member") == 0)
|
||||
{
|
||||
DWORD n = pCol->dwNumValues;
|
||||
DWORD show = (n > 5) ? 3 : n;
|
||||
|
||||
wprintf(L"(%u total%s)\n",
|
||||
n, (n > 5) ? L", showing first 3" : L"");
|
||||
|
||||
for (DWORD j = 0; j < show; j++)
|
||||
wprintf(L" [%u] %s\n", j,
|
||||
pCol->pADsValues[j].DNString);
|
||||
}
|
||||
|
||||
/* ── Generic fallback: DN string, case-ignore string, or integer ───── */
|
||||
else
|
||||
{
|
||||
if (pCol->dwNumValues == 0) {
|
||||
wprintf(L"N/A\n");
|
||||
} else {
|
||||
for (DWORD j = 0; j < pCol->dwNumValues; j++) {
|
||||
switch (pCol->dwADsType) {
|
||||
case ADSTYPE_DN_STRING:
|
||||
wprintf(L"%s", pCol->pADsValues[j].DNString
|
||||
? pCol->pADsValues[j].DNString : L"(null)");
|
||||
break;
|
||||
case ADSTYPE_CASE_IGNORE_STRING:
|
||||
wprintf(L"%s", pCol->pADsValues[j].CaseIgnoreString
|
||||
? pCol->pADsValues[j].CaseIgnoreString : L"(null)");
|
||||
break;
|
||||
case ADSTYPE_INTEGER:
|
||||
wprintf(L"%d", pCol->pADsValues[j].Integer);
|
||||
break;
|
||||
default:
|
||||
wprintf(L"[type=%u]", pCol->dwADsType);
|
||||
break;
|
||||
}
|
||||
if (j < pCol->dwNumValues - 1) wprintf(L", ");
|
||||
}
|
||||
wprintf(L"\n");
|
||||
}
|
||||
}
|
||||
|
||||
pSearch->lpVtbl->FreeColumn(pSearch, pCol);
|
||||
}
|
||||
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* RunQuery
|
||||
*
|
||||
* Executes one LDAP search, iterates rows, prints attributes.
|
||||
* All searches share this path — no duplicated loop logic.
|
||||
*
|
||||
* _In_ pSearch Active IDirectorySearch; must not be NULL.
|
||||
* _In_z_ filter LDAP filter string.
|
||||
* _In_z_ title Section header for console output.
|
||||
* _In_reads_(attrCount) attrs Array of attribute name strings.
|
||||
* _In_ attrCount Element count of attrs[].
|
||||
* _In_ limit Max rows to print (0 = unlimited).
|
||||
*
|
||||
* Returns S_OK on success, propagates ADSI HRESULTs on failure.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
static HRESULT RunQuery(
|
||||
_In_ IDirectorySearch* pSearch,
|
||||
_In_z_ LPCWSTR filter,
|
||||
_In_z_ LPCWSTR title,
|
||||
_In_reads_(attrCount) LPWSTR* attrs,
|
||||
_In_ DWORD attrCount,
|
||||
_In_ int limit)
|
||||
{
|
||||
wprintf(L"\n\n╔══ %s ══\n\n", title);
|
||||
|
||||
ADS_SEARCH_HANDLE hSearch = NULL;
|
||||
HRESULT hr = pSearch->lpVtbl->ExecuteSearch(
|
||||
pSearch, filter, attrs, attrCount, &hSearch);
|
||||
|
||||
hr = ADsGetObject(L"LDAP://rootDSE", &IID_IADs, (void**)&pDomain);
|
||||
if (FAILED(hr)) {
|
||||
wprintf(L"Failed to connect to rootDSE: 0x%x\n", hr);
|
||||
wprintf(L" [!] ExecuteSearch failed: 0x%08X\n", hr);
|
||||
return hr;
|
||||
}
|
||||
|
||||
VARIANT var;
|
||||
int count = 0;
|
||||
HRESULT hrRow;
|
||||
|
||||
while ((hrRow = pSearch->lpVtbl->GetNextRow(pSearch, hSearch))
|
||||
!= S_ADS_NOMORE_ROWS)
|
||||
{
|
||||
if (FAILED(hrRow)) break; /* network / server error */
|
||||
|
||||
count++;
|
||||
|
||||
if (limit > 0 && count > limit) {
|
||||
wprintf(L" ... result set capped at %d entries\n", limit);
|
||||
break;
|
||||
}
|
||||
|
||||
wprintf(L" ─── #%d ───\n", count);
|
||||
|
||||
for (DWORD i = 0; i < attrCount; i++) {
|
||||
ADS_SEARCH_COLUMN col;
|
||||
hr = pSearch->lpVtbl->GetColumn(pSearch, hSearch, attrs[i], &col);
|
||||
if (SUCCEEDED(hr)) {
|
||||
wprintf(L" %-30s ", attrs[i]);
|
||||
PrintColumn(pSearch, attrs[i], &col);
|
||||
}
|
||||
}
|
||||
|
||||
wprintf(L"\n");
|
||||
}
|
||||
|
||||
wprintf(L" Total: %d\n", count);
|
||||
pSearch->lpVtbl->CloseSearchHandle(pSearch, hSearch);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* BuildSearchObject
|
||||
*
|
||||
* Connects to LDAP://rootDSE, reads defaultNamingContext, returns a ready
|
||||
* IDirectorySearch bound to that naming context.
|
||||
*
|
||||
* Centralises all connection + preference setup so RunEnumeration stays
|
||||
* focused on "what to query" rather than "how to connect".
|
||||
*
|
||||
* _Outptr_ ppSearch Receives the COM object on success; always NULL on
|
||||
* failure. _Outptr_ guarantees PREfast that the pointer
|
||||
* itself (not just *ppSearch) is non-NULL.
|
||||
*
|
||||
* Cleanup: on any failure all locally acquired COM objects are released
|
||||
* via goto — no resource leaks on error paths.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
_Must_inspect_result_
|
||||
_Success_(SUCCEEDED(return))
|
||||
static HRESULT BuildSearchObject(_Outptr_ IDirectorySearch** ppSearch)
|
||||
{
|
||||
*ppSearch = NULL; /* guarantee caller's pointer is clean on failure */
|
||||
|
||||
HRESULT hr;
|
||||
IADs* pDSE = NULL;
|
||||
VARIANT var;
|
||||
WCHAR path[512];
|
||||
|
||||
VariantInit(&var);
|
||||
hr = pDomain->lpVtbl->Get(pDomain, L"defaultNamingContext", &var);
|
||||
|
||||
/* Step 1 – bind to the DSA entry point */
|
||||
hr = ADsGetObject(L"LDAP://rootDSE", &IID_IADs, (void**)&pDSE);
|
||||
if (FAILED(hr)) {
|
||||
pDomain->lpVtbl->Release(pDomain);
|
||||
return hr;
|
||||
wprintf(L"[!] rootDSE bind failed: 0x%08X\n", hr);
|
||||
goto done;
|
||||
}
|
||||
|
||||
WCHAR searchPath[512];
|
||||
swprintf_s(searchPath, 512, L"LDAP://%s", var.bstrVal);
|
||||
/* Step 2 – read the default naming context */
|
||||
hr = pDSE->lpVtbl->Get(pDSE, L"defaultNamingContext", &var);
|
||||
if (FAILED(hr)) {
|
||||
wprintf(L"[!] defaultNamingContext read failed: 0x%08X\n", hr);
|
||||
goto done;
|
||||
}
|
||||
|
||||
swprintf_s(path, ARRAYSIZE(path), L"LDAP://%s", var.bstrVal);
|
||||
wprintf(L"[*] Domain path: %s\n", path);
|
||||
|
||||
/* Step 3 – bind a search object to the naming context */
|
||||
hr = ADsGetObject(path, &IID_IDirectorySearch, (void**)ppSearch);
|
||||
if (FAILED(hr)) {
|
||||
wprintf(L"[!] IDirectorySearch bind failed: 0x%08X\n", hr);
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* Step 4 – apply search preferences.
|
||||
*
|
||||
* ASYNCHRONOUS = FALSE keeps the call synchronous; combined with paged
|
||||
* results (NE_PAGE_SIZE) this produces the same traffic pattern as any
|
||||
* standard domain-joined tool performing AD lookups. */
|
||||
ADS_SEARCHPREF_INFO prefs[3];
|
||||
|
||||
prefs[0].dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE;
|
||||
prefs[0].vValue.dwType = ADSTYPE_INTEGER;
|
||||
prefs[0].vValue.Integer = ADS_SCOPE_SUBTREE;
|
||||
|
||||
prefs[1].dwSearchPref = ADS_SEARCHPREF_PAGESIZE;
|
||||
prefs[1].vValue.dwType = ADSTYPE_INTEGER;
|
||||
prefs[1].vValue.Integer = NE_PAGE_SIZE;
|
||||
|
||||
prefs[2].dwSearchPref = ADS_SEARCHPREF_ASYNCHRONOUS;
|
||||
prefs[2].vValue.dwType = ADSTYPE_BOOLEAN;
|
||||
prefs[2].vValue.Boolean = FALSE;
|
||||
|
||||
hr = (*ppSearch)->lpVtbl->SetSearchPreference(*ppSearch, prefs, ARRAYSIZE(prefs));
|
||||
if (FAILED(hr)) {
|
||||
/* Non-fatal: ADSI will use defaults; log and continue. */
|
||||
wprintf(L"[!] SetSearchPreference warning: 0x%08X\n", hr);
|
||||
hr = S_OK;
|
||||
}
|
||||
|
||||
done:
|
||||
VariantClear(&var);
|
||||
pDomain->lpVtbl->Release(pDomain);
|
||||
if (pDSE) pDSE->lpVtbl->Release(pDSE);
|
||||
|
||||
wprintf(L"\n=== Connecting to: %s ===\n\n", searchPath);
|
||||
|
||||
hr = ADsGetObject(searchPath, &IID_IDirectorySearch, (void**)&pSearch);
|
||||
if (FAILED(hr)) {
|
||||
wprintf(L"Failed to create search: 0x%x\n", hr);
|
||||
return hr;
|
||||
/* On failure ensure caller receives NULL, not a partial object */
|
||||
if (FAILED(hr) && *ppSearch) {
|
||||
(*ppSearch)->lpVtbl->Release(*ppSearch);
|
||||
*ppSearch = NULL;
|
||||
}
|
||||
|
||||
// Attributes to retrieve for computers
|
||||
LPWSTR computerAttr[] = {
|
||||
return hr;
|
||||
}
|
||||
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* RunEnumeration
|
||||
*
|
||||
* Orchestrates four sequential LDAP queries:
|
||||
* 1. Computers – all, no limit
|
||||
* 2. User accounts – capped at NE_LIMIT_USERS
|
||||
* 3. Security groups – capped at NE_LIMIT_GROUPS
|
||||
* 4. Service accounts – SPN-bearing users, no limit
|
||||
*
|
||||
* Attribute arrays are declared const-correct (LPWSTR required by ADSI ABI
|
||||
* even though the strings are never mutated — ADSI predates const here).
|
||||
*
|
||||
* _Must_inspect_result_ Caller must check HRESULT.
|
||||
* _Success_(SUCCEEDED(return)) Post-condition for static analysis paths.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
_Must_inspect_result_
|
||||
_Success_(SUCCEEDED(return))
|
||||
HRESULT RunEnumeration(void)
|
||||
{
|
||||
IDirectorySearch* pSearch = NULL;
|
||||
|
||||
HRESULT hr = BuildSearchObject(&pSearch);
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
/* ── 1. Computers ───────────────────────────────────────────────────── */
|
||||
LPWSTR compAttrs[] = {
|
||||
L"cn", L"dNSHostName", L"operatingSystem",
|
||||
L"operatingSystemVersion", L"description",
|
||||
L"distinguishedName", L"whenCreated", L"objectClass",
|
||||
L"distinguishedName", L"whenCreated",
|
||||
L"lastLogon", L"servicePrincipalName", L"userAccountControl"
|
||||
};
|
||||
DWORD computerAttrCount = sizeof(computerAttr) / sizeof(LPWSTR);
|
||||
RunQuery(pSearch,
|
||||
L"(objectClass=computer)",
|
||||
L"COMPUTERS",
|
||||
compAttrs, ARRAYSIZE(compAttrs),
|
||||
0);
|
||||
|
||||
ADS_SEARCHPREF_INFO prefInfo[3];
|
||||
prefInfo[0].dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE;
|
||||
prefInfo[0].vValue.dwType = ADSTYPE_INTEGER;
|
||||
prefInfo[0].vValue.Integer = ADS_SCOPE_SUBTREE;
|
||||
|
||||
prefInfo[1].dwSearchPref = ADS_SEARCHPREF_PAGESIZE;
|
||||
prefInfo[1].vValue.dwType = ADSTYPE_INTEGER;
|
||||
prefInfo[1].vValue.Integer = 1000;
|
||||
|
||||
prefInfo[2].dwSearchPref = ADS_SEARCHPREF_ASYNCHRONOUS;
|
||||
prefInfo[2].vValue.dwType = ADSTYPE_BOOLEAN;
|
||||
prefInfo[2].vValue.Boolean = FALSE;
|
||||
|
||||
hr = pSearch->lpVtbl->SetSearchPreference(pSearch, prefInfo, 3);
|
||||
if (FAILED(hr)) {
|
||||
wprintf(L"Failed to set search preferences: 0x%x\n", hr);
|
||||
}
|
||||
|
||||
// Search for computers
|
||||
wprintf(L"=== COMPUTERS IN DOMAIN ===\n\n");
|
||||
hr = pSearch->lpVtbl->ExecuteSearch(
|
||||
pSearch,
|
||||
L"(objectClass=computer)",
|
||||
computerAttr,
|
||||
computerAttrCount,
|
||||
&hSearch
|
||||
);
|
||||
|
||||
if (SUCCEEDED(hr)) {
|
||||
int computerCount = 0;
|
||||
HRESULT hrRow;
|
||||
|
||||
while ((hrRow = pSearch->lpVtbl->GetNextRow(pSearch, hSearch)) != S_ADS_NOMORE_ROWS) {
|
||||
if (hrRow == S_OK) {
|
||||
computerCount++;
|
||||
ADS_SEARCH_COLUMN col;
|
||||
|
||||
wprintf(L"--- Computer #%d ---\n", computerCount);
|
||||
|
||||
for (DWORD i = 0; i < computerAttrCount; i++) {
|
||||
hr = pSearch->lpVtbl->GetColumn(pSearch, hSearch, computerAttr[i], &col);
|
||||
if (SUCCEEDED(hr)) {
|
||||
wprintf(L" %s: ", computerAttr[i]);
|
||||
|
||||
if (wcscmp(computerAttr[i], L"lastLogon") == 0) {
|
||||
if (col.dwNumValues > 0 && col.pADsValues[0].LargeInteger.QuadPart != 0) {
|
||||
WCHAR timeStr[64];
|
||||
FileTimeToString(&col.pADsValues[0].LargeInteger, timeStr, 64);
|
||||
wprintf(L"%s\n", timeStr);
|
||||
}
|
||||
else {
|
||||
wprintf(L"Never\n");
|
||||
}
|
||||
}
|
||||
else if (wcscmp(computerAttr[i], L"whenCreated") == 0) {
|
||||
if (col.dwNumValues > 0 && col.dwADsType == ADSTYPE_UTC_TIME) {
|
||||
SYSTEMTIME st = col.pADsValues[0].UTCTime;
|
||||
wprintf(L"%04d-%02d-%02d %02d:%02d:%02d UTC\n",
|
||||
st.wYear, st.wMonth, st.wDay,
|
||||
st.wHour, st.wMinute, st.wSecond);
|
||||
}
|
||||
else {
|
||||
wprintf(L"N/A\n");
|
||||
}
|
||||
}
|
||||
else if (wcscmp(computerAttr[i], L"userAccountControl") == 0) {
|
||||
if (col.dwNumValues > 0) {
|
||||
wprintf(L"%d\n", col.pADsValues[0].Integer);
|
||||
DecodeUAC(col.pADsValues[0].Integer);
|
||||
}
|
||||
}
|
||||
else if (wcscmp(computerAttr[i], L"servicePrincipalName") == 0) {
|
||||
if (col.dwNumValues > 0) {
|
||||
wprintf(L"\n");
|
||||
for (DWORD j = 0; j < col.dwNumValues; j++) {
|
||||
wprintf(L" [%d] %s\n", j, col.pADsValues[j].CaseIgnoreString);
|
||||
}
|
||||
}
|
||||
else {
|
||||
wprintf(L"None\n");
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (col.dwNumValues > 0) {
|
||||
for (DWORD j = 0; j < col.dwNumValues; j++) {
|
||||
if (col.dwADsType == ADSTYPE_DN_STRING && col.pADsValues[j].DNString) {
|
||||
wprintf(L"%s", col.pADsValues[j].DNString);
|
||||
}
|
||||
else if (col.dwADsType == ADSTYPE_CASE_IGNORE_STRING && col.pADsValues[j].CaseIgnoreString) {
|
||||
wprintf(L"%s", col.pADsValues[j].CaseIgnoreString);
|
||||
}
|
||||
else if (col.dwADsType == ADSTYPE_INTEGER) {
|
||||
wprintf(L"%d", col.pADsValues[j].Integer);
|
||||
}
|
||||
if (j < col.dwNumValues - 1) wprintf(L", ");
|
||||
}
|
||||
wprintf(L"\n");
|
||||
}
|
||||
else {
|
||||
wprintf(L"N/A\n");
|
||||
}
|
||||
}
|
||||
pSearch->lpVtbl->FreeColumn(pSearch, &col);
|
||||
}
|
||||
}
|
||||
wprintf(L"\n");
|
||||
}
|
||||
}
|
||||
|
||||
wprintf(L"Total computers found: %d\n\n", computerCount);
|
||||
pSearch->lpVtbl->CloseSearchHandle(pSearch, hSearch);
|
||||
}
|
||||
else {
|
||||
wprintf(L"ExecuteSearch failed: 0x%x\n", hr);
|
||||
}
|
||||
|
||||
// Search for users
|
||||
wprintf(L"\n=== USERS ===\n\n");
|
||||
LPWSTR userAttr[] = {
|
||||
/* ── 2. User accounts ───────────────────────────────────────────────── */
|
||||
LPWSTR userAttrs[] = {
|
||||
L"cn", L"sAMAccountName", L"mail", L"description",
|
||||
L"distinguishedName", L"memberOf", L"lastLogon",
|
||||
L"pwdLastSet", L"userAccountControl", L"adminCount"
|
||||
L"distinguishedName", L"memberOf",
|
||||
L"lastLogon", L"pwdLastSet",
|
||||
L"userAccountControl", L"adminCount"
|
||||
};
|
||||
DWORD userAttrCount = sizeof(userAttr) / sizeof(LPWSTR);
|
||||
RunQuery(pSearch,
|
||||
L"(&(objectClass=user)(objectCategory=person))",
|
||||
L"USER ACCOUNTS",
|
||||
userAttrs, ARRAYSIZE(userAttrs),
|
||||
NE_LIMIT_USERS);
|
||||
|
||||
hr = pSearch->lpVtbl->ExecuteSearch(
|
||||
pSearch,
|
||||
L"(&(objectClass=user)(objectCategory=person))",
|
||||
userAttr,
|
||||
userAttrCount,
|
||||
&hSearch
|
||||
);
|
||||
|
||||
if (SUCCEEDED(hr)) {
|
||||
int userCount = 0;
|
||||
HRESULT hrRow;
|
||||
|
||||
while ((hrRow = pSearch->lpVtbl->GetNextRow(pSearch, hSearch)) != S_ADS_NOMORE_ROWS) {
|
||||
if (hrRow == S_OK) {
|
||||
if (++userCount > 20) {
|
||||
wprintf(L"... (showing first 20 users)\n");
|
||||
break;
|
||||
}
|
||||
|
||||
ADS_SEARCH_COLUMN col;
|
||||
wprintf(L"--- User #%d ---\n", userCount);
|
||||
|
||||
for (DWORD i = 0; i < userAttrCount; i++) {
|
||||
hr = pSearch->lpVtbl->GetColumn(pSearch, hSearch, userAttr[i], &col);
|
||||
if (SUCCEEDED(hr)) {
|
||||
wprintf(L" %s: ", userAttr[i]);
|
||||
|
||||
if (wcscmp(userAttr[i], L"lastLogon") == 0 ||
|
||||
wcscmp(userAttr[i], L"pwdLastSet") == 0) {
|
||||
if (col.dwNumValues > 0 && col.pADsValues[0].LargeInteger.QuadPart != 0) {
|
||||
WCHAR timeStr[64];
|
||||
FileTimeToString(&col.pADsValues[0].LargeInteger, timeStr, 64);
|
||||
wprintf(L"%s\n", timeStr);
|
||||
}
|
||||
else {
|
||||
wprintf(L"Never\n");
|
||||
}
|
||||
}
|
||||
else if (wcscmp(userAttr[i], L"userAccountControl") == 0) {
|
||||
if (col.dwNumValues > 0) {
|
||||
wprintf(L"%d\n", col.pADsValues[0].Integer);
|
||||
DecodeUAC(col.pADsValues[0].Integer);
|
||||
}
|
||||
}
|
||||
else if (wcscmp(userAttr[i], L"adminCount") == 0) {
|
||||
if (col.dwNumValues > 0) {
|
||||
wprintf(L"%d ", col.pADsValues[0].Integer);
|
||||
if (col.pADsValues[0].Integer > 0) {
|
||||
wprintf(L"[PRIVILEGED USER]");
|
||||
}
|
||||
wprintf(L"\n");
|
||||
}
|
||||
else {
|
||||
wprintf(L"0\n");
|
||||
}
|
||||
}
|
||||
else if (wcscmp(userAttr[i], L"memberOf") == 0) {
|
||||
if (col.dwNumValues > 0) {
|
||||
wprintf(L"\n");
|
||||
for (DWORD j = 0; j < col.dwNumValues; j++) {
|
||||
wprintf(L" [%d] %s\n", j, col.pADsValues[j].DNString);
|
||||
}
|
||||
}
|
||||
else {
|
||||
wprintf(L"None\n");
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (col.dwNumValues > 0) {
|
||||
for (DWORD j = 0; j < col.dwNumValues; j++) {
|
||||
if (col.dwADsType == ADSTYPE_DN_STRING && col.pADsValues[j].DNString) {
|
||||
wprintf(L"%s", col.pADsValues[j].DNString);
|
||||
}
|
||||
else if (col.dwADsType == ADSTYPE_CASE_IGNORE_STRING && col.pADsValues[j].CaseIgnoreString) {
|
||||
wprintf(L"%s", col.pADsValues[j].CaseIgnoreString);
|
||||
}
|
||||
if (j < col.dwNumValues - 1) wprintf(L", ");
|
||||
}
|
||||
wprintf(L"\n");
|
||||
}
|
||||
else {
|
||||
wprintf(L"N/A\n");
|
||||
}
|
||||
}
|
||||
pSearch->lpVtbl->FreeColumn(pSearch, &col);
|
||||
}
|
||||
}
|
||||
wprintf(L"\n");
|
||||
}
|
||||
}
|
||||
|
||||
wprintf(L"Total users found: %d\n\n", userCount);
|
||||
pSearch->lpVtbl->CloseSearchHandle(pSearch, hSearch);
|
||||
}
|
||||
|
||||
// Search for groups
|
||||
wprintf(L"\n=== SECURITY GROUPS ===\n\n");
|
||||
LPWSTR groupAttr[] = {
|
||||
/* ── 3. Security groups ─────────────────────────────────────────────── */
|
||||
LPWSTR groupAttrs[] = {
|
||||
L"cn", L"description", L"distinguishedName", L"member"
|
||||
};
|
||||
DWORD groupAttrCount = sizeof(groupAttr) / sizeof(LPWSTR);
|
||||
RunQuery(pSearch,
|
||||
L"(objectClass=group)",
|
||||
L"SECURITY GROUPS",
|
||||
groupAttrs, ARRAYSIZE(groupAttrs),
|
||||
NE_LIMIT_GROUPS);
|
||||
|
||||
hr = pSearch->lpVtbl->ExecuteSearch(
|
||||
pSearch,
|
||||
L"(objectClass=group)",
|
||||
groupAttr,
|
||||
groupAttrCount,
|
||||
&hSearch
|
||||
);
|
||||
|
||||
if (SUCCEEDED(hr)) {
|
||||
int groupCount = 0;
|
||||
HRESULT hrRow;
|
||||
|
||||
while ((hrRow = pSearch->lpVtbl->GetNextRow(pSearch, hSearch)) != S_ADS_NOMORE_ROWS) {
|
||||
if (hrRow == S_OK) {
|
||||
if (++groupCount > 15) {
|
||||
wprintf(L"... (showing first 15 groups)\n");
|
||||
break;
|
||||
}
|
||||
|
||||
ADS_SEARCH_COLUMN col;
|
||||
wprintf(L"--- Group #%d ---\n", groupCount);
|
||||
|
||||
for (DWORD i = 0; i < groupAttrCount; i++) {
|
||||
hr = pSearch->lpVtbl->GetColumn(pSearch, hSearch, groupAttr[i], &col);
|
||||
if (SUCCEEDED(hr)) {
|
||||
wprintf(L" %s: ", groupAttr[i]);
|
||||
|
||||
if (wcscmp(groupAttr[i], L"member") == 0) {
|
||||
wprintf(L"(%d members)\n", col.dwNumValues);
|
||||
if (col.dwNumValues > 0 && col.dwNumValues <= 5) {
|
||||
for (DWORD j = 0; j < col.dwNumValues; j++) {
|
||||
wprintf(L" [%d] %s\n", j, col.pADsValues[j].DNString);
|
||||
}
|
||||
}
|
||||
else if (col.dwNumValues > 5) {
|
||||
wprintf(L" (too many to display, showing first 3)\n");
|
||||
for (DWORD j = 0; j < 3; j++) {
|
||||
wprintf(L" [%d] %s\n", j, col.pADsValues[j].DNString);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (col.dwNumValues > 0) {
|
||||
if (col.pADsValues[0].DNString) {
|
||||
wprintf(L"%s\n", col.pADsValues[0].DNString);
|
||||
}
|
||||
else if (col.pADsValues[0].CaseIgnoreString) {
|
||||
wprintf(L"%s\n", col.pADsValues[0].CaseIgnoreString);
|
||||
}
|
||||
else {
|
||||
wprintf(L"N/A\n");
|
||||
}
|
||||
}
|
||||
else {
|
||||
wprintf(L"N/A\n");
|
||||
}
|
||||
}
|
||||
pSearch->lpVtbl->FreeColumn(pSearch, &col);
|
||||
}
|
||||
}
|
||||
wprintf(L"\n");
|
||||
}
|
||||
}
|
||||
|
||||
wprintf(L"Total groups found: %d\n\n", groupCount);
|
||||
pSearch->lpVtbl->CloseSearchHandle(pSearch, hSearch);
|
||||
}
|
||||
|
||||
// Search for service accounts (accounts with SPN)
|
||||
wprintf(L"\n=== SERVICE ACCOUNTS (with SPN) ===\n\n");
|
||||
LPWSTR spnAttr[] = {
|
||||
L"cn", L"sAMAccountName", L"servicePrincipalName",
|
||||
L"distinguishedName", L"pwdLastSet"
|
||||
/* ── 4. Service accounts (Kerberoastable candidates) ────────────────── */
|
||||
LPWSTR spnAttrs[] = {
|
||||
L"cn", L"sAMAccountName",
|
||||
L"servicePrincipalName", L"distinguishedName", L"pwdLastSet"
|
||||
};
|
||||
DWORD spnAttrCount = sizeof(spnAttr) / sizeof(LPWSTR);
|
||||
|
||||
hr = pSearch->lpVtbl->ExecuteSearch(
|
||||
pSearch,
|
||||
L"(&(objectClass=user)(servicePrincipalName=*))",
|
||||
spnAttr,
|
||||
spnAttrCount,
|
||||
&hSearch
|
||||
);
|
||||
|
||||
if (SUCCEEDED(hr)) {
|
||||
int spnCount = 0;
|
||||
HRESULT hrRow;
|
||||
|
||||
while ((hrRow = pSearch->lpVtbl->GetNextRow(pSearch, hSearch)) != S_ADS_NOMORE_ROWS) {
|
||||
if (hrRow == S_OK) {
|
||||
spnCount++;
|
||||
ADS_SEARCH_COLUMN col;
|
||||
wprintf(L"--- Service Account #%d ---\n", spnCount);
|
||||
|
||||
for (DWORD i = 0; i < spnAttrCount; i++) {
|
||||
hr = pSearch->lpVtbl->GetColumn(pSearch, hSearch, spnAttr[i], &col);
|
||||
if (SUCCEEDED(hr)) {
|
||||
wprintf(L" %s: ", spnAttr[i]);
|
||||
|
||||
if (wcscmp(spnAttr[i], L"pwdLastSet") == 0) {
|
||||
if (col.dwNumValues > 0 && col.pADsValues[0].LargeInteger.QuadPart != 0) {
|
||||
WCHAR timeStr[64];
|
||||
FileTimeToString(&col.pADsValues[0].LargeInteger, timeStr, 64);
|
||||
wprintf(L"%s\n", timeStr);
|
||||
}
|
||||
else {
|
||||
wprintf(L"Never\n");
|
||||
}
|
||||
}
|
||||
else if (wcscmp(spnAttr[i], L"servicePrincipalName") == 0) {
|
||||
if (col.dwNumValues > 0) {
|
||||
wprintf(L"\n");
|
||||
for (DWORD j = 0; j < col.dwNumValues; j++) {
|
||||
wprintf(L" [%d] %s\n", j, col.pADsValues[j].CaseIgnoreString);
|
||||
}
|
||||
}
|
||||
else {
|
||||
wprintf(L"None\n");
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (col.dwNumValues > 0) {
|
||||
if (col.pADsValues[0].DNString) {
|
||||
wprintf(L"%s\n", col.pADsValues[0].DNString);
|
||||
}
|
||||
else if (col.pADsValues[0].CaseIgnoreString) {
|
||||
wprintf(L"%s\n", col.pADsValues[0].CaseIgnoreString);
|
||||
}
|
||||
else {
|
||||
wprintf(L"N/A\n");
|
||||
}
|
||||
}
|
||||
else {
|
||||
wprintf(L"N/A\n");
|
||||
}
|
||||
}
|
||||
pSearch->lpVtbl->FreeColumn(pSearch, &col);
|
||||
}
|
||||
}
|
||||
wprintf(L"\n");
|
||||
}
|
||||
}
|
||||
|
||||
wprintf(L"Total service accounts found: %d\n\n", spnCount);
|
||||
pSearch->lpVtbl->CloseSearchHandle(pSearch, hSearch);
|
||||
}
|
||||
RunQuery(pSearch,
|
||||
L"(&(objectClass=user)(servicePrincipalName=*))",
|
||||
L"SERVICE ACCOUNTS [SPN set — potential Kerberoast targets]",
|
||||
spnAttrs, ARRAYSIZE(spnAttrs),
|
||||
0);
|
||||
|
||||
pSearch->lpVtbl->Release(pSearch);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
int wmain(int argc, wchar_t* argv[]) {
|
||||
HRESULT hr = CoInitialize(NULL);
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* wmain
|
||||
*
|
||||
* Entry point. Initialises COM (STA), runs enumeration, tears down.
|
||||
*
|
||||
* _In_ argc Argument count.
|
||||
* _In_reads_(argc) argv Argument vector; argc elements guaranteed.
|
||||
*
|
||||
* Currently no CLI flags are parsed; argc/argv are suppressed via (void)
|
||||
* to avoid C4100 while keeping the annotations in place for future use.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
int wmain(
|
||||
_In_ int argc,
|
||||
_In_reads_(argc) wchar_t* argv[])
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
/*
|
||||
* CoInitializeEx with COINIT_APARTMENTTHREADED is preferred over
|
||||
* CoInitialize() — explicit threading model, same behaviour for
|
||||
* single-threaded console tools, better for future extension.
|
||||
*/
|
||||
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
|
||||
if (FAILED(hr)) {
|
||||
wprintf(L"COM initialization failed: 0x%x\n", hr);
|
||||
wprintf(L"[!] COM init failed: 0x%08X\n", hr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
wprintf(L"=== ACTIVE DIRECTORY ARCHITECTURE ENUMERATION ===\n");
|
||||
wprintf(L"NetEnum — Passive AD Enumeration\n");
|
||||
wprintf(L"Uses standard ADSI/LDAP; no elevated privileges required.\n\n");
|
||||
|
||||
hr = EnumerateComputers(NULL);
|
||||
hr = RunEnumeration();
|
||||
|
||||
if (FAILED(hr)) {
|
||||
wprintf(L"Enumeration failed: 0x%x\n", hr);
|
||||
}
|
||||
if (FAILED(hr))
|
||||
wprintf(L"\n[!] Enumeration terminated: 0x%08X\n", hr);
|
||||
else
|
||||
wprintf(L"\n[*] Done.\n");
|
||||
|
||||
CoUninitialize();
|
||||
return 0;
|
||||
}
|
||||
return SUCCEEDED(hr) ? 0 : 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user