Skip to content
Free lab

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.

0sPro

Review queue

Read each LDAP / AD snapshot and call it: OK, or pick the fix that names its defect.

#1

Staff directory ACL

config · slapd.conf
database mdb
suffix "dc=corp,dc=example"
access to *
by anonymous read
by * read
# disallow bind_anon -> NOT set

context: OpenLDAP directory fronting staff login and the company address book.

Call

#2

Login endpoint

code · node
// POST /login
const username = req.body.username; // raw form input
const 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.

Call

#3

AD connection settings

config · .env
LDAP_URL=ldap://dc01.corp.example:389
LDAP_START_TLS=false
LDAP_BIND_DN=cn=svc-ldap,ou=service,dc=corp,dc=example
LDAP_BIND_PW=•••••••• # simple bind, every connect

context: Web app connecting to Active Directory to authenticate users.

Call

#4

Domain controller LDAP policy

config · registry
; 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.

Call

#5

Portal bind account

config · LDIF
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.

Call

#6

SPA login check

code · browser
// 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.

Call

#7

Nightly directory export

code · node
// report needs each active user display name + email
const 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.

Call

#8

Reviewed login integration

config · .env
LDAP_URL=ldaps://dc01.corp.example:636
LDAP_BIND_DN=cn=svc-login-ro,ou=service,dc=corp,dc=example # member of svc-ldap-readers
LDAP_SEARCH_BASE=ou=people,dc=corp,dc=example
LDAP_SEARCH_ATTRS=uid,mail,memberOf
LDAP_FILTER=(uid={{escaped}})
# DC policy: LDAPServerIntegrity=2, LdapEnforceChannelBinding=2

context: A login integration reviewed end-to-end before sign-off.

Call

#9

Login (filter-builder)

code · node
// 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.

Call

#10

User lookup

code · node
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.

Call

#11

People search

code · node
// search by department, then name
const dept = escapeFilterValue(req.query.dept);
const name = req.query.name; // not escaped
const filter = `(&(department=${dept})(cn=${name}*))`;
await client.search(baseDN, { scope: 'sub', filter });

context: People-search feature taking two user-supplied inputs.

Call

#12

Hardened DC (partial)

config · registry
; ...\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.

Call

#13

HR-sync account

config · LDIF
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.

Call

#14

Login bind account

config · LDIF
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.

Call

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 active
adversary kill-chain1/4
dir-eng-okafor14%
ad-admin-lindqvist11%
iam-analyst-reyes2%
dir-eng-you (you)0%

adversary staging…

TL;DR

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.

🎉 New here? Use code WELCOME10 for 10% off any plan at checkout
Directory Security Lab — Free Interactive LDAP / AD Trainer · PlayCISO