Impacket on LDAP: GetNPUsers, GetUserSPNs, and What the Wire Looks Like

Impacket is still the Swiss army knife people reach for when they want AD pain without opening Visual Studio. In the lab I care less about the binary name and more about the LDAP questions GetNPUsers and GetUserSPNs ask — because those show up on 389 whether the operator used Impacket, a ripoff script, or something that ships next month with a different filename.

This sits next to the Rubeus post. Same attack ideas, different client, very similar directory filters.

Lab

  • Kali (I was on a 2024 image in my notes) + Impacket examples
  • Disposable domain / DetectionLab-class DC
  • Capture on the DC span or the DC vNIC while you run the tools
  • Wireshark filter to start: tcp.port == 389 and ldap
# AS-REP roast candidates (no preauth)
GetNPUsers.py lab.local/ -dc-ip 10.0.0.10 -usersfile users.txt -format hashcat

# or authenticated enum depending on what you’re doing
GetNPUsers.py 'lab.local/user:password' -dc-ip 10.0.0.10 -request

# Kerberoast via SPN discovery
GetUserSPNs.py 'lab.local/user:password' -dc-ip 10.0.0.10 -request

I’m not going to re-teach the entire attack. If you need the narrative: users without Kerberos preauth, users with SPNs + weak passwords, offline cracking. You already know. Detection is about catching the shopping list.

What GetNPUsers is hunting in LDAP

Conceptually it’s the DONT_REQ_PREAUTH crowd — same UAC bit energy as Rubeus asreproast:

(&
  (samAccountType=805306368)
  (userAccountControl:1.2.840.113556.1.4.803:=4194304)
)

4194304 = don’t require preauth. If you still have users with that flag in 2024+, that’s a finding even before anyone runs Impacket.

On the wire, Impacket’s BER encoding of that search is what content-match rules try to fingerprint. Open-source reference for the client behavior: Impacket’s GetNPUsers.py on GitHub. That’s the right place to diff versions when your rule dies after an update.

What GetUserSPNs is hunting

Kerberoast targeting — users with SPNs, not disabled, etc.:

(&
  (objectCategory=person)
  (!(userAccountControl:1.2.840.113556.1.4.803:=2))
  (servicePrincipalName=*)
)

(Exact filter strings drift by tool/version — always confirm in your PCAP decode, don’t memorize my blog as gospel.)

Timeline you want in a labeled capture:

  1. LDAP search with SPN / UAC flavor
  2. Kerberos TGS-REQ burst for those SPNs (88/tcp)
  3. Nothing useful for the offline crack — that’s offline on purpose

IDS angle (lab methodology, not a free SIEM pack)

What worked for me in spirit: tcp-pkt / content matches on distinctive BER sequences toward $HOME_NET 389, scoped to DC addresses if you can.

Pattern for AS-REP enum:

  • LDAP search request
  • UAC bitwise OID + 4194304 style payload
  • objectCategory/computer or user shopping list depending on version

Pattern for SPN enum:

  • servicePrincipalName attribute ask near person/user filters
  • UAC disabled-bit exclude

Example shape only — replace content with bytes from your PCAP:

alert tcp any any -> $DC_SERVERS 389 (
  msg:"LAB LDAP ASREP-style UAC 4194304 search";
  flow:to_server,established;
  content:"|34 31 39 34 33 30 34|";  # ASCII 4194304 — verify in BER context
  content:"UserAccountControl"; nocase;
  threshold:type limit, track by_src, count 3, seconds 60;
  classtype:attempted-recon;
  sid:9100201;
  rev:1;
)

alert tcp any any -> $DC_SERVERS 389 (
  msg:"LAB LDAP SPN enum - servicePrincipalName + person";
  flow:to_server,established;
  content:"servicePrincipalName"; nocase;
  content:"objectCategory"; nocase;
  threshold:type limit, track by_src, count 3, seconds 60;
  classtype:attempted-recon;
  sid:9100202;
  rev:1;
)

Those will false positive if you leave them dumb. Identity sync tools, admin scripts, scanners — all love LDAP. Threshold + source allowlists + “not from DCs/identity hosts” is the job.

Also: LDAPS 636 blinds pure NIDS unless you terminate TLS somewhere you can see. Know your estate before you promise wire coverage.

Host / log side (pair it)

  • 4769 — TGS requests after GetUserSPNs -request; watch RC4 (0x17) and high cardinality of services from one user
  • 4768 / AS-REP paths — for the no-preauth crowd
  • Process telemetry on the operator host if you have it (python/impacket) — nice, not sufficient alone

Hunt sketch:

4769 by Account_Name
| uncommon # of distinct ServiceName in 10 minutes
| TicketEncryptionType in (0x17, 0x18)
| Account_Name not in service_account_list

Impacket vs Rubeus — detection sameness

Impacket Rubeus
Where it runs Often Linux / python Often Windows PE
LDAP filters Same ideas Same ideas
Wire story 389 BER 389 BER
Hashing PE Weak strategy Weak strategy

If your only detection is “Rubeus.exe hash,” Impacket will clown you. Detect the directory question.

Prevention that makes the tools boring

  • Find and clear DONT_REQ_PREAUTH users
  • No user SPNs with garbage passwords; gMSA where it fits
  • AES where you can survive it; stop treating RC4 as normal
  • Tier admin so a random workstation can’t LDAP-browse the crown jewels
Get-ADUser -Filter {DoesNotRequirePreAuth -eq $true} -Properties DoesNotRequirePreAuth

Test plan (steal it)

  1. Snapshot lab
  2. PCAP: GetNPUsers
  3. PCAP: GetUserSPNs -request
  4. Confirm LDAP decode + 4769/4768
  5. Write 1–2 rules from bytes, not from blog posts
  6. Replay after every sensor upgrade

Related: Rubeus post, BloodHound LDAP.

Leave a Reply

Your email address will not be published. Required fields are marked *