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;
}