use std::time::Duration;
use ldap3::{Ldap, Scope, SearchEntry};
use rand::Rng;
use super::parser::LdapObject;
pub async fn query_asrep_candidates(ldap: &mut Ldap, base_dn: &str) -> anyhow::Result<Vec<LdapObject>> {
let filter = "(userAccountControl:1.2.840.113556.1.4.803:=4194304)";
let attrs = vec!["sAMAccountName", "userAccountControl", "distinguishedName"];
search(ldap, base_dn, filter, &attrs).await
}
pub async fn query_spn_accounts(ldap: &mut Ldap, base_dn: &str) -> anyhow::Result<Vec<LdapObject>> {
let filter = "(&(servicePrincipalName=*)(!(objectClass=computer))(!(userAccountControl:1.2.840.113556.1.4.803:=2)))";
let attrs = vec![
"sAMAccountName",
"servicePrincipalName",
"msDS-SupportedEncryptionTypes",
"distinguishedName",
"memberOf",
];
search(ldap, base_dn, filter, &attrs).await
}
pub async fn query_description_leaks(ldap: &mut Ldap, base_dn: &str) -> anyhow::Result<Vec<LdapObject>> {
let keywords = ["pass", "pwd", "secret", "cred", "token", "key", "p@ss"];
let mut results: Vec<LdapObject> = Vec::new();
let mut seen_dns = std::collections::HashSet::new();
for kw in &keywords {
let filter = format!("(&(objectCategory=person)(description=*{}*))", kw);
let attrs = vec!["sAMAccountName", "description", "distinguishedName"];
for obj in search(ldap, base_dn, &filter, &attrs).await? {
if seen_dns.insert(obj.dn.clone()) {
results.push(obj);
}
}
jitter().await;
}
Ok(results)
}
pub async fn query_unconstrained_delegation(ldap: &mut Ldap, base_dn: &str) -> anyhow::Result<Vec<LdapObject>> {
let filter = "(&(objectCategory=computer)(userAccountControl:1.2.840.113556.1.4.803:=524288))";
let attrs = vec!["cn", "dnsHostName", "distinguishedName", "operatingSystem", "userAccountControl"];
search(ldap, base_dn, filter, &attrs).await
}
pub async fn query_password_policy(ldap: &mut Ldap, base_dn: &str) -> anyhow::Result<Vec<LdapObject>> {
let attrs = vec![
"minPwdLength",
"maxPwdAge",
"minPwdAge",
"lockoutThreshold",
"lockoutDuration",
"pwdProperties",
"pwdHistoryLength",
];
search_with_scope(ldap, base_dn, Scope::Base, "(objectClass=*)", &attrs).await
}
pub async fn query_constrained_delegation(ldap: &mut Ldap, base_dn: &str) -> anyhow::Result<Vec<LdapObject>> {
let filter = "(|(msDS-AllowedToDelegateTo=*)(userAccountControl:1.2.840.113556.1.4.803:=1048576))";
let attrs = vec![
"sAMAccountName",
"msDS-AllowedToDelegateTo",
"userAccountControl",
"distinguishedName",
"objectClass",
];
search(ldap, base_dn, filter, &attrs).await
}
pub async fn query_rbcd(ldap: &mut Ldap, base_dn: &str) -> anyhow::Result<Vec<LdapObject>> {
let filter = "(msDS-AllowedToActOnBehalfOfOtherIdentity=*)";
let attrs = vec![
"cn",
"sAMAccountName",
"dnsHostName",
"distinguishedName",
"objectClass",
];
search(ldap, base_dn, filter, &attrs).await
}
pub async fn query_privileged_groups(ldap: &mut Ldap, base_dn: &str) -> anyhow::Result<Vec<(String, Vec<LdapObject>)>> {
let group_names = [
"Domain Admins",
"Enterprise Admins",
"Schema Admins",
"Backup Operators",
"Account Operators",
"Print Operators",
"Server Operators",
"Group Policy Creator Owners",
];
let mut results: Vec<(String, Vec<LdapObject>)> = Vec::new();
for group_name in &group_names {
let group_filter = format!("(&(objectClass=group)(cn={}))", group_name);
let group_objs = search(ldap, base_dn, &group_filter, &["distinguishedName"]).await?;
let group_dn = match group_objs.first().and_then(|o| Some(o.dn.as_str())) {
Some(dn) => dn.to_string(),
None => continue,
};
let member_filter = format!(
"(&(objectCategory=person)(memberOf:1.2.840.113556.1.4.1941:={}))",
group_dn
);
let members = search(ldap, base_dn, &member_filter, &[
"sAMAccountName",
"distinguishedName",
"userAccountControl",
"pwdLastSet",
])
.await
.unwrap_or_default();
if !members.is_empty() {
results.push((group_name.to_string(), members));
}
jitter().await;
}
Ok(results)
}
pub async fn query_stale_service_passwords(ldap: &mut Ldap, base_dn: &str) -> anyhow::Result<Vec<LdapObject>> {
let threshold = windows_timestamp_days_ago(365);
let filter = format!(
"(&(servicePrincipalName=*)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(objectClass=computer))(pwdLastSet<={threshold})(!(pwdLastSet=0)))",
threshold = threshold
);
let attrs = vec![
"sAMAccountName",
"pwdLastSet",
"servicePrincipalName",
"distinguishedName",
"userAccountControl",
];
search(ldap, base_dn, &filter, &attrs).await
}
fn windows_timestamp_days_ago(days: u64) -> i64 {
let days_secs = (days as i64).saturating_mul(86400);
let now_secs = chrono::Utc::now().timestamp();
let past_secs = now_secs.saturating_sub(days_secs);
past_secs
.saturating_mul(10_000_000)
.saturating_add(116_444_736_000_000_000)
}
async fn search(ldap: &mut Ldap, base: &str, filter: &str, attrs: &[&str]) -> anyhow::Result<Vec<LdapObject>> {
search_with_scope(ldap, base, Scope::Subtree, filter, attrs).await
}
async fn search_with_scope(
ldap: &mut Ldap,
base: &str,
scope: Scope,
filter: &str,
attrs: &[&str],
) -> anyhow::Result<Vec<LdapObject>> {
let (rs, _res) = ldap
.search(base, scope, filter, attrs)
.await?
.success()
.map_err(|e| anyhow::anyhow!("LDAP search error: {}", e))?;
Ok(rs
.into_iter()
.map(|entry| LdapObject::from_entry(SearchEntry::construct(entry)))
.collect())
}
pub async fn jitter() {
let ms: u64 = rand::thread_rng().gen_range(100..=500);
tokio::time::sleep(Duration::from_millis(ms)).await;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_asrep_candidates_filter_structure() {
let filter = "(userAccountControl:1.2.840.113556.1.4.803:=4194304)";
assert!(filter.contains("1.2.840.113556.1.4.803")); assert!(filter.contains("4194304")); }
#[test]
fn test_spn_accounts_filter_excludes_computers() {
let filter = "(&(servicePrincipalName=*)(!(objectClass=computer))(!(userAccountControl:1.2.840.113556.1.4.803:=2)))";
assert!(filter.contains("servicePrincipalName=*"));
assert!(filter.contains("!(objectClass=computer)"));
assert!(filter.contains("!(userAccountControl")); }
#[test]
fn test_unconstrained_delegation_filter() {
let filter = "(&(objectCategory=computer)(userAccountControl:1.2.840.113556.1.4.803:=524288))";
assert!(filter.contains("objectCategory=computer"));
assert!(filter.contains("524288")); }
#[test]
fn test_constrained_delegation_filter() {
let filter = "(|(msDS-AllowedToDelegateTo=*)(userAccountControl:1.2.840.113556.1.4.803:=1048576))";
assert!(filter.contains("|")); assert!(filter.contains("msDS-AllowedToDelegateTo=*"));
assert!(filter.contains("1048576")); }
#[test]
fn test_rbcd_filter() {
let filter = "(msDS-AllowedToActOnBehalfOfOtherIdentity=*)";
assert!(filter.contains("msDS-AllowedToActOnBehalfOfOtherIdentity=*"));
}
#[test]
fn test_stale_password_filter_structure() {
let threshold = 130680960000000000i64;
let filter = format!(
"(&(servicePrincipalName=*)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(objectClass=computer))(pwdLastSet<={threshold})(!(pwdLastSet=0)))",
threshold = threshold
);
assert!(filter.contains("servicePrincipalName=*"));
assert!(filter.contains("!(userAccountControl")); assert!(filter.contains("!(objectClass=computer)")); assert!(filter.contains(&threshold.to_string())); assert!(filter.contains("!(pwdLastSet=0)")); }
#[test]
fn test_description_leak_filter_keywords() {
let keywords = ["pass", "pwd", "secret"];
for kw in &keywords {
let filter = format!("(&(objectCategory=person)(description=*{}*))", kw);
assert!(filter.contains("objectCategory=person"));
assert!(filter.contains(&format!("description=*{}*", kw)));
}
}
#[test]
fn test_privileged_groups_filter_structure() {
let group_name = "Domain Admins";
let filter = format!("(&(objectClass=group)(cn={}))", group_name);
assert!(filter.contains("objectClass=group"));
assert!(filter.contains("cn=Domain Admins"));
}
#[test]
fn test_group_members_recursive_filter() {
let group_dn = "CN=Domain Admins,CN=Users,DC=corp,DC=local";
let filter = format!(
"(&(objectCategory=person)(memberOf:1.2.840.113556.1.4.1941:={}))",
group_dn
);
assert!(filter.contains("objectCategory=person"));
assert!(filter.contains("1.2.840.113556.1.4.1941")); assert!(filter.contains(group_dn));
}
#[test]
fn test_windows_timestamp_days_ago_calculation() {
let timestamp = windows_timestamp_days_ago(365);
assert!(timestamp > 0);
let now_filetime = chrono::Utc::now().timestamp() * 10_000_000 + 116_444_736_000_000_000;
let expected_max = now_filetime;
assert!(timestamp <= expected_max);
}
#[test]
fn test_windows_timestamp_days_ago_zero_days() {
let timestamp = windows_timestamp_days_ago(0);
let now_filetime = chrono::Utc::now().timestamp() * 10_000_000 + 116_444_736_000_000_000;
assert!((timestamp - now_filetime).abs() < 10_000_000); }
#[test]
fn test_windows_timestamp_days_ago_overflow_safe() {
let timestamp = windows_timestamp_days_ago(1_000_000);
assert!(timestamp >= 0 || timestamp < 0); }
#[test]
fn test_windows_timestamp_order() {
let ts_90 = windows_timestamp_days_ago(90);
let ts_180 = windows_timestamp_days_ago(180);
let ts_365 = windows_timestamp_days_ago(365);
assert!(ts_365 < ts_180);
assert!(ts_180 < ts_90);
}
#[test]
fn test_all_filters_have_balanced_parens() {
fn count_parens(s: &str) -> (usize, usize) {
let opens = s.chars().filter(|&c| c == '(').count();
let closes = s.chars().filter(|&c| c == ')').count();
(opens, closes)
}
let filters = vec![
"(userAccountControl:1.2.840.113556.1.4.803:=4194304)",
"(&(servicePrincipalName=*)(!(objectClass=computer))(!(userAccountControl:1.2.840.113556.1.4.803:=2)))",
"(&(objectCategory=computer)(userAccountControl:1.2.840.113556.1.4.803:=524288))",
"(|(msDS-AllowedToDelegateTo=*)(userAccountControl:1.2.840.113556.1.4.803:=1048576))",
];
for filter in filters {
let (opens, closes) = count_parens(filter);
assert_eq!(opens, closes, "Unbalanced parens in: {}", filter);
}
}
#[test]
fn test_filter_no_ldap_injection_chars() {
let filter = "(userAccountControl:1.2.840.113556.1.4.803:=4194304)";
assert!(!filter.contains("*=*")); }
#[test]
fn test_group_name_special_chars_in_filter() {
let group_name = "Group With Spaces";
let filter = format!("(&(objectClass=group)(cn={}))", group_name);
assert!(filter.contains(group_name));
}
#[test]
fn test_asrep_candidates_attributes() {
let attrs = vec!["sAMAccountName", "userAccountControl", "distinguishedName"];
assert_eq!(attrs.len(), 3);
assert!(attrs.contains(&"sAMAccountName"));
assert!(attrs.contains(&"distinguishedName"));
}
#[test]
fn test_spn_accounts_includes_encryption_types() {
let attrs = vec![
"sAMAccountName",
"servicePrincipalName",
"msDS-SupportedEncryptionTypes",
"distinguishedName",
"memberOf",
];
assert!(attrs.contains(&"msDS-SupportedEncryptionTypes"));
assert!(attrs.contains(&"servicePrincipalName"));
}
#[test]
fn test_stale_password_attributes() {
let attrs = vec![
"sAMAccountName",
"pwdLastSet",
"servicePrincipalName",
"distinguishedName",
"userAccountControl",
];
assert!(attrs.contains(&"pwdLastSet"));
}
#[test]
fn test_uac_bit_values() {
assert_eq!(4194304, 0x400000); assert_eq!(524288, 0x80000); assert_eq!(1048576, 0x100000); assert_eq!(2, 0x2); }
#[test]
fn test_ldap_oid_values() {
let bitwise_and_oid = "1.2.840.113556.1.4.803"; let in_chain_oid = "1.2.840.113556.1.4.1941";
let filter1 = "(userAccountControl:1.2.840.113556.1.4.803:=4194304)";
let filter2 = "(memberOf:1.2.840.113556.1.4.1941:=CN=Admins,DC=corp,DC=local)";
assert!(filter1.contains(bitwise_and_oid));
assert!(filter2.contains(in_chain_oid));
}
}