update 21.07.2026 20:48:17,89

This commit is contained in:
ssteelfactor-oss
2026-07-21 20:48:18 +05:00
parent 29d370d02d
commit ed51e3945e
4 changed files with 233 additions and 66 deletions
+162
View File
@@ -0,0 +1,162 @@
# Kestrel — Detection Footprint
Kestrel is built to be **detectable by design**. It is read-only, uses ordinary
domain-user rights, native LDAP over the normal DC ports, and no evasion. This
document is an honest, complete account of the trace Kestrel leaves so that a
defender can see it, whitelist it, or tell it apart from an attacker — without
reverse-engineering the binary.
Nothing here is obfuscation guidance. It is the opposite: the more precisely a
blue team knows what Kestrel does on the wire and in the logs, the better.
---
## The one-line version
Kestrel **reads**. It never writes. So it can only ever produce read/access
events (LDAP searches, property reads, SYSVOL file reads). It can **never**
produce a directory-modification event, because it never modifies the directory.
---
## What Kestrel never generates
Because every operation is a read, none of the following can come from Kestrel.
If you see these, they are not Kestrel:
| Event | Meaning |
|-------|---------|
| 5136 | A directory object was modified |
| 5137 / 5138 / 5139 / 5141 | Object created / undeleted / moved / deleted |
| 4720 / 4722 / 4725 / 4726 / 4738 | Account created / enabled / disabled / deleted / changed |
| 4728 / 4729 / 4732 / 4733 / 4756 / 4757 | Member added to / removed from a group |
| 4670 | Permissions on an object were changed |
| 4724 / 4723 | Password reset / change |
Kestrel writes no ACEs, adds no members, resets no passwords, plants nothing. A
correct run changes the directory in exactly zero ways.
---
## Kestrel is not DCSync
This is the distinction that matters most for a blue team. Kestrel enumerates
delegation (DCSync rights, RBCD, shadow credentials, ACL edges) by **reading
`nTSecurityDescriptor` and normal attributes over LDAP** — it does **not**
replicate the directory.
- No DRSUAPI / `IDL_DRSGetNCChanges` call is ever made.
- The DS-Replication-Get-Changes / -All extended rights are **detected**, never
**used**.
- Security descriptors are read with a **DACL-only** security mask
(`ADS_SECURITY_INFO_DACL`), so Kestrel never requests the SACL and never needs
`SeSecurityPrivilege`.
Consequently Kestrel does **not** produce the DCSync signature — event **4662**
carrying the replication property-set GUIDs `1131f6aa-…` / `1131f6ad-…`. A tool
that reads *who can* DCSync looks nothing, on the wire, like a tool that *does*
DCSync. This is the whole point of an auditor.
---
## What Kestrel does generate
All of the below are conditional on the DC actually having the corresponding
auditing turned on. On a default-audit DC most of these are silent.
### Event 1644 — expensive / inefficient LDAP search
Fires only when the DC has search-statistics logging enabled (the `15 Field
Engineering` diagnostic level, or the *Expensive*/*Inefficient Search Results
Threshold* registry values). Kestrel's filters lead with **indexed** clauses
(`objectClass`, `objectCategory`, `objectSid`, `sAMAccountName`, `adminCount`),
so the trigger, where it exists, is **result-set size** on the full-domain
enumerations (the ACL and hygiene passes), not a non-indexed filter. All searches
are paged (`ADS_SEARCHPREF_PAGESIZE`).
### Event 4662 — operation on a directory object
Fires when *Audit Directory Service Access* is on **and** the target object has a
SACL that audits the read. Reading `nTSecurityDescriptor` and object properties
on a SACL'd object produces 4662 — but with the **object's own** access mask, not
the replication GUIDs (see above). Putting audit-read SACLs on your Tier-0
objects is, in fact, the cleanest way to see Kestrel (and real attackers) reading
them.
### Event 4661 — a handle to a SAM/DS object was requested
Same preconditions as 4662, for SAM-class objects.
### Events 5145 / 4663 — network-share / file access (SYSVOL)
The SYSVOL-facing scans (`--gpp`, `--policy`, `--gpolateral`, and the SYSVOL leg
of `--trust`) read files under `\\<domain>\SYSVOL` over SMB. With file-share or
object-access auditing enabled, these appear as share/file reads of `GptTmpl.inf`,
GPP XML, answer files, and scripts. Files are capped at 1 MB and recursion is
bounded.
### Authentication
Each LDAP bind authenticates like any domain tool (4624 at the DC). There is
nothing Kestrel-specific here.
---
## Per-scan catalog
Scope key: **B** = base (one object), **1** = one-level, **S** = subtree.
"SD" = reads `nTSecurityDescriptor` (DACL-only mask). "SMB" = reads SYSVOL files.
| Flag | Primary LDAP filter(s) | Scope | SD | SMB | Dominant trace |
|------|------------------------|:----:|:--:|:---:|----------------|
| `--acl` | object classes across the domain NC; domain-head base read (reanimate) | S/B | ✔ | | 4662 on SACL'd objects; 1644 by result size |
| `--groups` | `(&(objectClass=group)(objectSid=…))`, `LDAP_MATCHING_RULE_IN_CHAIN` | B/S | | | indexed; low |
| `--adminsdholder` | `(adminCount=1)` | S | ✔ | | indexed |
| `--sidhistory` | user/group read of `sIDHistory` | B/S | | | indexed |
| `--pwdpolicy` | domain root, PSO container, `(sAMAccountName=krbtgt*)` | B/1/S | | | indexed; small |
| `--hygiene` | `(&(objectCategory=person)(objectClass=user))` | S | | | 1644 by result size |
| `--roast` | SPN / `DONT_REQ_PREAUTH` UAC bit accounts | S | | | indexed lead |
| `--shadowcreds` | `msDS-KeyCredentialLink` readers | S | | | indexed lead |
| `--delegation` | UAC delegation bits + `msDS-AllowedToActOnBehalfOf…` | S | | | indexed lead |
| `--schema` | `(objectClass=classSchema)` in the Schema NC | 1 | | | indexed |
| `--trust` | `(objectClass=trustedDomain)` + SYSVOL | S | | ✔ | indexed; 5145 |
| `--gmsa` | `(objectClass=msDS-Group-Managed-Service-Account)` | S | | | indexed |
| `--adcs` | `pKICertificateTemplate`, `pKIEnrollmentService`, `(cn=NTAuthCertificates)` in Config NC | S | ✔ | | indexed; 4662 |
| `--adfs` | `(&(objectClass=contact)(thumbnailPhoto=*))` under `CN=Microsoft,CN=Program Data` | S | ✔ | | indexed lead; small |
| `--gpp` | — (pure SYSVOL sweep) | — | | ✔ | 5145 / 4663 |
| `--gpolateral` | `(objectClass=groupPolicyContainer)`, `(objectClass=computer)` + SYSVOL | S | | ✔ | indexed; 5145 |
| `--policy` | GPO objects + SYSVOL (`GptTmpl.inf`) | 1/S | | ✔ | indexed; 5145 |
| `--topology` | `(objectClass=nTDSService)`, computer SPNs | S | | | indexed |
| `--adws` | — (TCP connect to 9389 per DC) | — | | | connection only |
The `msDS-Repl*MetaData` provenance reads (`KestrelProvenance.c`) are **base-scope,
single-object**, targeted at the specific high-value object being explained — the
smallest possible footprint, and readable by anyone who can read the object
(*not* gated by DCSync).
---
## For defenders: how to actually see it
1. **Put audit-read SACLs on Tier-0** (Domain Admins, the AdminSDHolder, the
`CN=NTAuthCertificates` object, the AD FS DKM contact object, GPO objects).
Kestrel's SD and property reads then surface as 4662 — and so do a real
attacker's.
2. **Turn on directory-service and file-share auditing** on DCs, and, if you want
the LDAP query view, enable expensive/inefficient search logging (event 1644)
in a test window.
3. **Watch SYSVOL reads** of `GptTmpl.inf`, GPP XML, `unattend`/`sysprep`, and
`.ps1`/`.bat` from a single principal in quick succession — that shape is the
`--gpp`/`--policy` sweep.
## For defenders: how to tell Kestrel from an attacker
The trace shape overlaps with recon tooling — that is unavoidable for any auditor,
and Kestrel does not try to hide it. The distinguishing facts are behavioural:
- **Read-only.** Correlate the read burst with the modification events above. An
auditor produces none of them; post-exploitation almost always produces some.
- **No replication.** No DRSUAPI/`GetNCChanges`, so no DCSync-signature 4662.
- **Ordinary rights.** No privilege escalation, no `SeSecurityPrivilege`
(DACL-only SD reads), no SACL requests.
- **Honest volume.** Paged, indexed-led queries and bounded SYSVOL reads — no
attempt to spread reads out to stay under a threshold.
If you run Kestrel yourself on a schedule, the simplest disambiguation is to
whitelist the account and host you run it from, and treat the same read shape
from anywhere else as worth a look.
+3 -1
View File
@@ -47,6 +47,8 @@ Two honest caveats:
Kestrel does not fragment queries, randomize timing, or hide. It looks normal because it does normal things - that is the design, and its honest limit. Kestrel does not fragment queries, randomize timing, or hide. It looks normal because it does normal things - that is the design, and its honest limit.
Because that is the design, the trace is documented rather than hidden: **[FOOTPRINT.md](FOOTPRINT.md)** catalogues, per scan, the exact LDAP filter / scope / SYSVOL reads and the DC events each one generates (1644 / 4662 / 4661 / 5145) — and, just as importantly, the modification events Kestrel can **never** produce, because it only ever reads. It also explains why reading *who can* DCSync is not DCSync, and how a blue team can tell an audit run from an attacker.
## Requirements ## Requirements
- Windows, domain-joined machine - Windows, domain-joined machine
@@ -506,7 +508,7 @@ Not a scan but a filter for the ACL module. It builds a baseline of "expected" A
| v0.15 | ✅ | **ACL structure audit (ADeleg-class)** — owner ≠ admin · disabled inheritance · non-canonical DACL · orphaned trustees · low-priv → Tier-0 aggregation · schema `defaultSecurityDescriptor` backdoor | | v0.15 | ✅ | **ACL structure audit (ADeleg-class)** — owner ≠ admin · disabled inheritance · non-canonical DACL · orphaned trustees · low-priv → Tier-0 aggregation · schema `defaultSecurityDescriptor` backdoor |
| v0.16 | ✅ | **Stealth persistence + SYSVOL/ADCS depth** — hidden-object / OWNER RIGHTS (`S-1-3-4`) deny-ACE persistence · unattend.xml + SYSVOL secret sweep · ADCS persistence (template validity + NTAuth store) | | v0.16 | ✅ | **Stealth persistence + SYSVOL/ADCS depth** — hidden-object / OWNER RIGHTS (`S-1-3-4`) deny-ACE persistence · unattend.xml + SYSVOL secret sweep · ADCS persistence (template validity + NTAuth store) |
| v0.17 | ✅ | **Cross-domain + hybrid footprint** — foreign security principals in privileged groups · Entra Connect (`MSOL_` / `AAD_`) Tier-0 tagging · ADFS DKM key ACL (Golden SAML precondition) | | v0.17 | ✅ | **Cross-domain + hybrid footprint** — foreign security principals in privileged groups · Entra Connect (`MSOL_` / `AAD_`) Tier-0 tagging · ADFS DKM key ACL (Golden SAML precondition) |
| v0.18 | 🔲 | **Query hygiene + honest footprint** — minimal `SDflags` / security-mask · attribute-list & filter-indexability audit · "Detection footprint" documentation (how each scan appears in event 1644) | | v0.18 | ✅ | **Query hygiene + honest footprint** — minimal `SDflags` / security-mask · attribute-list & filter-indexability audit · [detection-footprint documentation](FOOTPRINT.md) |
| v1.0 | 🔲 | Feature-complete for the on-prem, directory-side posture mission | | v1.0 | 🔲 | Feature-complete for the on-prem, directory-side posture mission |
| post-1.0 | 🔲 | ADExplorer `.dat` snapshot as an offline input source (optional; touches the data-source layer) | | post-1.0 | 🔲 | ADExplorer `.dat` snapshot as an offline input source (optional; touches the data-source layer) |
+63 -63
View File
@@ -69,9 +69,9 @@ static LPWSTR g_rgszRightAttrs[] = {
static BOOL static BOOL
_IsAdminOwner(_In_ PSID pSid) _IsAdminOwner(_In_ PSID pSid)
{ {
LPWSTR s = NULL; LPWSTR s = 0;
BOOL bAdmin = FALSE; BOOL bAdmin = FALSE;
LPCWSTR rid; LPCWSTR rid = 0;
if (!pSid || !IsValidSid(pSid)) return TRUE; /* unknown → don't flag */ if (!pSid || !IsValidSid(pSid)) return TRUE; /* unknown → don't flag */
if (!ConvertSidToStringSidW(pSid, &s) || !s) return TRUE; if (!ConvertSidToStringSidW(pSid, &s) || !s) return TRUE;
@@ -154,7 +154,7 @@ _CheckDaclHygiene(_In_ PACL pDacl, _In_z_ LPCWSTR pwszDN)
if (!pDacl) return; if (!pDacl) return;
for (i = 0; i < pDacl->AceCount; i++) { for (i = 0; i < pDacl->AceCount; i++) {
ACE_HEADER *pAce = NULL; ACE_HEADER *pAce = 0;
BOOL bAllow, bInherited; BOOL bAllow, bInherited;
int rank = 0; int rank = 0;
@@ -176,9 +176,9 @@ _CheckDaclHygiene(_In_ PACL pDacl, _In_z_ LPCWSTR pwszDN)
if (pSid && IsValidSid(pSid) && ConvertSidToStringSidW(pSid, &s) && s) { if (pSid && IsValidSid(pSid) && ConvertSidToStringSidW(pSid, &s) && s) {
LPCWSTR rid = wcsrchr(s, L'-'); LPCWSTR rid = wcsrchr(s, L'-');
if (_wcsnicmp(s, L"S-1-5-21-", 9) == 0 && rid && wcslen(rid + 1) >= 4) { if (_wcsnicmp(s, L"S-1-5-21-", 9) == 0 && rid && wcslen(rid + 1) >= 4) {
WCHAR name[256], dom[256]; WCHAR name[256] = { 0 }, dom[256] = { 0 };
DWORD cn = ARRAYSIZE(name), cd = ARRAYSIZE(dom); DWORD cn = ARRAYSIZE(name), cd = ARRAYSIZE(dom);
SID_NAME_USE use; SID_NAME_USE use = { 0 };
if (!LookupAccountSidW(NULL, pSid, name, &cn, dom, &cd, &use) && if (!LookupAccountSidW(NULL, pSid, name, &cn, dom, &cd, &use) &&
GetLastError() == ERROR_NONE_MAPPED) GetLastError() == ERROR_NONE_MAPPED)
wprintf(L" [ORPHAN-SID] %s — ACE for unresolvable trustee %s\n", pwszDN, s); wprintf(L" [ORPHAN-SID] %s — ACE for unresolvable trustee %s\n", pwszDN, s);
@@ -429,11 +429,11 @@ KestrelBuildExtendedRightsTable(
KESTREL_EXTENDED_RIGHT** ppRightsHead) KESTREL_EXTENDED_RIGHT** ppRightsHead)
{ {
HRESULT hr = S_OK; HRESULT hr = S_OK;
IDirectorySearch* pSearch = NULL; IDirectorySearch* pSearch = 0;
ADS_SEARCH_HANDLE hSearch = NULL; ADS_SEARCH_HANDLE hSearch = 0;
KESTREL_EXTENDED_RIGHT* pHead = NULL; KESTREL_EXTENDED_RIGHT* pHead = 0;
KESTREL_EXTENDED_RIGHT** ppTail = &pHead; /* build list in order */ KESTREL_EXTENDED_RIGHT** ppTail = &pHead; /* build list in order */
WCHAR wszPath[512]; WCHAR wszPath[512] = { 0 };
DWORD cTotal = 0; DWORD cTotal = 0;
if (!pwszConfigNC || !ppRightsHead) return E_INVALIDARG; if (!pwszConfigNC || !ppRightsHead) return E_INVALIDARG;
@@ -606,12 +606,12 @@ KestrelGetObjectSecurityDescriptor(
_Out_ DWORD* pcbSD) _Out_ DWORD* pcbSD)
{ {
HRESULT hr = S_OK; HRESULT hr = S_OK;
ADS_ATTR_INFO* pAttrInfo = NULL; ADS_ATTR_INFO* pAttrInfo = 0;
DWORD cAttrs = 0; DWORD cAttrs = 0;
PSECURITY_DESCRIPTOR pSdCopy = NULL; PSECURITY_DESCRIPTOR pSdCopy = 0;
if (!pDirObj || !ppSD || !pcbSD) return E_INVALIDARG; if (!pDirObj || !ppSD || !pcbSD) return E_INVALIDARG;
*ppSD = NULL; *ppSD = 0;
*pcbSD = 0; *pcbSD = 0;
LPWSTR rgszAttrs[] = { L"nTSecurityDescriptor" }; LPWSTR rgszAttrs[] = { L"nTSecurityDescriptor" };
@@ -629,7 +629,7 @@ KestrelGetObjectSecurityDescriptor(
} }
/* ── 2. Locate the nTSecurityDescriptor attribute ─────────────────── */ /* ── 2. Locate the nTSecurityDescriptor attribute ─────────────────── */
ADS_ATTR_INFO* pSD_Attr = NULL; ADS_ATTR_INFO* pSD_Attr = 0;
for (DWORD i = 0; i < cAttrs; i++) { for (DWORD i = 0; i < cAttrs; i++) {
if (_wcsicmp(pAttrInfo[i].pszAttrName, L"nTSecurityDescriptor") == 0) { if (_wcsicmp(pAttrInfo[i].pszAttrName, L"nTSecurityDescriptor") == 0) {
@@ -665,14 +665,14 @@ KestrelGetObjectSecurityDescriptor(
/* ── 5. Quick sanity check before handing off ─────────────────────── */ /* ── 5. Quick sanity check before handing off ─────────────────────── */
if (!IsValidSecurityDescriptor(pSdCopy)) { if (!IsValidSecurityDescriptor(pSdCopy)) {
HeapFree(GetProcessHeap(), 0, pSdCopy); HeapFree(GetProcessHeap(), 0, pSdCopy);
pSdCopy = NULL; pSdCopy = 0;
hr = E_UNEXPECTED; hr = E_UNEXPECTED;
goto Cleanup; goto Cleanup;
} }
*ppSD = pSdCopy; *ppSD = pSdCopy;
*pcbSD = pProvSpec->dwLength; *pcbSD = pProvSpec->dwLength;
pSdCopy = NULL; /* ownership transferred */ pSdCopy = 0; /* ownership transferred */
Cleanup: Cleanup:
/* pAttrInfo allocated by ADSI — must use FreeADsMem, not HeapFree */ /* pAttrInfo allocated by ADSI — must use FreeADsMem, not HeapFree */
@@ -702,7 +702,7 @@ KestrelWalkDacl(
return HRESULT_FROM_WIN32(GetLastError()); return HRESULT_FROM_WIN32(GetLastError());
for (DWORD i = 0; i < aclInfo.AceCount; i++) { for (DWORD i = 0; i < aclInfo.AceCount; i++) {
LPVOID pAce = NULL; LPVOID pAce = 0;
if (!GetAce(pDacl, i, &pAce)) continue; if (!GetAce(pDacl, i, &pAce)) continue;
ACE_HEADER* pHeader = (ACE_HEADER*)pAce; ACE_HEADER* pHeader = (ACE_HEADER*)pAce;
@@ -715,8 +715,8 @@ KestrelWalkDacl(
continue; continue;
DWORD dwMask = 0; DWORD dwMask = 0;
PSID pTrusteeSid = NULL; PSID pTrusteeSid = 0;
GUID* pObjectType = NULL; GUID* pObjectType = 0;
switch (pHeader->AceType) { switch (pHeader->AceType) {
case ACCESS_ALLOWED_ACE_TYPE: case ACCESS_ALLOWED_ACE_TYPE:
@@ -868,13 +868,13 @@ _ReanimateDefaultHolder(_In_z_ LPCWSTR sid)
static VOID static VOID
_CheckReanimateRights(_In_z_ LPCWSTR pwszDomainNC) _CheckReanimateRights(_In_z_ LPCWSTR pwszDomainNC)
{ {
WCHAR wszPath[600]; WCHAR wszPath[600] = { 0 };
IDirectoryObject *pDirObj = NULL; IDirectoryObject *pDirObj = 0;
PSECURITY_DESCRIPTOR pSD = NULL; PSECURITY_DESCRIPTOR pSD = 0;
DWORD cbSD = 0; DWORD cbSD = 0;
PACL pDacl = NULL; PACL pDacl = 0;
BOOL bPresent = FALSE, bDefault = FALSE; BOOL bPresent = FALSE, bDefault = FALSE;
WORD i; WORD i = 0;
if (FAILED(StringCchPrintfW(wszPath, ARRAYSIZE(wszPath), L"LDAP://%s", pwszDomainNC))) if (FAILED(StringCchPrintfW(wszPath, ARRAYSIZE(wszPath), L"LDAP://%s", pwszDomainNC)))
return; return;
@@ -885,11 +885,11 @@ _CheckReanimateRights(_In_z_ LPCWSTR pwszDomainNC)
GetSecurityDescriptorDacl(pSD, &bPresent, &pDacl, &bDefault) && bPresent && pDacl) { GetSecurityDescriptorDacl(pSD, &bPresent, &pDacl, &bDefault) && bPresent && pDacl) {
for (i = 0; i < pDacl->AceCount; i++) { for (i = 0; i < pDacl->AceCount; i++) {
ACE_HEADER *pAce = NULL; ACE_HEADER *pAce = 0;
ACCESS_ALLOWED_OBJECT_ACE *pObj; ACCESS_ALLOWED_OBJECT_ACE *pObj;
WCHAR wszGuid[64]; WCHAR wszGuid[64] = { 0 };
PSID pSid; PSID pSid = 0;
LPWSTR s = NULL; LPWSTR s = 0;
if (!GetAce(pDacl, i, (LPVOID *)&pAce) || !pAce) continue; if (!GetAce(pDacl, i, (LPVOID *)&pAce) || !pAce) continue;
if (pAce->AceType != ACCESS_ALLOWED_OBJECT_ACE_TYPE) continue; if (pAce->AceType != ACCESS_ALLOWED_OBJECT_ACE_TYPE) continue;
@@ -935,8 +935,8 @@ KestrelScanACLEdges(
KESTREL_ACL_SCAN_RESULT* pResult = 0; KESTREL_ACL_SCAN_RESULT* pResult = 0;
IDirectorySearch* pSearch = 0; IDirectorySearch* pSearch = 0;
ADS_SEARCH_HANDLE hSearch = 0; ADS_SEARCH_HANDLE hSearch = 0;
WCHAR wszPath[512]; WCHAR wszPath[512] = { 0 };
WCHAR wszConfigNC[512]; WCHAR wszConfigNC[512] = { 0 } ;
BOOL bUsePlanB = FALSE; BOOL bUsePlanB = FALSE;
KESTREL_ACL_BASELINE* pBaseline = 0; KESTREL_ACL_BASELINE* pBaseline = 0;
@@ -1001,7 +1001,7 @@ KestrelScanACLEdges(
prefs[2].dwSearchPref = ADS_SEARCHPREF_SECURITY_MASK; prefs[2].dwSearchPref = ADS_SEARCHPREF_SECURITY_MASK;
prefs[2].vValue.dwType = ADSTYPE_INTEGER; prefs[2].vValue.dwType = ADSTYPE_INTEGER;
prefs[2].vValue.Integer = 0x4; /* DACL_SECURITY_INFORMATION only — prefs[2].vValue.Integer = ADS_SECURITY_INFO_DACL; /* DACL_SECURITY_INFORMATION only —
readable by any authenticated user */ readable by any authenticated user */
hr = pSearch->lpVtbl->SetSearchPreference(pSearch, prefs, 3); hr = pSearch->lpVtbl->SetSearchPreference(pSearch, prefs, 3);
@@ -1076,7 +1076,7 @@ KestrelScanACLEdges(
} }
/* ── Obtain SECURITY_DESCRIPTOR ──────────────────────────────── */ /* ── Obtain SECURITY_DESCRIPTOR ──────────────────────────────── */
PSECURITY_DESCRIPTOR pSD = NULL; PSECURITY_DESCRIPTOR pSD = 0;
BOOL bOwnsSD = FALSE; /* TRUE = HeapAlloc, must HeapFree BOOL bOwnsSD = FALSE; /* TRUE = HeapAlloc, must HeapFree
FALSE = ADSI owns, FreeColumn */ FALSE = ADSI owns, FreeColumn */
@@ -1129,7 +1129,7 @@ KestrelScanACLEdges(
/* ── Walk DACL ────────────────────────────────────────────────── */ /* ── Walk DACL ────────────────────────────────────────────────── */
if (pSD && IsValidSecurityDescriptor(pSD)) { if (pSD && IsValidSecurityDescriptor(pSD)) {
PACL pDacl = NULL; PACL pDacl = 0;
BOOL bPresent = FALSE; BOOL bPresent = FALSE;
BOOL bDefault = FALSE; BOOL bDefault = FALSE;
@@ -1137,7 +1137,7 @@ KestrelScanACLEdges(
{ {
SECURITY_DESCRIPTOR_CONTROL sdCtrl = 0; SECURITY_DESCRIPTOR_CONTROL sdCtrl = 0;
DWORD dwSdRev = 0; DWORD dwSdRev = 0;
PSID pOwner = NULL; PSID pOwner = 0;
BOOL bOwnerDef = FALSE; BOOL bOwnerDef = FALSE;
if (GetSecurityDescriptorControl(pSD, &sdCtrl, &dwSdRev) && if (GetSecurityDescriptorControl(pSD, &sdCtrl, &dwSdRev) &&
@@ -1146,7 +1146,7 @@ KestrelScanACLEdges(
if (GetSecurityDescriptorOwner(pSD, &pOwner, &bOwnerDef) && if (GetSecurityDescriptorOwner(pSD, &pOwner, &bOwnerDef) &&
pOwner && !_IsAdminOwner(pOwner)) { pOwner && !_IsAdminOwner(pOwner)) {
LPWSTR so = NULL; LPWSTR so = 0;
if (ConvertSidToStringSidW(pOwner, &so) && so) { if (ConvertSidToStringSidW(pOwner, &so) && so) {
wprintf(L" [OWNER] %s — owned by non-admin %s\n", wszDN, so); wprintf(L" [OWNER] %s — owned by non-admin %s\n", wszDN, so);
LocalFree(so); LocalFree(so);
@@ -1457,8 +1457,8 @@ static BOOL _ReadDomainSid(_In_z_ LPCWSTR pwszDomainNC,
HRESULT hr; HRESULT hr;
IDirectorySearch *pSearch = 0; IDirectorySearch *pSearch = 0;
ADS_SEARCH_HANDLE hSearch = 0; ADS_SEARCH_HANDLE hSearch = 0;
WCHAR wszPath[512]; WCHAR wszPath[512] = { 0 } ;
ADS_SEARCHPREF_INFO prefs[1]; ADS_SEARCHPREF_INFO prefs[1] = { 0 };
LPWSTR attrs[] = { (LPWSTR)L"objectSid" }; LPWSTR attrs[] = { (LPWSTR)L"objectSid" };
BOOL bOk = FALSE; BOOL bOk = FALSE;
@@ -1511,7 +1511,7 @@ KestrelEmitRbcdFromSd(
_In_z_ LPCWSTR pwszLocalDomainSid, _In_z_ LPCWSTR pwszLocalDomainSid,
_Inout_ KESTREL_DELEG_SCAN_RESULT *pResult) _Inout_ KESTREL_DELEG_SCAN_RESULT *pResult)
{ {
PACL pDacl = NULL; PACL pDacl = 0;
BOOL bPresent = FALSE, bDefault = FALSE; BOOL bPresent = FALSE, bDefault = FALSE;
if (!IsValidSecurityDescriptor(pSD)) return S_OK; if (!IsValidSecurityDescriptor(pSD)) return S_OK;
@@ -1523,12 +1523,12 @@ KestrelEmitRbcdFromSd(
return S_OK; return S_OK;
for (DWORD i = 0; i < aclInfo.AceCount; i++) { for (DWORD i = 0; i < aclInfo.AceCount; i++) {
LPVOID pAce = NULL; LPVOID pAce = 0;
if (!GetAce(pDacl, i, &pAce)) continue; if (!GetAce(pDacl, i, &pAce)) continue;
DWORD dwMask = 0; DWORD dwMask = 0;
PSID pSid = NULL; PSID pSid = 0;
GUID *pObjType = NULL; GUID *pObjType = 0;
BOOL bDeny = FALSE; BOOL bDeny = FALSE;
if (!KestrelAceDecode(pAce, &dwMask, &pSid, &pObjType, &bDeny)) continue; if (!KestrelAceDecode(pAce, &dwMask, &pSid, &pObjType, &bDeny)) continue;
@@ -1576,10 +1576,10 @@ KestrelScanDelegation(
_Outptr_ KESTREL_DELEG_SCAN_RESULT **ppResult) _Outptr_ KESTREL_DELEG_SCAN_RESULT **ppResult)
{ {
HRESULT hr = S_OK; HRESULT hr = S_OK;
IDirectorySearch *pSearch = NULL; IDirectorySearch *pSearch = 0;
ADS_SEARCH_HANDLE hSearch = NULL; ADS_SEARCH_HANDLE hSearch = 0;
KESTREL_DELEG_SCAN_RESULT *pResult = NULL; KESTREL_DELEG_SCAN_RESULT *pResult = 0;
WCHAR wszPath[512]; WCHAR wszPath[512] = { 0 };
WCHAR wszDomainSid[64] = L""; WCHAR wszDomainSid[64] = L"";
if (!pwszDomainNC || !ppResult) return E_INVALIDARG; if (!pwszDomainNC || !ppResult) return E_INVALIDARG;
@@ -1781,7 +1781,7 @@ KestrelScanDelegation(
pResult->cObjectsScanned, pResult->cFindings, pResult->cObjectsErrored); pResult->cObjectsScanned, pResult->cFindings, pResult->cObjectsErrored);
*ppResult = pResult; *ppResult = pResult;
pResult = NULL; pResult = 0;
Cleanup: Cleanup:
if (hSearch && pSearch) if (hSearch && pSearch)
@@ -1857,10 +1857,10 @@ KestrelResolveSchemaGuid(
_Out_ BOOL *pbFound) _Out_ BOOL *pbFound)
{ {
HRESULT hr = S_OK; HRESULT hr = S_OK;
IDirectorySearch *pSearch = NULL; IDirectorySearch *pSearch = 0;
ADS_SEARCH_HANDLE hSearch = NULL; ADS_SEARCH_HANDLE hSearch = 0;
WCHAR wszPath[600]; WCHAR wszPath[600] = { 0 };
WCHAR wszFilter[256]; WCHAR wszFilter[256] = { 0 } ;
LPWSTR attrs[] = { L"schemaIDGUID" }; LPWSTR attrs[] = { L"schemaIDGUID" };
ZeroMemory(pGuid, sizeof(*pGuid)); ZeroMemory(pGuid, sizeof(*pGuid));
@@ -1943,7 +1943,7 @@ KestrelEmitLapsReadersFromSd(
_In_ DWORD cAttrs, _In_ DWORD cAttrs,
_Inout_ KESTREL_LAPS_SCAN_RESULT *pResult) _Inout_ KESTREL_LAPS_SCAN_RESULT *pResult)
{ {
PACL pDacl = NULL; PACL pDacl = 0;
BOOL bPresent = FALSE, bDefault = FALSE; BOOL bPresent = FALSE, bDefault = FALSE;
if (!IsValidSecurityDescriptor(pSD)) return S_OK; if (!IsValidSecurityDescriptor(pSD)) return S_OK;
@@ -1955,12 +1955,12 @@ KestrelEmitLapsReadersFromSd(
return S_OK; return S_OK;
for (DWORD i = 0; i < aclInfo.AceCount; i++) { for (DWORD i = 0; i < aclInfo.AceCount; i++) {
LPVOID pAce = NULL; LPVOID pAce = 0;
if (!GetAce(pDacl, i, &pAce)) continue; if (!GetAce(pDacl, i, &pAce)) continue;
DWORD dwMask = 0; DWORD dwMask = 0;
PSID pSid = NULL; PSID pSid = 0;
GUID *pObjType = NULL; GUID *pObjType = 0;
BOOL bDeny = FALSE; BOOL bDeny = FALSE;
if (!KestrelAceDecode(pAce, &dwMask, &pSid, &pObjType, &bDeny)) continue; if (!KestrelAceDecode(pAce, &dwMask, &pSid, &pObjType, &bDeny)) continue;
@@ -1997,7 +1997,7 @@ KestrelEmitLapsReadersFromSd(
if (!bGrants) continue; if (!bGrants) continue;
LPWSTR pwszSidStr = NULL; LPWSTR pwszSidStr = 0;
if (!ConvertSidToStringSidW(pSid, &pwszSidStr) || !pwszSidStr) if (!ConvertSidToStringSidW(pSid, &pwszSidStr) || !pwszSidStr)
continue; continue;
@@ -2028,12 +2028,12 @@ KestrelScanLapsReaders(
_Outptr_ KESTREL_LAPS_SCAN_RESULT **ppResult) _Outptr_ KESTREL_LAPS_SCAN_RESULT **ppResult)
{ {
HRESULT hr = S_OK; HRESULT hr = S_OK;
IDirectorySearch *pSearch = NULL; IDirectorySearch *pSearch = 0;
ADS_SEARCH_HANDLE hSearch = NULL; ADS_SEARCH_HANDLE hSearch = 0;
KESTREL_LAPS_SCAN_RESULT *pResult = NULL; KESTREL_LAPS_SCAN_RESULT *pResult = 0;
WCHAR wszConfigNC[512]; WCHAR wszConfigNC[512] = { 0 };
WCHAR wszSchemaNC[600]; WCHAR wszSchemaNC[600] = { 0 };
WCHAR wszPath[512]; WCHAR wszPath[512] = { 0 };
KESTREL_LAPS_ATTR rgAttrs[KESTREL_LAPS_MAX_ATTRS] = { 0 }; KESTREL_LAPS_ATTR rgAttrs[KESTREL_LAPS_MAX_ATTRS] = { 0 };
DWORD cAttrs = 0; DWORD cAttrs = 0;
@@ -2092,7 +2092,7 @@ KestrelScanLapsReaders(
if (cAttrs == 0) { if (cAttrs == 0) {
wprintf(L" [*] No LAPS attributes present in schema — nothing to enumerate.\n"); wprintf(L" [*] No LAPS attributes present in schema — nothing to enumerate.\n");
*ppResult = pResult; *ppResult = pResult;
pResult = NULL; pResult = 0;
goto Cleanup; /* S_OK with an empty, informative result */ goto Cleanup; /* S_OK with an empty, informative result */
} }
@@ -2106,7 +2106,7 @@ KestrelScanLapsReaders(
goto Cleanup; goto Cleanup;
} }
ADS_SEARCHPREF_INFO prefs[3]; ADS_SEARCHPREF_INFO prefs[3] = { 0 };
prefs[0].dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE; prefs[0].dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE;
prefs[0].vValue.dwType = ADSTYPE_INTEGER; prefs[0].vValue.dwType = ADSTYPE_INTEGER;
prefs[0].vValue.Integer = ADS_SCOPE_SUBTREE; prefs[0].vValue.Integer = ADS_SCOPE_SUBTREE;
@@ -2115,7 +2115,7 @@ KestrelScanLapsReaders(
prefs[1].vValue.Integer = KESTREL_LDAP_PAGESIZE; prefs[1].vValue.Integer = KESTREL_LDAP_PAGESIZE;
prefs[2].dwSearchPref = ADS_SEARCHPREF_SECURITY_MASK; prefs[2].dwSearchPref = ADS_SEARCHPREF_SECURITY_MASK;
prefs[2].vValue.dwType = ADSTYPE_INTEGER; prefs[2].vValue.dwType = ADSTYPE_INTEGER;
prefs[2].vValue.Integer = 0x4; /* DACL_SECURITY_INFORMATION — domain-user readable */ prefs[2].vValue.Integer = ADS_SECURITY_INFO_DACL; /* DACL_SECURITY_INFORMATION — domain-user readable */
hr = pSearch->lpVtbl->SetSearchPreference(pSearch, prefs, 3); hr = pSearch->lpVtbl->SetSearchPreference(pSearch, prefs, 3);
if (FAILED(hr)) goto Cleanup; if (FAILED(hr)) goto Cleanup;
+5 -2
View File
@@ -148,8 +148,11 @@ KestrelRunADFSDkmScan(_In_z_ LPCWSTR pwszDomainNC)
prefs[2].vValue.Integer = KESTREL_LDAP_PAGESIZE; prefs[2].vValue.Integer = KESTREL_LDAP_PAGESIZE;
pS->lpVtbl->SetSearchPreference(pS, prefs, ARRAYSIZE(prefs)); pS->lpVtbl->SetSearchPreference(pS, prefs, ARRAYSIZE(prefs));
/* The DKM master key lives in thumbnailPhoto on the key object(s). */ /* The DKM master key lives in thumbnailPhoto on the AD FS contact object.
hr = pS->lpVtbl->ExecuteSearch(pS, (LPWSTR)L"(thumbnailPhoto=*)", Leading with the indexed objectClass=contact clause lets the DC filter on
the index first, so the non-indexed thumbnailPhoto presence test runs on a
handful of objects instead of the whole subtree (avoids event 1644). */
hr = pS->lpVtbl->ExecuteSearch(pS, (LPWSTR)L"(&(objectClass=contact)(thumbnailPhoto=*))",
rgAttrs, ARRAYSIZE(rgAttrs), &h); rgAttrs, ARRAYSIZE(rgAttrs), &h);
if (FAILED(hr)) { if (FAILED(hr)) {
wprintf(L" [=] Container present but not searchable as this user (skipping)\n"); wprintf(L" [=] Container present but not searchable as this user (skipping)\n");