LDAP 实战与排错 — ldapsearch 抓包、AD 对接与错误码定位
LDAP 动手实战:Wireshark 过滤式、ldapsearch/ldapadd/ldappasswd 命令族、LDIF 编写、invalidCredentials/noSuchObject/sizeLimit 排错、与 SQL/Kerberos 对比 / TCP 389
Table of Contents
- 这篇你能学到
- 抓包观察
- 常用命令 / 配置
- 常见故障与排错
- 故障 1:ldap_bind: Invalid credentials (49)
- 故障 2:No such object (32) —— 搜不到任何东西
- 故障 3:搜索返回 0 条但 Base DN 没错
- 故障 4:Size limit exceeded (4) / Administrative limit exceeded (11)
- 故障 5:Confidentiality required (13) / Strong(er) auth required (8)
- 故障 6:TLS 证书验证失败
- 故障 7:Object class violation (65) —— 添加条目失败
- 故障 8:Not allowed on non-leaf (66) —— 删不掉
- 故障 9:安全 —— LDAP 注入
- 与其他协议对比
- 速查表 / 常见面试题
这篇你能学到
- 用 Wireshark / tshark 把 LDAP 交互看穿:Simple Bind 的明文密码长什么样、怎么从抓包里揪出暴力破解。
ldapsearch全套用法(从匿名探测 RootDSE 到分页拉全量)、LDIF 增删改、AD 专属查询(位掩码、递归查组)、Linux 用 sssd 接入 LDAP 认证。- 九类高频故障的定位套路:49 凭据错怎么细分、32 Base DN 错怎么找对、搜到 0 条怎么逐层放宽、结果被截断怎么分页、TLS 报错怎么查证书,外加 LDAP 注入防御、协议对比、速查表与面试题。
抓包观察
Wireshark 显示过滤式
下面这些是抓包排错的"筛子"。先记两条最有用的:ldap.resultCode != 0 一眼看到所有失败操作;ldap.simple 一旦能过滤出内容,就说明你的 LDAP 是明文裸奔的。
| 过滤式 | 用途 |
|---|---|
ldap | 所有 LDAP 报文(Wireshark 自动解 BER) |
tcp.port == 389 | 明文 LDAP + StartTLS |
tcp.port == 636 | LDAPS(内容已加密,只能看握手) |
tcp.port == 3268 || tcp.port == 3269 | AD 全局编录(Global Catalog) |
ldap.protocolOp == 0 | 只看 BindRequest |
ldap.protocolOp == 3 | 只看 SearchRequest |
ldap.resultCode != 0 | 只看失败的操作,排错第一过滤式 |
ldap.resultCode == 49 | 只看 invalidCredentials(暴力破解检测) |
ldap.simple | 明文密码字段! 抓到即说明未加密 |
ldap.bindRequest_element | Bind 请求详情 |
ldap.filter contains "uid" | 按过滤器内容筛 |
ldap.baseObject contains "dc=example" | 按 Base DN 筛 |
ldap.messageID == 2 | 追踪某一次操作的完整请求+响应 |
ldap.attributeDesc == "userPassword" | 谁在读密码属性 |
命令行抓包:
这四条命令覆盖了排错和审计的常见诉求:把包存下来、把明文口令提出来、统计谁在猛试密码、实时看应用到底发了什么查询。第四条在排查"应用说搜不到用户"时特别管用——直接看它发的 Base DN 和过滤器对不对。
| |
典型字段说明
一个 Simple Bind 在 Wireshark 中展开:
对应 SearchRequest:
| |
排错三看:
- 看
resultCode:SearchResultDone/BindResponse里的结果码直接指明问题(49=密码错、32=Base DN 错、50=权限不足)。 - 看
diagnosticMessage:服务器附带的文字说明,AD 会在这里给出子错误码(见下文故障 1)。 - 看有没有
ldap.simple字段:能看到 = 未加密 = 高危。生产环境应当只能看到 TLS 握手。
常用命令 / 配置
安装客户端工具
装的是 ldapsearch、ldapadd 这一族命令行客户端,服务端不需要它们。
ldapsearch —— 最核心的命令
这是 LDAP 世界里最重要的一条命令,读、探测、验证全靠它。其中第 ① 条是"到了陌生服务器先做的事":不用任何账号就能问出"你有哪些 Base DN、支不支持 StartTLS、支不支持分页"。后面几条依次是查 Schema、认证后搜人、生产可用的组合、只要 DN、分页拉全量、查组、查人所属组、用 Kerberos 免密登录、验证"我现在是谁"。
| |
Active Directory 常用查询
AD 换了一套属性名,所以查询写法也不一样。下面这组分别解决:按登录名找人、只列启用的账号、把嵌套组里的人全挖出来、揪出密码永不过期的高危账号、列出所有域控、以及不知道域控地址时用 DNS 自动发现。
| |
userAccountControl 常用位掩码速查(配合 :1.2.840.113556.1.4.803:= 位与匹配):
| 值 | 含义 |
|---|---|
| 2 (0x2) | 账号已禁用 |
| 16 (0x10) | 账号已锁定 |
| 512 (0x200) | 普通启用账号 |
| 65536 (0x10000) | 密码永不过期 |
| 8192 (0x2000) | 域控计算机账号 |
| 4194304 (0x400000) | 不要求 Kerberos 预认证(AS-REP Roasting 风险) |
写操作:LDIF 与 ldapadd / ldapmodify
LDIF(LDAP Data Interchange Format,RFC 2849)是 LDAP 的标准文本交换格式。
下面第一个文件是"新建一个用户"的完整模板:第一行是这个条目的完整路径,接着声明它属于哪几个类,然后才是具体属性(注意 mail 写了两行——这就是多值属性)。
| |
改属性的文件格式不一样:要写 changetype: modify,然后用 replace(整体替换)/ add(追加)/ delete(删除)三种动作,每个动作之间用单独一行的 - 分隔。
改名和"把人挪到另一个部门"用的是同一个操作:newrdn 是新的小名,newsuperior 是新的父节点——员工离职挪到 ou=leavers 就是这么做的。
写好 LDIF 后用下面的命令执行。注意改密码不要自己算哈希再写属性,用 ldappasswd 走标准扩展操作让服务器自己处理最稳妥;最后一条则是把整棵树导出成 LDIF 做备份。
| |
服务端:快速起一个测试 OpenLDAP
学 LDAP 最快的方式是自己起一个来练手。下面一条 docker 命令就能得到一台可读写的目录服务器,第二条验证它活了。
| |
Linux 主机接入 LDAP 认证(sssd)
这份配置的作用是:让这台 Linux 不再只认 /etc/passwd 里的本地账号,而是把"用户存不存在"“密码对不对"都交给 LDAP 服务器判断。配好之后,目录里的员工可以直接 ssh 登录这台机器。
| |
改完必须收紧文件权限(否则 sssd 拒绝启动)再重启,然后用下面三条验证:getent 看能不能查到人,id 看组映射对不对,sssctl 是官方诊断工具。
常见故障与排错
故障 1:ldap_bind: Invalid credentials (49)
先记住结论:49 不等于"密码打错了”。 它是个大杂烩——DN 写错、账号被禁、密码过期、空密码,全都报 49。在 AD 上一定要去看 diagnosticMessage 里的 data xxx 子码,那才是真正的原因。
现象:Bind 失败。但"凭据无效"这个提示很笼统,实际原因很多。
| 原因 | 排查 |
|---|---|
| 密码真错了 | 换个已知正确的账号试 |
| Bind DN 写错 | Bind 要用完整 DN(cn=admin,dc=example,dc=com),不是用户名 admin |
| DN 里有多余空格/逗号 | ldapwhoami 验证;注意 dc=example, dc=com 中间的空格在某些实现会出问题 |
| 账号被锁定/禁用/过期 | AD 看 userAccountControl;OpenLDAP 看 pwdAccountLockedTime |
| 未认证 Bind(密码为空) | 空密码在部分服务器会被当成匿名 Bind"成功",反而更危险 |
AD 的诊断消息带子错误码,非常有用:
| data 码 | 含义 |
|---|---|
525 | 用户不存在 |
52e | 用户名正确但密码错误 |
530 | 不允许在此时间登录 |
531 | 不允许在此工作站登录 |
532 | 密码已过期 |
533 | 账号已禁用 |
701 | 账号已过期 |
773 | 用户必须修改密码(首次登录) |
775 | 账号已锁定 |
用这个码就能区分"密码错"和"账号被禁",避免误判。
故障 2:No such object (32) —— 搜不到任何东西
先记住结论:九成是 Base DN 写错了,而不是数据没了。 别猜,直接匿名查一下 RootDSE 的 namingContexts,服务器会告诉你它到底管着哪几棵树。
| 原因 | 排查 |
|---|---|
| Base DN 错误(最常见) | 先查 RootDSE 拿正确的 namingContexts:ldapsearch -x -H ldap://host -s base -b "" namingContexts |
| Base DN 大小写/空格 | LDAP 的 DN 属性名大小写不敏感,但值的空格敏感 |
| 域名转 DN 转错 | example.com → dc=example,dc=com;corp.example.com → dc=corp,dc=example,dc=com |
| 条目在别的域/分区 | AD 多域森林要用全局编录 3268 端口跨域搜 |
故障 3:搜索返回 0 条但 Base DN 没错
先记住结论:这时候要怀疑的不是数据,而是你的三个参数——scope 挖得够不够深、objectClass 名字在这个实现里对不对、以及你这个账号有没有读的权限。
| 原因 | 排查 |
|---|---|
scope 用了 base 或 one | 改成 -s sub |
| 过滤器 objectClass 写错 | OpenLDAP 是 inetOrgPerson,AD 是 user;换成 (objectClass=*) 先看有没有东西 |
| ACL 限制:能 Bind 但读不到 | 用管理员账号对比测试;OpenLDAP 检查 olcAccess |
| 属性名大小写/拼写 | sAMAccountName 不是 samaccountname(虽然多数实现不敏感,但个别客户端敏感) |
| 中文/特殊字符编码 | LDAP 使用 UTF-8,确保终端 locale 是 UTF-8 |
万能排查法:逐层放宽
思路是从"最宽松"开始一步步收紧,哪一步开始搜不到,问题就在那一步引入的条件上。
| |
故障 4:Size limit exceeded (4) / Administrative limit exceeded (11)
先记住结论:这不是"查询失败",而是"结果被悄悄截断了"——最危险的地方在于前面那部分数据看起来是正常返回的。 如果你的同步脚本忽略了这个错误码,就会把上千个用户当成"已删除"处理。
结果集超过服务器上限(AD 默认 MaxPageSize=1000,OpenLDAP sizelimit 默认 500)。注意此时返回的结果是不完整的,直接用会导致数据丢失。
三种解法按推荐度排序:分页是标准做法;按 OU 分批是不改服务端时的变通;调服务端上限需要管理员权限且治标不治本。
| |
故障 5:Confidentiality required (13) / Strong(er) auth required (8)
先记住结论:服务器在说"你没加密,我不跟你谈"——这不是配置坏了,是安全策略生效了,正确做法是把连接加密,而不是想办法关掉这个限制。
服务器强制要求加密连接(AD 开启 LDAP Signing/Channel Binding 后是默认行为;改密码操作也必须加密)。
两条路二选一:在 389 上升级成 TLS,或者直接连 636。
故障 6:TLS 证书验证失败
先记住结论:绝大多数 TLS 报错只有三种根因——CA 没被信任、证书上的名字和你连的主机名对不上、或者证书本身过期了。 先把服务器证书打出来看一眼,比盲试快得多。
| 排查 | 命令 |
|---|---|
| 看服务器证书链 | openssl s_client -connect host:636 -showcerts </dev/null |
| StartTLS 方式看证书 | openssl s_client -connect host:389 -starttls ldap </dev/null |
| 证书 CN/SAN 与连接主机名不符 | 必须用证书里的名字连(不能用 IP 连一个只签了域名的证书) |
| CA 未信任 | 把 CA 加入 /etc/openldap/ldap.conf:TLS_CACERT /path/ca.crt |
| 临时跳过(仅调试) | LDAPTLS_REQCERT=never ldapsearch ... 或 ldap.conf 里 TLS_REQCERT allow |
故障 7:Object class violation (65) —— 添加条目失败
先记住结论:这是 Schema 在拦你——你声明的 objectClass 要求的必填属性没给全,或者你给条目安了两个互斥的结构类。 属于"数据格式不合法",跟权限、网络都没关系。
| 原因 | 排查 |
|---|---|
| 缺 MUST 属性 | 如 inetOrgPerson 必须有 cn 和 sn |
| 两个 STRUCTURAL 类冲突 | 一个条目只能有一条结构类继承链,不能同时是 inetOrgPerson 和 groupOfNames |
缺 top | 多数实现要求带上 objectClass: top |
| RDN 属性未在条目中出现 | DN 是 uid=zhangsan,... 就必须有 uid: zhangsan 属性(否则报 namingViolation(64)) |
故障 8:Not allowed on non-leaf (66) —— 删不掉
先记住结论:LDAP 没有"级联删除"这回事,只能从最深的叶子开始一层层往上删。 想删一整个 OU,必须先把里面的条目清空。
LDAP 的 DelRequest 只能删叶子节点。要删整个 OU 必须先递归删光子节点:
下面这条流水线的思路是:把子树里所有 DN 列出来,按逗号个数(也就是层级深度)从多到少排序,深的先删,最后才轮到 OU 本身。
故障 9:安全 —— LDAP 注入
先记住结论:只要你是用字符串拼接来构造过滤器的,你就有 LDAP 注入漏洞——修复方式只有一个,对用户输入做 RFC 4515 转义。
下面的代码对比了错误写法和正确写法:上半段演示攻击者如何把 (uid=X) 变成永真式,下半段是必须加上的转义函数。
| |
其它安全要点:
- 服务账号只给只读权限,且限制可读子树;
- 禁用匿名 Bind 与空密码 Bind(应用层必须显式拒绝空密码);
- 全面启用 StartTLS/LDAPS,AD 开启 LDAP Signing + Channel Binding;
- 监控
resultCode=49的高频来源,识别口令喷洒(Password Spraying); - 不要在 LDAP 里存业务敏感数据(目录数据往往对全公司可读)。
与其他协议对比
| 维度 | LDAP | 关系数据库(SQL) | Kerberos | SAML / OIDC | SCIM |
|---|---|---|---|---|---|
| 定位 | 目录查询 + 认证 | 通用数据存储 | 票据认证 | Web SSO 联邦 | 账号供给/同步 |
| 数据模型 | 树形 DIT,稀疏多值属性 | 关系表,行列固定 | 无(只管票据) | 断言/令牌 | JSON 资源 |
| 端口 | TCP 389 / 636 | 3306 / 5432 等 | 88(TCP/UDP) | HTTPS 443 | HTTPS 443 |
| 编码 | BER(ASN.1 二进制) | 各自私有协议 | ASN.1 | XML / JWT | JSON |
| 读写倾向 | 读多写少,为读优化 | 读写均衡 | — | — | 写为主 |
| 事务 | 无跨条目事务 | ACID | — | — | — |
| 认证方式 | Bind(Simple / SASL) | 用户名密码 | 票据(TGT/ST) | 令牌 | Bearer Token |
| 是否携带密码 | Simple Bind 传明文密码 | 传密码 | 从不传密码(挑战响应) | 不传(重定向到 IdP) | 不传 |
| 单点登录 | ❌ 每次都要 Bind | ❌ | ✅ 票据可复用 | ✅ | — |
| 典型场景 | 企业通讯录、统一账号库 | 业务数据 | Windows 域登录 | 云应用登录 | 云端账号同步 |
在 AD 域里三者如何配合:
速查表 / 常见面试题
端口 / 关键值速查
| 项 | 值 |
|---|---|
| LDAP 明文 / StartTLS | TCP 389 |
| LDAPS(隐式 TLS) | TCP 636 |
| AD 全局编录 | TCP 3268 / SSL 3269 |
| AD Kerberos | 88 |
| StartTLS 扩展 OID | 1.3.6.1.4.1.1466.20037 |
| 分页控制 OID | 1.2.840.113556.1.4.319 |
| AD 递归查组 OID | 1.2.840.113556.1.4.1941 |
| AD 位与匹配 OID | 1.2.840.113556.1.4.803 |
| 协议版本号 | LDAPv3 = 3 |
| AD 默认返回上限 | 1000 条 |
| 特殊属性列表 | * 全部用户属性 / + 全部操作属性 / 1.1 只要 DN |
过滤器速查
| |
结果码速查
| 码 | 名称 | 一句话 |
|---|---|---|
| 0 | success | 成功 |
| 4 | sizeLimitExceeded | 结果太多 → 分页 |
| 10 | referral | 去别的服务器找 |
| 11 | adminLimitExceeded | AD 超 1000 条 |
| 13 | confidentialityRequired | 必须加密 |
| 32 | noSuchObject | Base DN 错 |
| 34 | invalidDNSyntax | DN 格式错 |
| 49 | invalidCredentials | 账号或密码错 |
| 50 | insufficientAccessRights | 权限不足 |
| 53 | unwillingToPerform | 服务器拒绝 |
| 65 | objectClassViolation | 缺 MUST 属性 |
| 66 | notAllowedOnNonLeaf | 删不了非叶子 |
| 68 | entryAlreadyExists | DN 已存在 |
常见面试题
Q1:DN 和 RDN 的区别?
A:RDN 是条目在父节点下的相对名(uid=zhangsan),只要求在兄弟节点间唯一;DN 是从条目到树根拼接的完整路径(uid=zhangsan,ou=people,dc=example,dc=com),全局唯一,相当于主键。书写顺序是从叶到根,与文件路径相反。
Q2:LDAP 的 scope 有哪三种?各查什么?
A:base 只查 Base DN 这一个条目;one 查它的直接子节点(不含自己、不含孙节点);sub 查它及所有后代。应用集成一般用 sub;验证某 DN 是否存在用 base。
Q3:LDAP 和关系数据库有什么本质区别?为什么不用 MySQL 存用户? A:① 数据模型:LDAP 是树形 + 稀疏多值属性,天然贴合组织架构;SQL 是固定行列,存稀疏属性会退化成大宽表或 EAV。② 读写倾向:LDAP 为读优化(倒排索引),写和复制代价高;SQL 读写均衡。③ 标准化协议:LDAP 有统一的协议和 Schema,几百种应用开箱即支持"对接 LDAP";用 MySQL 存用户则每个应用都要自己写适配。④ 无事务:LDAP 没有跨条目事务,所以不适合做业务数据库。
Q4:应用集成 LDAP 的"两次 Bind"是什么?为什么不能直接读密码比对?
A:① 用服务账号 Bind 获得搜索权限;② Search 找到用户的完整 DN;③ 用该 DN + 用户输入的密码再 Bind,成功即认证通过。不能直接读 userPassword 是因为:密码通常是加盐哈希({SSHA})且算法不定、服务器一般禁止读取该属性、AD 根本不通过 LDAP 暴露密码。让服务器自己验证是唯一正确方式。
Q5:StartTLS 和 LDAPS 有什么区别?该用哪个?
A:LDAPS 走 636 端口,连上就 TLS 握手(隐式 TLS),无 RFC 但是事实标准;StartTLS 走 389 端口,先明文连接再用扩展操作(OID 1.3.6.1.4.1.1466.20037)升级,RFC 4513 官方推荐。StartTLS 的风险是可能被中间人剥离(降级攻击),所以客户端必须用 -ZZ 强制成功。现代实践两者都可以,关键是必须加密——Simple Bind 传的是明文密码。
Q6:LDAP 和 Active Directory 是什么关系? A:LDAP 是协议,AD 是微软的目录服务产品。AD 用 LDAP 作为主要访问协议,但 AD 远不止 LDAP——它还整合了 Kerberos(88,认证)、DNS(53,SRV 服务发现)、SMB(445,SYSVOL 组策略分发)、全局编录(3268)、复制协议(DRSUAPI/RPC)。反过来 OpenLDAP 是纯 LDAP 实现,没有 Kerberos 和组策略。
Q7:什么是 LDAP 注入?怎么防?
A:把用户输入直接拼进过滤器字符串。输入 *)(uid=*))(|(uid=* 会把 (uid=<输入>) 变成永真式,可能绕过认证或越权查询。防御是对 * ( ) \ NUL 五个字符按 RFC 4515 转义为 \2a \28 \29 \5c \00,或使用支持参数化的 LDAP 库。
Q8:为什么删除一个 OU 会报错 66?
A:notAllowedOnNonLeaf —— LDAP 的 DelRequest 语义上只能删除叶子节点,不存在"级联删除"。必须先递归删光所有子条目(按 DN 深度从深到浅),最后才能删这个 OU。
Q9:AD 搜索用户只返回 1000 条怎么办?
A:AD 的 MaxPageSize 默认 1000,超过会返回 adminLimitExceeded(11) 且结果不完整。正确做法是启用分页控制(Control OID 1.2.840.113556.1.4.319),命令行用 ldapsearch -E pr=500/noprompt,代码里用库的 paged search API。也可以按 OU 分批查询规避。
Q10:怎么快速摸清一台陌生 LDAP 服务器?
A:查 RootDSE(-s base -b "")。它是匿名可读的元数据条目,能拿到:namingContexts(有哪些 Base DN)、supportedLDAPVersion、supportedSASLMechanisms(支持哪些认证)、supportedExtension(有没有 StartTLS)、supportedControl(支不支持分页)、subschemaSubentry(Schema 在哪)。这是所有 LDAP 排错和渗透的第一步。