[pitboss] phase 06: Track J.4 + Track L.4 — LDAP_INJECTION corpus + LdapTemplate / python-ldap / php-ldap adapters

This commit is contained in:
pitboss 2026-05-17 22:32:44 -05:00
parent 993bfabe28
commit b2eeaabb09
27 changed files with 2189 additions and 18 deletions

View file

@ -0,0 +1,53 @@
//! Java `Cap::LDAP_INJECTION` payloads — `LdapTemplate.search` /
//! `DirContext.search` filter injection.
//!
//! Vuln payload: a filter fragment whose `*)(uid=*` tail breaks out of
//! the host template's `(uid=…)` clause and rewraps the search as
//! `(|(uid=…)(uid=*))`, matching every user the directory carries.
//! The harness's instrumented LDAP client (talking to
//! [`crate::dynamic::stubs::ldap_server`]) records
//! `ProbeKind::Ldap { entries_returned: 3 }`.
//!
//! Benign control: the same intended username quoted through
//! `EscapeDN` so the LDAP filter stays pinned to a single entry; the
//! shim records `entries_returned: 1` and the oracle does not fire.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"alice*)(uid=*",
label: "ldap-java-filter-wildcard",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::LdapResultCountGreaterThan { n: 1 }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 10,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ldap_injection/java/Vuln.java"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::LdapResultCountGreaterThan { n: 1 }],
benign_control: Some(PayloadRef {
label: "ldap-java-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"alice",
label: "ldap-java-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::LdapResultCountGreaterThan { n: 1 }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 10,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ldap_injection/java/Benign.java"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,30 @@
//! LDAP filter injection (`Cap::LDAP_INJECTION`) per-language payload
//! slices.
//!
//! Phase 06 (Track J.4) carves LDAP filter injection across the three
//! most-common directory clients: Java (`LdapTemplate.search` /
//! `DirContext.search`), Python (`ldap.search_s`), and PHP
//! (`ldap_search`). Every vuln payload appends the canonical
//! `*)(uid=*` quote-escape break — once the host code substitutes the
//! attacker bytes into its filter template the synthesized LDAP
//! filter matches every entry the directory carries (the
//! [`crate::dynamic::stubs::ldap_server`] stub returns its three
//! provisioned users). The paired benign control quotes the same
//! bytes through `EscapeDN` / `ldap.dn.escape_filter_chars` /
//! `ldap_escape`, leaving the filter pinned to the originally
//! intended single user.
//!
//! The oracle's
//! [`crate::dynamic::oracle::ProbePredicate::LdapResultCountGreaterThan`]
//! checks the per-payload `ProbeKind::Ldap.entries_returned` against
//! `n = 1` — vuln passes (3 entries), benign clears (1 entry),
//! fulfilling the §4.1 differential rule.
//!
//! C# is intentionally omitted: the [`crate::symbol::Lang`] enum has
//! no `CSharp` variant, so the corpus has nowhere to register it.
//! Tracked in `.pitboss/play/deferred.md` alongside the Phase 05
//! Lang::CSharp gap.
pub mod java;
pub mod php;
pub mod python;

View file

@ -0,0 +1,51 @@
//! PHP `Cap::LDAP_INJECTION` payloads — `ldap_search` filter injection.
//!
//! Vuln payload: a filter fragment whose `*)(uid=*` tail breaks out of
//! the host template's `(uid=…)` clause; the synthesized filter
//! becomes `(|(uid=…)(uid=*))` and matches every directory entry.
//! The harness's instrumented `ldap_search` records
//! `ProbeKind::Ldap { entries_returned: 3 }`.
//!
//! Benign control: the same intended username quoted via
//! `ldap_escape($value, "", LDAP_ESCAPE_FILTER)` — `entries_returned:
//! 1`, oracle clear.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"alice*)(uid=*",
label: "ldap-php-filter-wildcard",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::LdapResultCountGreaterThan { n: 1 }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 10,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ldap_injection/php/vuln.php"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::LdapResultCountGreaterThan { n: 1 }],
benign_control: Some(PayloadRef {
label: "ldap-php-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"alice",
label: "ldap-php-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::LdapResultCountGreaterThan { n: 1 }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 10,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ldap_injection/php/benign.php"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,52 @@
//! Python `Cap::LDAP_INJECTION` payloads — `ldap.search_s` filter
//! injection.
//!
//! Vuln payload: a filter fragment whose `*)(uid=*` tail breaks out of
//! the host template's `(uid=…)` clause; the synthesized filter
//! becomes `(|(uid=…)(uid=*))` and matches every directory entry.
//! The harness's instrumented `ldap.search_s` records
//! `ProbeKind::Ldap { entries_returned: 3 }`.
//!
//! Benign control: the same intended username quoted via
//! `ldap.dn.escape_filter_chars`, leaving the filter pinned to a
//! single entry — `entries_returned: 1`, oracle clear.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"alice*)(uid=*",
label: "ldap-python-filter-wildcard",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::LdapResultCountGreaterThan { n: 1 }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 10,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ldap_injection/python/vuln.py"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::LdapResultCountGreaterThan { n: 1 }],
benign_control: Some(PayloadRef {
label: "ldap-python-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"alice",
label: "ldap-python-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::LdapResultCountGreaterThan { n: 1 }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 10,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ldap_injection/python/benign.py"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -23,7 +23,7 @@
use std::collections::HashMap;
use std::sync::OnceLock;
use super::{cmdi, deserialize, fmt_string, path_trav, sqli, ssrf, ssti, xss, xxe};
use super::{cmdi, deserialize, fmt_string, ldap, path_trav, sqli, ssrf, ssti, xss, xxe};
use super::{CapCorpus, CuratedPayload, Oracle};
use crate::dynamic::oracle::ProbePredicate;
use crate::labels::Cap;
@ -40,7 +40,6 @@ pub const CORPUS_UNSUPPORTED_LANG_NEUTRAL: u32 = Cap::ENV_VAR.bits()
| Cap::CRYPTO.bits()
| Cap::UNAUTHORIZED_ID.bits()
| Cap::DATA_EXFIL.bits()
| Cap::LDAP_INJECTION.bits()
| Cap::XPATH_INJECTION.bits()
| Cap::HEADER_INJECTION.bits()
| Cap::OPEN_REDIRECT.bits()
@ -69,6 +68,9 @@ const ENTRIES: &[(Cap, Lang, &[CuratedPayload])] = &[
(Cap::XXE, Lang::Php, xxe::php::PAYLOADS),
(Cap::XXE, Lang::Ruby, xxe::ruby::PAYLOADS),
(Cap::XXE, Lang::Go, xxe::go::PAYLOADS),
(Cap::LDAP_INJECTION, Lang::Java, ldap::java::PAYLOADS),
(Cap::LDAP_INJECTION, Lang::Python, ldap::python::PAYLOADS),
(Cap::LDAP_INJECTION, Lang::Php, ldap::php::PAYLOADS),
];
/// Reserved for per-cap oracle defaults. Empty in Phase 02; populated by
@ -278,6 +280,7 @@ mod tests {
assert!(!payloads_for(Cap::DESERIALIZE).is_empty());
assert!(!payloads_for(Cap::SSTI).is_empty());
assert!(!payloads_for(Cap::XXE).is_empty());
assert!(!payloads_for(Cap::LDAP_INJECTION).is_empty());
}
#[test]
@ -290,7 +293,6 @@ mod tests {
Cap::CRYPTO,
Cap::UNAUTHORIZED_ID,
Cap::DATA_EXFIL,
Cap::LDAP_INJECTION,
Cap::XPATH_INJECTION,
Cap::HEADER_INJECTION,
Cap::OPEN_REDIRECT,
@ -325,6 +327,7 @@ mod tests {
Cap::DESERIALIZE,
Cap::SSTI,
Cap::XXE,
Cap::LDAP_INJECTION,
] {
let has_vuln = payloads_for(cap).iter().any(|p| !p.is_benign);
assert!(has_vuln, "{cap:?} must have at least one vuln payload");
@ -374,6 +377,7 @@ mod tests {
Cap::DESERIALIZE,
Cap::SSTI,
Cap::XXE,
Cap::LDAP_INJECTION,
];
for cap in caps {
for p in payloads_for(cap) {
@ -398,6 +402,7 @@ mod tests {
Cap::DESERIALIZE,
Cap::SSTI,
Cap::XXE,
Cap::LDAP_INJECTION,
];
for cap in caps {
for p in payloads_for(cap) {
@ -509,6 +514,7 @@ mod tests {
Cap::DESERIALIZE,
Cap::SSTI,
Cap::XXE,
Cap::LDAP_INJECTION,
];
for cap in caps {
for p in payloads_for(cap).iter().filter(|p| p.is_benign) {
@ -677,6 +683,49 @@ mod tests {
}
}
#[test]
fn ldap_has_per_lang_slices_for_phase_06() {
// Phase 06 (Track J.4) acceptance: LDAP_INJECTION registers
// payloads in Java / Python / PHP and the lang-aware lookup
// never returns empty for any of them.
for lang in [Lang::Java, Lang::Python, Lang::Php] {
assert!(
!payloads_for_lang(Cap::LDAP_INJECTION, lang).is_empty(),
"LDAP_INJECTION must have at least one payload for {lang:?}",
);
}
// Rust / C / Cpp / Ruby / Go / JS / TS not yet covered.
for lang in [
Lang::Rust,
Lang::C,
Lang::Cpp,
Lang::Ruby,
Lang::Go,
Lang::JavaScript,
Lang::TypeScript,
] {
assert!(
payloads_for_lang(Cap::LDAP_INJECTION, lang).is_empty(),
"LDAP_INJECTION has unexpected payloads for {lang:?}",
);
}
}
#[test]
fn ldap_payloads_pair_benign_controls_per_lang() {
for lang in [Lang::Java, Lang::Python, Lang::Php] {
let slice = payloads_for_lang(Cap::LDAP_INJECTION, lang);
let vuln = slice
.iter()
.find(|p| !p.is_benign)
.expect("each lang must have an LDAP vuln payload");
let resolved =
super::resolve_benign_control_lang(vuln, Cap::LDAP_INJECTION, lang)
.expect("lang-aware benign control must resolve");
assert!(resolved.is_benign);
}
}
#[test]
fn deserialize_payloads_pair_benign_controls_per_lang() {
// The lang-aware resolver must find the paired benign control