ssteelfactor-oss/Kestrel
GitHub: ssteelfactor-oss/Kestrel
Stars: 25 | Forks: 1
# Kestrel
## The problem with existing tooling
SharpHound, BloodHound's collector, runs as a .NET assembly. It generates LDAP
traffic in patterns that no legitimate domain workstation produces, and it does
so from a managed runtime that EDR hooks and inspects. EDR flags it not because
it exploits anything, but because the behavioral signature is unmistakable. Same
story with ADRecon, PowerView, and most Python-based alternatives: the runtime
is the fingerprint.
This is a real problem for defenders running internal audits. You need to
enumerate your own domain to find misconfigurations before an attacker does, but
the common tooling announces itself loudly — both in its runtime and in the
*shape* of the traffic it puts on the wire.
## A different approach
## How it works
IDirectorySearch *pSearch = NULL;
ADsGetObject(ldapPath, &IID_IDirectorySearch, (void **)&pSearch);
pSearch->lpVtbl->SetSearchPreference(pSearch, prefs, 2);
pSearch->lpVtbl->ExecuteSearch(pSearch, filter, attrs, count, &hSearch);
The one exception is **ADWS endpoint detection**, which performs a bare TCP
connect to port 9389 on each DC (connect + `SO_ERROR` check, no payload, no WCF
framing). It is the only behavior in Kestrel that is not a plain LDAP query — a
single, payload-free probe directed at the DC, never at a member host. It is
called out explicitly rather than folded into the "looks like normal LDAP" claim.
Groups are resolved by **Well-Known RID + Domain SID**, not by name. Kestrel works
correctly on domains installed in any language (English, Russian, German, etc.)
without hardcoded group-name strings.
## Scope and non-goals
Kestrel's philosophy is a hard boundary, not a preference. A capability belongs in
Kestrel only if **both** hold: (1) its traffic targets only the DC / directory
(or a SYSVOL read that is part of normal Group Policy processing), and (2) it
succeeds with the rights of an ordinary authenticated domain user.
By design, Kestrel therefore **does not**:
- send packets to member hosts (no host-touch enumeration of any kind);
- collect logged-on sessions, query SAMR/remote registry, or scan SMB shares;
- require local-administrator rights on any target;
- write to the directory — every operation is read-only;
- attempt to evade or disguise itself. It produces a *low and familiar* footprint,
which is not the same as *no* footprint.
Anything that needs to touch a member host or needs privileges above a normal
domain user is out of scope by definition and belongs to a separate, explicitly
labeled active tool — not to Kestrel.
## Requirements
- Windows, domain-joined machine
- Authenticated domain user account (no elevated privileges required for most scans)
- Visual Studio 2019+ with Windows SDK
- Linked libraries: `activeds.lib`, `adsiid.lib`, `ws2_32.lib`, `advapi32.lib`
## Build
Open `Kestrel.sln` in Visual Studio, select **Release | x64**, build.
For a self-contained binary with no runtime DLL dependencies:
Project Properties → C/C++ → Code Generation → Runtime Library → **Multi-threaded (/MT)**
## Modules
### v0.1 — Five passive scans (`adws_scan.c`)
All queries are read-only. Zero packets sent to member hosts (the ADWS probe
targets DCs only, as noted above).
| Module | What it does |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **ADWS Endpoint Detection** | Probes port 9389/TCP per DC. Raw TCP connect, SO\_ERROR verification, no WCF framing. The one non-LDAP behavior; payload-free; DC-directed only. |
| **Computer Topology** | Full computer inventory with SPN-based service inference. MSSQLSvc → SQL Server, WSMAN → WinRM, TERMSRV → RDP. One LDAP query covers the entire domain. |
| **Delegation Risks** | Separates three categories: unconstrained delegation (TGT forwarding), constrained delegation (msDS-AllowedToDelegateTo), and Protocol Transition / S4U2Self (UAC 0x1000000). Reported separately — different risk profiles. |
| **LAPS Coverage** | Detects legacy LAPS (ms-Mcs-AdmPwdExpirationTime) and Windows LAPS 2023+ (msLAPS-EncryptedPasswordHistory). Splits computer population into managed/unmanaged with percentage breakdown. |
| **Stale Computers** | Uses lastLogonTimestamp as primary reference — it replicates across DCs, unlike lastLogon which is per-DC only. Both values reported side by side. |
### v0.2 — ACL edge extraction (`KestrelACL.C`)
Extended Rights GUID→name mapping is built dynamically from
`CN=Extended-Rights,CN=Configuration` — no hardcoded GUID tables.
Classified edge types: `GenericAll`, `WriteDACL`, `WriteOwner`, `GenericWrite`,
`ExtendedRight`, `WriteProperty`, `CreateChild`, `DeleteChild`, `Self`.
Two read modes:
- **Plan A** — per-object `IDirectoryObject` bind (requires elevated rights in some environments)
- **Plan B** — reads `nTSecurityDescriptor` directly from the LDAP search column (works for any authenticated domain user)
Plan A is attempted first. On the first access denial, Kestrel switches to Plan B
automatically for all remaining objects, keeping the run within ordinary-user rights.
### v0.3 — Transitive group membership (`KestrelGroup.c`)
Expands high-value groups using `LDAP_MATCHING_RULE_IN_CHAIN` (OID
`1.2.840.113556.1.4.1941`). One LDAP query per group — the DC performs full
recursive traversal server-side, with no client-side BFS.
Groups are located by **RID**, not by name:
| RID | Group |
| ------- | ------------------------------------- |
| 512 | Domain Admins |
| 518 | Schema Admins |
| 519 | Enterprise Admins |
| 520 | Group Policy Creator Owners |
| 521 | Read-only Domain Controllers |
| 526 | Key Admins |
| 527 | Enterprise Key Admins |
| 548–551 | Account/Server/Print/Backup Operators |
After expansion, group membership is cross-referenced against the ACL edges from
v0.2 to surface attack paths: `member → [via group] → EdgeType → target`.
## Roadmap
| Version | Status | Description |
| ------- | ------ | -------------------------------------------------------------------------------------------------------- |
| v0.1 | ✅ | Five passive AD scans |
| v0.2 | ✅ | ACL edge extraction via IDirectoryObject / nTSecurityDescriptor |
| v0.3 | ✅ | Transitive group membership via LDAP\_MATCHING\_RULE\_IN\_CHAIN |
| v0.4 | ✅ | In-memory graph from ACL + membership + delegation data. Structured export (JSON / YAML). |
| v0.5 | 🔲 | BFS shortest-path finder: any principal → Domain Admins. Pure in-memory traversal over v0.4 data — no additional queries, no host contact. |
Every roadmap item stays inside the boundary in [Scope and non-goals](#scope-and-non-goals).
Edge types that would require touching member hosts — logged-on sessions, local-admin
relationships via remote SAM — are deliberately excluded from Kestrel's graph.
## Code quality
**SAL 2.0 annotations** on every function signature, validated by PREfast
(`/analyze`) at compile time. `_Must_inspect_result_` on HRESULT-returning
functions, `_Outptr_` vs `_Out_` where semantics differ.
**Single rootDSE resolution** — `defaultNamingContext` and
`configurationNamingContext` are read once at startup and passed as parameters.
No redundant DC round-trips.
**No runtime dependencies** when built with `/MT` — a single executable, no
VCRUNTIME DLLs required on the machine it runs from.
## Related
## Author
[@ssteelfactor-oss](https://github.com/ssteelfactor-oss)
Security research and COM/Windows internals
标签:客户端加密