$npx -y skills add elementalsouls/Claude-BugHunter --skill hunt-ldapHunt LDAP Injection and XPath Injection — authentication bypass, blind char-by-char attribute exfiltration, AD user/group enumeration, XML-store XPath bypass. Covers the LDAP special-character set (* ( ) \\ NUL /), search-filter-context vs DN-injection, parenthesis-balancing, AND
| 1 | # HUNT-LDAP — LDAP Injection & XPath Injection |
| 2 | |
| 3 | > Grounding note: LDAP injection is rarely disclosed with verbatim payloads on |
| 4 | > public platforms (most live on internal-pentest reports). This skill is |
| 5 | > grounded in the **OWASP LDAP Injection Prevention / Testing Guide |
| 6 | > (WSTG-INPV-06)**, **PortSwigger Web Security Academy (LDAP injection)**, and |
| 7 | > the **RFC 4515** filter grammar — all publicly verifiable references rather |
| 8 | > than invented HackerOne IDs. Do not cite a report you cannot link. |
| 9 | |
| 10 | ## Crown Jewel Targets |
| 11 | |
| 12 | LDAP injection that bypasses authentication = **Critical**. Blind attribute |
| 13 | exfiltration of credentials/secrets = **High**. AD enumeration alone = Medium-High. |
| 14 | |
| 15 | **Highest-value chains:** |
| 16 | - **LDAP auth bypass** — close the `uid` filter and append an always-true OR so the |
| 17 | bind/search returns the admin entry without a valid password. |
| 18 | - **Blind attribute exfil** — char-by-char extraction of an attribute value via a |
| 19 | boolean oracle (login success/failure, result count, or response length). |
| 20 | - **userPassword hash exfil (non-AD only)** — on OpenLDAP/389-DS the |
| 21 | `userPassword` attribute can hold `{SSHA}`/`{CRYPT}` hashes that ARE readable |
| 22 | by query. See the AD-vs-generic warning below. |
| 23 | - **XPath injection auth bypass** — `' or '1'='1` against XML-backed auth. |
| 24 | |
| 25 | --- |
| 26 | |
| 27 | ## CRITICAL — Active Directory vs generic LDAP |
| 28 | |
| 29 | Do **not** conflate the two. They behave very differently: |
| 30 | |
| 31 | | | Generic LDAP (OpenLDAP, 389-DS, ApacheDS) | Active Directory | |
| 32 | |---|---|---| |
| 33 | | Password attribute | `userPassword` — may hold `{SSHA}`/`{MD5}`/`{CRYPT}` and **is readable** if ACL allows | `unicodePwd` — **write-only**, never returned by any search | |
| 34 | | Hash exfil via injection | **Possible** where ACLs leak `userPassword` | **Not possible** — there is no readable hash attribute over LDAP | |
| 35 | | Useful enum attrs | `uid`, `cn`, `mail`, `userPassword` | `sAMAccountName`, `userPrincipalName`, `mail`, `memberOf`, `description` (often holds plaintext secrets!) | |
| 36 | |
| 37 | **Do not tell a reader that blind LDAP injection yields AD password hashes — it |
| 38 | does not.** `unicodePwd` is write-only. Against AD, the win is enumeration |
| 39 | (`sAMAccountName`, `memberOf`, `description`/`info` fields that admins misuse to |
| 40 | store passwords) and auth bypass — not hash dumping. The hash-exfil technique |
| 41 | applies **only** to non-AD directories exposing `userPassword`. |
| 42 | |
| 43 | --- |
| 44 | |
| 45 | ## Attack Surface Signals |
| 46 | |
| 47 | ``` |
| 48 | Corporate SSO / intranet login pages (often legacy Java/Spring/PHP) |
| 49 | Windows + IIS + "integrated" directory auth |
| 50 | /api/ldap/* /api/directory/* /people /address-book /search?dir= |
| 51 | "Find a colleague" / org-chart / employee-search features |
| 52 | XML-backed config or auth → XPath injection candidate |
| 53 | Error strings that confirm an LDAP backend: |
| 54 | javax.naming.NameNotFoundException |
| 55 | javax.naming.directory.InvalidSearchFilterException |
| 56 | LDAP: error code 49 - 80090308 (AD invalid creds / bind failure) |
| 57 | com.sun.jndi.ldap.* / System.DirectoryServices / ldap_search(): |
| 58 | "Bad search filter" / net.ldap (Go) / python-ldap SERVER_DOWN |
| 59 | ``` |
| 60 | |
| 61 | --- |
| 62 | |
| 63 | ## LDAP filter grammar (RFC 4515) — why injection works |
| 64 | |
| 65 | A login filter is typically built by string-concat: |
| 66 | |
| 67 | ``` |
| 68 | (&(uid=<USERNAME>)(userPassword=<PASSWORD>)) |
| 69 | ``` |
| 70 | |
| 71 | `&` = AND, `|` = OR, `!` = NOT. **Filters are prefix/Polish notation** — the |
| 72 | operator comes first and every sub-filter is parenthesised. To inject you must |
| 73 | (a) escape the current `(uid=...)` group, (b) inject your own logic, and |
| 74 | (c) leave the overall parenthesis count **balanced** or the server throws a |
| 75 | filter-syntax error instead of executing. |
| 76 | |
| 77 | ### The special-character set — TEST EACH ONE |
| 78 | |
| 79 | These characters are syntactically meaningful and MUST be escaped by a safe app |
| 80 | (RFC 4515 §3). If the app reflects an error or behaves differently when you send |
| 81 | them raw, the input is unescaped → injectable: |
| 82 | |
| 83 | | Char | Filter escape | Why it matters | |
| 84 | |------|---------------|----------------| |
| 85 | | `*` | `\2a` | wildcard — matches any value | |
| 86 | | `(` | `\28` | opens a filter group | |
| 87 | | `)` | `\29` | closes a filter group | |
| 88 | | `\` | `\5c` | escape char itself | |
| 89 | | NUL | `\00` | string terminator — truncates filter in C-backed servers | |
| 90 | | `/` | (DN context) | RDN separator — relevant for DN injection | |
| 91 | |
| 92 | **Search-filter context vs DN injection** are different bugs: |
| 93 | - **Search-filter injection** (most common): your input lands inside a |
| 94 | `(attr=VALUE)` filter. Payloads use `* ( ) & | !`. |