Impacket Deep Dive Part 2: Domain Recon Without a Single Exploit
Picking up from Part 1: CONTOSO.LOCAL, one DC, one member server, one workstation, one phished user — CONTOSO\j.reyes, local admin on WKS01 and nothing else. Before touching Kerberos, relaying anything, or popping a shell, the first thing that happens on a real engagement is recon. Specifically, recon against protocols that predate the idea of “zero trust” by about two decades and are, by default, remarkably chatty.
None of the four tools in this post crack a password, exploit a vulnerability, or require anything beyond a network path to the DC. Two of them barely need credentials at all.
RPC endpoint mapping: rpcdump.py
Every Windows RPC service registers itself with the endpoint mapper (epmapper) on TCP/135. Query it and it tells you, unauthenticated, exactly which RPC interfaces are alive on the box and what UUID identifies each one:
$ rpcdump.py 10.10.30.10
Impacket v0.13.1 - Copyright Fortra, LLC and its affiliated companies
[*] Retrieving endpoint list from 10.10.30.10
Protocol: [MS-DRSR]: Directory Replication Service (DRS) Remote Protocol
Provider: NTDSAI.DLL
UUID: E3514235-4B06-11D1-AB04-00C04FC2DCD2 v4.0
Bindings:
ncacn_ip_tcp:10.10.30.10[49664]
ncalrpc:[LRPC-...]
Protocol: N/A
Provider: SAMSRV.DLL
UUID: 12345778-1234-ABCD-EF00-0123456789AC v1.0
Bindings:
ncacn_np:\\DC01[\PIPE\samr]
ncacn_ip_tcp:10.10.30.10[49667]
...
rpcdump.py’s --help is intentionally sparse — target, a hash for auth if you have one, nothing else:
usage: rpcdump.py [-h] [-debug] [-ts] [-target-ip ip address]
[-port [destination port]] [-hashes LMHASH:NTHASH]
target
Dumps the remote RPC enpoints information via epmapper.
The interesting line above is E3514235-4B06-11D1-AB04-00C04FC2DCD2, the DRSUAPI interface — the same one secretsdump.py uses for DCSync in Part 7. Seeing it listed here, on a DC, unauthenticated, is the first confirmation that the replication interface is reachable at all. It always is on a DC; it’s supposed to be, since that’s how real domain controllers replicate with each other. The point of running rpcdump.py first isn’t to find something surprising — it’s to build the map before deciding where to spend authenticated effort.
SAM enumeration over SAMR: samrdump.py and lookupsid.py
SAMR (Security Account Manager Remote) is the RPC interface Windows itself uses to manage local and domain accounts. It’s reachable over the same named pipe SMB already opened, \PIPE\samr, and depending on domain-level RestrictAnonymous settings, it will answer to a null session — a connection authenticated as literally nobody.
samrdump.py walks it and dumps the account list:
$ samrdump.py CONTOSO.LOCAL/j.reyes:'Summer2026!'@10.10.30.10
Impacket v0.13.1 - Copyright Fortra, LLC and its affiliated companies
[*] Retrieving endpoint list from 10.10.30.10
[*] Trying protocol 445/SMB...
[*] Found domain(s):
[*] . CONTOSO
[*] . Builtin
[*] Looking up users in domain CONTOSO
[*] Found user: Administrator, uid 500
[*] Found user: Guest, uid 501
[*] Found user: krbtgt, uid 502
[*] Found user: svc-backup, uid 1104
[*] Found user: svc-legacy-scan, uid 1108
[*] Found user: j.reyes, uid 1121
[*] Found user: a.oyelaran, uid 1122
...
Every account, its RID, and — depending on domain policy — a fair bit more. -csv flattens the same output for feeding into other tooling:
usage: samrdump.py [-h] [-csv] [-ts] [-debug] [-dc-ip ip address]
[-target-ip ip address] [-port [destination port]]
[-hashes LMHASH:NTHASH] [-no-pass] [-k] [-aesKey hex key]
target
This script downloads the list of users for the target system.
lookupsid.py comes at the same interface from a different angle: rather than walking the account list directly, it takes the domain’s SID and brute-forces the RID space, translating each SID-RID pair back into a name via LsaLookupSids:
$ lookupsid.py CONTOSO.LOCAL/j.reyes:'Summer2026!'@10.10.30.10 5000
Impacket v0.13.1 - Copyright Fortra, LLC and its affiliated companies
[*] Brute forcing SIDs at 10.10.30.10
[*] StringBinding ncacn_np:10.10.30.10[\pipe\lsarpc]
[*] Domain SID is: S-1-5-21-1957667318-3765306688-1889875232
500: CONTOSO\Administrator (SidTypeUser)
501: CONTOSO\Guest (SidTypeUser)
502: CONTOSO\krbtgt (SidTypeUser)
512: CONTOSO\Domain Admins (SidTypeGroup)
513: CONTOSO\Domain Users (SidTypeGroup)
1104: CONTOSO\svc-backup (SidTypeUser)
1108: CONTOSO\svc-legacy-scan (SidTypeUser)
...
That 512: CONTOSO\Domain Admins line matters more than it looks. Once you have the name of the Domain Admins group, net group "Domain Admins" /domain (or the LDAP equivalent in GetADUsers.py below) tells you exactly who to target for Kerberoasting, phishing, or credential-stuffing — the actual humans or service accounts sitting in that group. lookupsid.py’s -domain-sids flag goes one step further and enumerates trusted-domain SIDs too, useful the moment CONTOSO.LOCAL isn’t standing alone:
usage: lookupsid.py [-h] [-debug] [-ts] [-target-ip ip address]
[-port [destination port]] [-domain-sids]
[-hashes LMHASH:NTHASH] [-no-pass] [-k]
target [maxRid]
The maxRid positional argument is the brute-force ceiling — 4000 by default, bumped to 5000 above since CONTOSO.LOCAL’s RIDs run a little past the default in this lab. On a domain with tens of thousands of objects, raising that further (and expecting the run to take a while) is standard practice.
LDAP enumeration: GetADUsers.py
Where SAMR gives you names and RIDs, LDAP gives you the rest of the object: pwdLastSet, userAccountControl flags (which is where “account disabled” and “password never expires” live), email addresses, and — critically for Parts 3 and 4 — which accounts have servicePrincipalName set or have Kerberos pre-authentication turned off.
$ GetADUsers.py CONTOSO.LOCAL/j.reyes:'Summer2026!' -all -dc-ip 10.10.30.10
Impacket v0.13.1 - Copyright Fortra, LLC and its affiliated companies
Name Email PasswordLastSet LastLogon UAC
-------------------- -------------------------- ---------------- -------------------- -------
Administrator 2024-01-08 09:14 2026-07-10 07:41 66048
krbtgt 2024-01-08 09:12 <never> 66082
svc-backup 2024-01-08 10:03 2025-11-02 03:00 590336
svc-legacy-scan 2019-06-14 15:22 2026-02-11 22:10 4260352
j.reyes j.reyes@contoso.local 2026-04-02 11:00 2026-07-13 08:55 66048
a.oyelaran a.oyelaran@contoso.local 2026-01-19 09:30 2026-07-13 09:02 66048
Two rows are worth stopping on before Parts 3 and 4 even start. svc-legacy-scan’s PasswordLastSet is 2019 — six years stale in this lab’s timeline — a strong signal of an abandoned service account nobody’s rotated since it was created, and its UAC value of 4260352 includes the DONT_REQ_PREAUTH bit, which is exactly what makes AS-REP roasting possible in Part 3. svc-backup’s UAC (590336) doesn’t have that bit, but it does have an SPN registered — the prerequisite for Kerberoasting in Part 4.
The full --help shows how narrow the tool’s job is — a target identity and one switch, -all, to widen the query:
usage: GetADUsers.py [-h] [-user username] [-all] [-ts] [-debug]
[-hashes LMHASH:NTHASH] [-no-pass] [-k] [-aesKey hex key]
[-dc-ip ip address] [-dc-host hostname]
target
Queries target domain for users data
Without -all, it returns only enabled accounts with a mail attribute set — a deliberately conservative default that misses exactly the kind of forgotten service account that matters most, which is why every worked example above passes -all explicitly.
Why this stage even needs credentials in this lab
Everything above ran as CONTOSO\j.reyes — a standard, unprivileged domain user — because that’s realistic for CONTOSO.LOCAL specifically: RestrictAnonymous is set to 1 (no anonymous SAM enumeration) on DC01, which most estates get right today. What it doesn’t stop is any authenticated domain user reading almost all of this by design — LDAP being world-readable to authenticated users is a deliberate Active Directory feature, not a misconfiguration. The moment j.reyes was phished and authenticated once, the entire domain’s account list, RID space, and RPC surface opened up. That’s the actual lesson of this stage: the meaningful security boundary in most domains isn’t “authenticated vs. not,” it’s “how much a single compromised low-privilege account can see” — and today, the answer is usually “nearly everything.”
Defensive notes
Null session hardening (RestrictAnonymous). Confirm HKLM\SYSTEM\CurrentControlSet\Control\Lsa\RestrictAnonymous is 1 or 2 on every DC and member server — 2 is stricter and blocks anonymous SAMR enumeration entirely, though it can break legacy trust relationships and some monitoring tooling, so test before a blanket rollout. This closes the unauthenticated half of samrdump.py and lookupsid.py, but not the authenticated half described above.
LDAP query auditing. Enable Directory Service Access auditing (4662) for the domain root object with a SACL targeting broad reads, or use a dedicated tool like Group3r or Microsoft Defender for Identity, which specifically flags reconnaissance-shaped LDAP query patterns — a single client running large, unusual queries against userAccountControl or servicePrincipalName in a short window is the actual signature to hunt, not “any LDAP query,” which is far too common to alert on by itself.
RID cycling detection. lookupsid.py generates a very distinctive pattern: hundreds to thousands of sequential LsaLookupSids calls against the same DC in seconds. Splunk’s and Microsoft’s own Impacket detection content both flag this specifically — it’s one of the highest-confidence, lowest-false-positive signals in this entire series, because no legitimate admin tool brute-forces the RID space sequentially like this.
Least-privilege service accounts, checked now, used later. svc-legacy-scan’s six-year-old password and svc-backup’s SPN aren’t recon-stage problems on their own — they’re the setup for Parts 3 and 4. The fix belongs here anyway: an inventory of every service account’s pwdLastSet, whether pre-auth is required, and whether it actually needs a registered SPN, reviewed on a schedule rather than discovered by an attacker first.
Next: Part 3, where svc-legacy-scan’s DONT_REQ_PREAUTH flag — spotted in the GetADUsers.py output above — turns into a crackable Kerberos ticket with a single unauthenticated request.