Native, lockout-aware, multi-protocol credential validation & access-mapping
for authorized Active Directory penetration testing.
On engagements, the moment you get a working credential the next question is always the same: "Okay β where else does this work, and where am I admin?" The usual answer is to spray it with NetExec/CrackMapExec across the subnet.
That works, but two things always bothered me:
- Lockouts. Spraying is the fastest way to lock out a real employee's account, trip a SOC alert, and burn the engagement. Most tools count nothing and trust you to do the math in your head.
- Black-box shelling out. Wrapping a dozen external binaries and grepping their stdout is fragile, and it hides why an attempt was skipped or failed.
So I built advalidate: it validates one set of credentials against many hosts over five protocols using native Python libraries in-process β no shelling out β and gates every single attempt behind a lockout budget it computes from the domain's own password policy. It tells you where your creds work and where you're admin, and it refuses to push any account toward its lockout threshold.
It's a scoping tool, not an exploitation tool β see what it deliberately does not do.
- π§© Five protocols, one process β SMB/WMI, LDAP(S), WinRM, SSH and RDP, each a native Python library. No NetExec, no CrackMapExec, no
smbclientsubprocess. - π Three credential materials β
user:password,user:NTLMhash(pass-the-hash), and Kerberos ccache tickets. - π‘οΈ Lockout-safety engine on every attempt β reads the domain password policy + fine-grained PSOs, keeps a per-account budget, and stops before the threshold. Falls back to a loud SAFE MODE when policy can't be read.
- π§ Impossible-combo aware β a declarative capability map means it never fires an attempt that can't work (e.g. an NTLM hash over SSH). Those are skipped and logged β and a skip costs no lockout budget.
- π Access matrix + prioritized findings β a
richtable offail/auth/adminper host Γ protocol, plus a rule-based engine that surfaces what matters first (admin on many hosts, reaches the DC, admin not yet enumerated). All scoring is explainable β no AI/LLM calls. - π In-process SOCKS5 pivoting β
--socks HOST:PORTroutes every native handler through a proxy inside the process (proxychainscan't β here's why). - πΎ Persistent, cumulative state β a SQLite ledger keeps the budget honest across re-runs; add a credential later and the matrix/scoring build on everything learned so far.
advalidate is a defensive-boundary tool. It validates credentials and maps access β nothing more. It does not:
- β extract credentials (no SAM / LSASS / NTDS dumping)
- β exploit vulnerabilities or run exploitation modules
- β self-propagate or move laterally on its own
- β ingest into BloodHound
Those are out of scope by design. This tool tells you where your existing, authorized credentials already work.
This software is for authorized security testing only β a signed penetration test, red-team authorization, or a lab/CTF you own. Validating credentials against systems you don't own or aren't authorized to test is illegal in most jurisdictions, and account-lockout activity is disruptive enough to count as denial of service. You alone are responsible for staying within your scope and rules of engagement. The lockout-safety engine reduces β but does not eliminate β the risk of locking out accounts.
# 1. clone
git clone https://github.com/Muhammad-Tayab/advalidate.git
cd advalidate
# 2. isolated environment (recommended)
python3 -m venv .venv && source .venv/bin/activate
# 3. install dependencies + the `advalidate` command
pip install -r requirements.txt
pip install -e .Confirm it's on your PATH:
advalidate --version
advalidate capabilities # print the credential Γ protocol map and exitPrefer not to install? Run it straight from the repo β identical behaviour:
python -m advalidate capabilitiesKali / Debian notes (PEP 668, and aardwolf's Rust build)
On externally-managed Python (Kali/Debian) add --break-system-packages, or just use the venv above. On Kali the transport libraries are usually pre-installed; if aardwolf tries to build from source and fails needing setuptools_rust/a Rust toolchain, install the package alone against the already-present deps:
pip install -e . --no-build-isolation --no-deps --break-system-packages
advalidateis the installed command;python -m advalidateis the no-install equivalent.
# Validate one credential across a subnet, all protocols, lockout-safe
advalidate run --targets 10.10.0.0/24 --domain corp.local --cred 'jdoe:Summer2025!'
# Pass-the-hash (impossible combos are skipped automatically)
advalidate run --targets hosts.txt --domain corp.local \
--cred 'administrator:aad3b4...:31d6cfe0d16ae931b73c59d7e0c089c0'
# Kerberos ticket from a ccache
KRB5CCNAME=jdoe.ccache advalidate run -t dc01.corp.local -d corp.local -c 'jdoe:@'
# Route everything through an in-process SOCKS5 pivot
advalidate run -t 10.10.0.0/24 -d corp.local -c 'jdoe:Summer2025!' --socks 127.0.0.1:1080
# Preview what WOULD be attempted, without sending a packet
advalidate run -t 10.10.0.0/24 -d corp.local -c 'jdoe:Summer2025!' --dry-run
# Enforce rules of engagement: refuse anything outside the authorized scope
advalidate run ... --scope 10.10.0.0/24 --scope @scope.txt
# Pace attempts (fixed delay + random jitter) to reduce timing/detection risk
advalidate run ... --delay 1 --jitter 3
# Manual policy override + persistent state + JSON report
advalidate run ... --lockout-threshold 5 --observation-window 30 --state corp.db --json report.json| Flag | Purpose |
|---|---|
-t, --targets |
host / IP / CIDR / @file (repeatable) |
-d, --domain |
AD domain (FQDN) |
-c, --cred |
user:password | user:[LM:]NT | user:@[ccache] (repeatable) |
--scope |
authorized CIDR/host/@file; refuses everything outside it |
--socks HOST:PORT |
in-process SOCKS5 routing for every native handler |
--lockout-threshold / --observation-window |
supply domain policy manually |
--safety-margin N |
headroom kept below the threshold (default 1) |
--force |
override the budget entirely (requires interactive confirmation) |
--dry-run |
print the attempt plan; send nothing |
--delay / --jitter |
fixed + random pacing between attempts |
--state |
SQLite ledger + results file (enables cross-run safety) |
--json |
write access matrix + findings to a JSON file |
-v, -vv |
show every skip / budget HOLD / the moment SAFE MODE is left |
Run advalidate run -h for the full list.
Account lockout is the single biggest risk when spraying, and it's the reason this tool exists. The model works in layers:
-
Policy discovery. On the first valid credential,
advalidatereads the domain password policy over LDAP/SAMR βlockoutThresholdandlockoutObservationWindowβ and enumerates fine-grained password policies (PSOs) that can override the domain default per account or group, applying the most specific one to each target. -
Per-account budget. The budget is keyed on the account being authenticated, not the host. Every attempt across all protocols and all hosts decrements that one account's budget. Timestamps let entries age out of the observation window and free budget back up.
-
The stop rule. It never lets an account reach its threshold:
attempts_in_window β₯ threshold β safety_margin β HOLDWith the default
safety_margin = 1and a threshold of 5, it uses at most 4 attempts per account per window, then holds. -
SAFE MODE. If policy can't be read (pre-auth, or the read fails), it does not guess optimistically β it drops to conservative hardcoded defaults (
threshold 3 / 30 min / margin 1β 2 attempts per window) and prints a loud warning. -
Always visible, always overridable. It prints the active mode and per-account budget before spraying.
--lockout-threshold/--observation-windowsupply policy manually,--safety-marginchanges the headroom, and--forceoverrides the budget entirely β behind an explicit interactive confirmation, for accounts you know have no lockout policy (break-glass, lab targets).
Persistence. Lockout windows are long (often 30 min). Stop and re-run inside a window and an in-memory budget would forget prior attempts and could lock accounts β so the timestamped attempt ledger is persisted to SQLite (
--state) and reloaded. The budget is honored across invocations.
advalidate knows which auth material each protocol can actually consume, and skips impossible combinations instead of failing them (a failed attempt burns lockout budget β a skipped one doesn't). The map is declarative and printable via advalidate capabilities.
| Credential material | SMB / WMI | LDAP(S) | WinRM | SSH | RDP |
|---|---|---|---|---|---|
| Password | β | β | β | β | β |
| NTLM hash (PtH) | β | β | β | β | |
| Kerberos ticket | β | β | β |
- β = skipped and logged β never attempted (e.g. SSH can't consume an NTLM hash).
β οΈ RDP pass-the-hash works only when the target has Restricted Admin mode enabled; otherwise it's skipped.- ΒΉ SSH-over-Kerberos needs GSSAPI/GSS support on both ends; treated as environment-dependent.
TL;DR β for the native handlers, use --socks. proxychains does not and cannot proxy them.
proxychains works by LD_PRELOAD-ing a shim that hooks libc's connect() for a child process it launches. advalidate's handlers are in-process Python libraries β impacket, ldap3, paramiko and aardwolf all open sockets inside this very process. There's no child process to wrap, so proxychains python -m advalidate ... would route nothing from the handlers.
So advalidate does the proxying itself:
--socks HOST:PORT(primary) wires every handler to a SOCKS5 proxy in-process β paramiko via a pre-connected socket, impacket via a SOCKS-wrapped socket, ldap3 through a proxied socket source, aardwolf via its nativeasysockssupport.--proxychains(secondary) prependsproxychainsonly to genuine subprocess calls (if any) β it has no effect on the native handlers, and the tool says so explicitly when you pass it.
advalidate/ # repo root
βββ advalidate/ # the Python package
β βββ __main__.py # enables `python -m advalidate`
β βββ cli.py # click entry point (-> `advalidate` command)
β βββ handlers/ # one native handler per protocol
β β βββ smb.py # impacket β SMB + WMI (+ admin detection)
β β βββ ldap.py # ldap3 β LDAP/LDAPS, NTLM + simple bind
β β βββ winrm.py # pywinrm β NTLM
β β βββ ssh.py # paramiko
β β βββ rdp.py # aardwolf β NLA/CredSSP check (async, bridged)
β βββ core/
β β βββ credential.py # credential model (password / hash / ticket)
β β βββ capability.py # declarative cred Γ protocol legality map
β β βββ engine.py # orchestrator: gates + sequences every attempt
β β βββ targets.py # host/CIDR/file expansion + in-scope guard
β β βββ store.py # validated-result store (persistent SQLite)
β β βββ matrix.py # access matrix: fail | auth | admin
β β βββ scoring.py # rule-based prioritization / re-scoring engine
β βββ safety/
β β βββ policy.py # domain policy + PSO reader (LDAP/SAMR)
β β βββ budget.py # per-account lockout budget + SAFE MODE + ledger
β βββ proxy/socks.py # in-process SOCKS5 wiring for each library
βββ tests/ # pytest suite (safety/scoring/CLI logic, no network)
βββ .github/workflows/ci.yml
βββ pyproject.toml # packaging + `advalidate` console script
βββ requirements.txt
βββ LICENSE
The entry point is advalidate.cli:main, exposed as both the advalidate console command and python -m advalidate.
A rich access matrix β one row per host, one column per protocol β followed by a prioritized findings block from the rule-based scoring engine:
Access Matrix β corp.local
βββββββββββββββββ³ββββββββ³βββββββ³ββββββββ³ββββββ³ββββββ
β host β SMB β LDAP β WinRM β SSH β RDP β
β‘βββββββββββββββββββββββββββββββββββββββββββββββββββ©
β dc01 β admin β auth β admin β β β authβ
β fs01 β admin β auth β auth β β β β β
β web01 β auth β β β fail β authβ β β
βββββββββββββββββ΄ββββββββ΄βββββββ΄ββββββββ΄ββββββ΄ββββββ
Prioritized findings
1. [CRITICAL] jdoe is local admin on 2 hosts (dc01, fs01)
2. [CRITICAL] jdoe authenticates to the Domain Controller (dc01)
3. [INFO] admin on fs01 not yet enumerated
Add --json report.json for a machine-readable matrix + findings you can drop into a report.
The suite covers the safety, scoring, capability and CLI logic β no network, no live targets:
pip install pytest
pytest -qCI runs the same suite on Python 3.10 / 3.11 / 3.12 on every push (see .github/workflows/ci.yml).
| Package | Purpose |
|---|---|
impacket |
SMB + WMI, SAMR/LDAP policy reads |
ldap3 |
LDAP / LDAPS (NTLM + simple bind) |
pywinrm |
WinRM (NTLM) |
paramiko |
SSH |
aardwolf |
RDP NLA / CredSSP credential check |
python-socks, PySocks, asysocks |
in-process SOCKS5 routing |
click |
CLI |
rich |
access-matrix tables + colored logging |
All pinned in requirements.txt. Not required: NetExec / CrackMapExec / evil-winrm / smbclient β every protocol is implemented natively.
Already implemented beyond the base spec: persistent cross-run attempt ledger (SQLite), in-scope CIDR scope-guard (--scope), per-attempt jitter/throttle, and --dry-run plan mode.
Planned: config-file support, richer group-aware PSO resolution, structured NDJSON streaming.
Permanently out of scope (by design): credential extraction (SAM/LSASS/NTDS), exploitation modules, autonomous propagation/lateral movement, BloodHound ingestion.
Issues and pull requests are welcome. If you're adding a protocol handler, follow the handlers/base.py interface and keep it native and in-process β no shelling out to external toolkits. Please make sure pytest -q passes and add tests for any new safety/scoring logic.
MIT β see LICENSE. Provided for authorized security testing and education only; use is entirely at your own risk and responsibility.
Built by Muhammad Tayab β because a working credential is only useful if you know exactly where it works, and locking out the client on day one isn't the way to find out.