Directory Security Lab
Most directory breaches start with one line of config or code that is quietly wrong. Work a queue of LDAP and Active Directory snapshots and call each one: hardened, or pick the fix — disable an anonymous bind, escape an injectable login filter, require LDAPS / StartTLS instead of cleartext 389, enforce signing + channel binding on the DC, least-privilege an over-powered bind account, move a password check server-side, or bound an unbounded read. You are scored on catch rate against false positives — because flagging a correct config (an escaped filter builder, StartTLS over 389, a scoped read-only account) is its own failure. Clear the queue clean.
The real job. More noise, a tighter clock.
Review queue
Read each LDAP / AD snapshot and call it: OK, or pick the fix that names its defect.
#1
Staff directory ACL
database mdbsuffix "dc=corp,dc=example"access to *by anonymous readby * read# disallow bind_anon -> NOT set
context: OpenLDAP directory fronting staff login and the company address book.
#2
Login endpoint
// POST /loginconst username = req.body.username; // raw form inputconst filter = '(uid=' + username + ')';const r = await client.search(baseDN, { scope: 'sub', filter });if (r.entries.length) await client.bind(r.entries[0].dn, req.body.password);
context: Staff login: find the user by uid, then bind as them to verify the password.
#3
AD connection settings
LDAP_URL=ldap://dc01.corp.example:389LDAP_START_TLS=falseLDAP_BIND_DN=cn=svc-ldap,ou=service,dc=corp,dc=exampleLDAP_BIND_PW=•••••••• # simple bind, every connect
context: Web app connecting to Active Directory to authenticate users.
#4
Domain controller LDAP policy
; HKLM\SYSTEM\CurrentControlSet\Services\NTDS\Parameters"LDAPServerIntegrity" = 1 ; 1 = none required; ...\Services\ldap"LdapEnforceChannelBinding" = 0 ; 0 = disabled
context: Domain controller accepting LDAP and LDAPS from domain-joined apps.
#5
Portal bind account
bindDN: cn=svc-portal,ou=service,dc=corp,dc=example# group membership:# CN=Domain Admins,CN=Users,DC=corp,DC=example# app operations: search (uid, mail, memberOf) under ou=people
context: Bind account used only to look up a few user attributes during login.
#6
SPA login check
// browser, after GET /api/user?uid=...const user = await res.json(); // { uid, mail, userPassword }if (sha1(passwordInput.value) === user.userPassword) {location.href = '/dashboard'; // 'authenticated'}
context: Single-page-app login: the API returns the user record and the browser checks the password.
#7
Nightly directory export
// report needs each active user display name + emailconst r = await client.search('dc=corp,dc=example', {scope: 'sub',filter: '(objectClass=*)',attributes: ['*'],sizeLimit: 0, // 0 = no limit});
context: Nightly job that only needs each active user’s display name and email.
#8
Reviewed login integration
LDAP_URL=ldaps://dc01.corp.example:636LDAP_BIND_DN=cn=svc-login-ro,ou=service,dc=corp,dc=example # member of svc-ldap-readersLDAP_SEARCH_BASE=ou=people,dc=corp,dc=exampleLDAP_SEARCH_ATTRS=uid,mail,memberOfLDAP_FILTER=(uid={{escaped}})# DC policy: LDAPServerIntegrity=2, LdapEnforceChannelBinding=2
context: A login integration reviewed end-to-end before sign-off.
#9
Login (filter-builder)
// POST /login, library filter objects (RFC 4515 escaping built in)const filter = new EqualityFilter({ attribute: 'uid', value: req.body.username });const r = await client.search(baseDN, { scope: 'sub', filter });if (r.entries.length === 1) await client.bind(r.entries[0].dn, req.body.password);
context: Login endpoint using the client library’s filter objects instead of string building.
#10
User lookup
const r = await client.search('ou=people,dc=corp,dc=example', {scope: 'one',filter: '(&(objectClass=inetOrgPerson)(uid=' + esc(uid) + '))',attributes: ['cn', 'mail'],sizeLimit: 2,});
context: Lookup of a single user’s name and email for a profile card.
#11
People search
// search by department, then nameconst dept = escapeFilterValue(req.query.dept);const name = req.query.name; // not escapedconst filter = `(&(department=${dept})(cn=${name}*))`;await client.search(baseDN, { scope: 'sub', filter });
context: People-search feature taking two user-supplied inputs.
#12
Hardened DC (partial)
; ...\Services\NTDS\Parameters"LDAPServerIntegrity" = 2 ; signing required; ...\Services\ldap"LdapEnforceChannelBinding" = 0 ; still disabled
context: A DC hardened for signing after an audit; channel binding was left untouched.
#13
HR-sync account
bindDN: cn=svc-hr-sync,ou=service,dc=corp,dc=example# delegated on ou=people:# Read all properties# Write all properties <--# Reset password <--# app operations: read uid/mail/title; write "title" nightly
context: HR-sync account that reads a few attributes and writes the "title" attribute nightly.
#14
Login bind account
bindDN: cn=svc-login-ro,ou=service,dc=corp,dc=example# group: CN=svc-ldap-readers (custom)# delegated: Read (uid, mail, memberOf) on ou=people only# app operations: search uid/mail/memberOf under ou=people
context: A login bind account reviewed against what the app actually does.
Catch rate vs. false positives
Review each snapshot, then grade. So far 0/14 called, 0/10 defects fixed, 0 false positive(s).
How it is scored
Catch rate = defective snapshots given the exact right fix.
A false positive (flagging a hardened config) takes an extra hygiene bite — a review that cries wolf is as useless as one that misses.
Harder tiers add subtler snapshots and deliberate traps (escaped filters, StartTLS on 389, scoped read-only accounts).
Live arena
4 activeadversary staging…
What this lab is
- A free, browser-based directory-security lab: work a queue of ~20 LDAP / Active Directory config and code snapshots and call each one OK or pick the fix that names its defect.
- Defects seeded across the queue: anonymous bind enabled, LDAP injection in a login filter, cleartext LDAP on 389, missing LDAP signing + channel binding (relay exposure), an over-privileged bind/service account, passwords compared client-side, and unbounded directory reads / excessive attribute exposure.
- You are graded on catch rate weighed against false positives — flagging a hardened config takes an extra hygiene penalty, because a review that cries wolf is as useless as one that waves a vulnerability through.
- Difficulty tiers add subtler snapshots and deliberate traps: an RFC 4515-escaped filter builder, StartTLS over port 389, a scoped read-only bind account, and the standard "by anonymous auth" rule on userPassword.
- It runs entirely in your browser — nothing is parsed, connected or sent anywhere — and after grading every snapshot shows why the right fix is right.
Frequently asked questions
What is the Directory Security Lab?
A free, in-browser exercise where you review LDAP and Active Directory snapshots — slapd ACLs, LDIF, .env connection settings, domain-controller registry policy, and login/search code — and give each one a verdict: OK, or the specific fix such as disable anonymous bind, parameterize/escape the filter, require LDAPS/StartTLS, enforce signing + channel binding, least-privilege the bind account, verify the password server-side, or bound reads and limit attributes. You are scored on how many real defects you catch versus how many correct configs you wrongly flag.
How is it graded?
Each snapshot has one correct verdict, and that verdict names the fix — picking the right fix is how you flag the right defect. Your catch rate is the share of defective snapshots you identify with the exact right fix. False positives — flagging a hardened config — carry an extra hygiene penalty, because a review a team cannot trust is as useless as one that misses defects. A perfect run calls every snapshot exactly right, correct ones included.
Does it connect to a directory?
No. Nothing is uploaded, no LDAP server is contacted, no LDIF is parsed and no code is executed. The snapshots are authored, realistic config and code excerpts bundled with the page, and every credential, DN and host is an obviously fake placeholder. The queue is fully static and deterministic. It runs entirely in your browser — no account, no install, nothing sent anywhere.
Who is it for?
Identity, platform and security engineers who run directory services, analysts reviewing LDAP integrations and AD hardening, and anyone prepping for a security or infrastructure interview who wants to feel why an anonymous bind, a concatenated login filter, a cleartext bind on 389, an unsigned domain controller or a Domain-Admin service account are findings — and why an escaped filter builder, StartTLS over 389 or a scoped read-only account is not.