1use crate::args::Options;
8use crate::banner::progress_bar;
9use crate::storage::Storage;
10use crate::utils::format::domain_to_dc;
11
12use colored::Colorize;
13use indicatif::ProgressBar;
14use ldap3::adapters::{Adapter, EntriesOnly};
15use ldap3::exop::WhoAmI;
16use ldap3::{adapters::PagedResults, controls::RawControl, LdapConnAsync, LdapConnSettings};
17use ldap3::{Scope, SearchEntry};
18use log::{info, debug, error, trace};
19use std::io::{self, Write, stdin};
20use std::collections::HashMap;
21use std::error::Error;
22
23pub async fn ldap_auth(options: &Options) -> Result<ldap3::Ldap, Box<dyn Error>> {
29 let use_cert = options.pfx.is_some() || options.crt.is_some();
30
31 let starttls = use_cert && !options.ldaps;
33 let effective_ldaps = if use_cert { !starttls } else { options.ldaps };
34 let effective_port = if use_cert {
35 options.port.or(Some(if starttls { 389 } else { 636 }))
36 } else {
37 options.port
38 };
39
40 let ldap_args = ldap_constructor(
41 effective_ldaps,
42 options.ip.as_deref(),
43 effective_port,
44 &options.domain,
45 options.ldapfqdn.as_deref(),
46 options.username.as_deref(),
47 options.password.as_deref(),
48 options.hashes.as_deref(),
49 options.kerberos,
50 use_cert,
51 )?;
52
53 let mut consettings = LdapConnSettings::new()
54 .set_conn_timeout(std::time::Duration::from_secs(10))
55 .set_no_tls_verify(true);
56 if use_cert {
57 let config = crate::transport::cert::build_client_config(
58 options.pfx.as_deref(),
59 options.pfx_pass.as_deref(),
60 options.crt.as_deref(),
61 options.key.as_deref(),
62 )?;
63 consettings = consettings.set_config(config);
64 if starttls {
65 consettings = consettings.set_starttls(true);
66 }
67 }
68
69 let (conn, mut ldap) = LdapConnAsync::with_settings(consettings, &ldap_args.s_url).await?;
70 ldap3::drive!(conn);
71
72 let domain = &options.domain;
73
74 if use_cert {
75 if starttls {
78 debug!("Certificate authentication (StartTLS + SASL EXTERNAL)");
79 ldap.sasl_external_bind()
80 .await
81 .and_then(|r| r.success())
82 .map_err(|e| format!("certificate SASL EXTERNAL bind failed (try --ldaps): {e}"))?;
83 } else {
84 debug!("Certificate authentication (LDAPS, implicit Schannel mapping)");
85 }
86 let who = ldap
87 .extended(WhoAmI)
88 .await
89 .map(|r| r.success())
90 .map_err(|e| format!("certificate whoami request failed: {e}"))?
91 .map_err(|e| format!("certificate whoami failed: {e}"))?
92 .0
93 .val
94 .as_ref()
95 .map(|v| String::from_utf8_lossy(v).to_string())
96 .unwrap_or_default();
97 if who.is_empty() {
98 return Err(format!(
99 "certificate not mapped by {} (empty whoami); check the cert SID and DC enforcement (KB5014754)",
100 domain.to_uppercase()
101 )
102 .into());
103 }
104 info!(
105 "Connected to {} Active Directory via certificate as {}!",
106 domain.to_uppercase().bold().green(),
107 who.bold().green()
108 );
109 } else if let Some(ref ntlm_password) = ldap_args.s_ntlm_password {
110 debug!("NTLM pass-the-hash (sasl_ntlm_bind)");
111 ldap.sasl_ntlm_bind(&ldap_args.s_username, ntlm_password)
112 .await?
113 .success()
114 .map_err(|e| format!("NTLM authentication to {} failed: {e}", domain.to_uppercase()))?;
115 info!("Connected to {} Active Directory via NTLM!", domain.to_uppercase().bold().green());
116 } else if !options.kerberos {
117 debug!("Simple bind (username:password)");
118 ldap.simple_bind(&ldap_args.s_username, &ldap_args.s_password)
119 .await?
120 .success()
121 .map_err(|e| format!("authentication to {} failed: {e}", domain.to_uppercase()))?;
122 info!("Connected to {} Active Directory!", domain.to_uppercase().bold().green());
123 } else {
124 debug!("Kerberos (sasl_gssapi_bind)");
125 let fqdn = options
126 .ldapfqdn
127 .as_deref()
128 .filter(|f| !f.is_empty())
129 .ok_or("Kerberos requires the Domain Controller FQDN (set options.ldapfqdn, e.g. DC01.DOMAIN.LOCAL)")?;
130 #[cfg(not(feature = "nogssapi"))]
131 {
132 gssapi_connection(&mut ldap, fqdn, domain).await?;
133 }
134 #[cfg(feature = "nogssapi")]
135 {
136 let _ = fqdn;
137 return Err("Kerberos/GSSAPI is not available in this build (nogssapi feature)".into());
138 }
139 }
140
141 Ok(ldap)
142}
143
144pub(crate) async fn collect_from_ldap_into<S: Storage<LdapSearchEntry>>(
148 ldap: &mut ldap3::Ldap,
149 ldapfilter: &str,
150 storage: &mut S,
151) -> Result<usize, Box<dyn Error>> {
152 let mut total = 0usize;
153
154 let res = get_all_naming_contexts(ldap).await?;
155 trace!("naming_contexts: {:?}", &res);
156
157 if !res.iter().any(|s| s.contains("Configuration")) {
158 return Err("no Configuration namingContext found (is the target a Domain Controller?)".into());
159 }
160
161 for cn in &res {
162 let sd_flags = RawControl {
164 ctype: String::from("1.2.840.113556.1.4.801"),
165 crit: true,
166 val: Some(vec![48, 3, 2, 1, 5]), };
168 let show_deleted = RawControl {
170 ctype: String::from("1.2.840.113556.1.4.417"),
171 crit: false,
172 val: None,
173 };
174 ldap.with_controls(vec![sd_flags, show_deleted]);
175
176 info!("Ldap filter : {}", ldapfilter.bold().green());
177
178 let adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
179 Box::new(EntriesOnly::new()),
180 Box::new(PagedResults::new(999)),
181 ];
182
183 let mut search = ldap
184 .streaming_search_with(
185 adapters,
186 cn,
187 Scope::Subtree,
188 ldapfilter,
189 vec!["*", "nTSecurityDescriptor", "msDS-User-Account-Control-Computed"],
190 )
191 .await?;
192
193 let mut ranged_ldap = ldap.clone();
197
198 let pb = ProgressBar::new(1);
199 let mut count = 0;
200 while let Some(entry) = search.next().await? {
201 let mut entry = SearchEntry::construct(entry);
202 complete_ranged_attributes(&mut ranged_ldap, &mut entry).await?;
203 total += 1;
204 count += 1;
205 progress_bar(
206 pb.to_owned(),
207 "LDAP objects retrieved".to_string(),
208 count,
209 "#".to_string(),
210 );
211 storage.add(entry.into())?;
212 }
213 pb.finish_and_clear();
214
215 match search.finish().await.success() {
216 Ok(_) => info!("All data collected for NamingContext {}", &cn.bold()),
217 Err(err) => error!("No data collected on {}! Reason: {err}", &cn.bold().red()),
218 }
219 }
220
221 storage.flush()?;
222 Ok(total)
223}
224
225async fn complete_ranged_attributes(
235 ldap: &mut ldap3::Ldap,
236 entry: &mut SearchEntry,
237) -> Result<(), Box<dyn Error>> {
238 let ranged: Vec<(String, String)> = entry
240 .attrs
241 .keys()
242 .filter_map(|k| {
243 let (base, opt) = k.split_once(';')?;
244 if opt.to_ascii_lowercase().starts_with("range=") {
245 Some((k.to_string(), base.to_string()))
246 } else {
247 None
248 }
249 })
250 .collect();
251
252 for (ranged_key, base_attr) in ranged {
253 let mut values = entry.attrs.remove(&ranged_key).unwrap_or_default();
255
256 let mut next_start = match range_high(&ranged_key) {
257 Some(high) => high + 1,
258 None => {
259 entry.attrs.entry(base_attr).or_default().append(&mut values);
261 continue;
262 }
263 };
264
265 debug!("Ranged attribute '{}' on {} exceeds MaxValRange; retrieving remaining values", base_attr.bold().yellow(), entry.dn.bold());
266
267 loop {
269 let want = format!("{base_attr};range={next_start}-*");
270 let (rs, _res) = ldap
271 .search(&entry.dn, Scope::Base, "(objectClass=*)", vec![want.as_str()])
272 .await?
273 .success()?;
274
275 let Some(re) = rs.into_iter().next() else { break };
276 let chunk = SearchEntry::construct(re);
277
278 let found = chunk.attrs.iter().find_map(|(k, _)| {
280 let (b, opt) = k.split_once(';')?;
281 if b.eq_ignore_ascii_case(&base_attr)
282 && opt.to_ascii_lowercase().starts_with("range=")
283 {
284 Some((k.clone(), opt.ends_with("-*")))
285 } else {
286 None
287 }
288 });
289
290 let Some((key, is_last)) = found else { break };
291
292 if let Some(v) = chunk.attrs.get(&key) {
293 values.extend(v.iter().cloned());
294 }
295
296 if is_last {
297 break; }
299 match range_high(&key) {
300 Some(high) => next_start = high + 1,
301 None => break,
302 }
303 }
304
305 trace!("'{}' on {}: {} values after ranged retrieval", base_attr, entry.dn, values.len());
306 entry.attrs.entry(base_attr).or_default().append(&mut values);
307 }
308
309 Ok(())
310}
311
312fn range_high(key: &str) -> Option<usize> {
315 let opt = key.split(';').nth(1)?; let spec = opt.split_once('=')?.1; let high = spec.split_once('-')?.1; high.parse::<usize>().ok()
319}
320
321struct LdapArgs {
323 s_url: String,
324 _s_dc: Vec<String>,
325 _s_email: String,
326 s_username: String,
327 s_password: String,
328 s_ntlm_password: Option<String>,
329}
330
331#[allow(clippy::too_many_arguments)]
333fn ldap_constructor(
334 ldaps: bool,
335 ip: Option<&str>,
336 port: Option<u16>,
337 domain: &str,
338 ldapfqdn: Option<&str>,
339 username: Option<&str>,
340 password: Option<&str>,
341 hashes: Option<&str>,
342 kerberos: bool,
343 use_cert: bool,
344) -> Result<LdapArgs, Box<dyn Error>> {
345 let s_url = prepare_ldap_url(ldaps, ip, port, domain);
346 let s_dc = prepare_ldap_dc(domain);
347 let use_ntlm = hashes.is_some();
348
349 let mut s = String::new();
351 let mut _s_username: String;
352 if username.is_none() && !kerberos && !use_cert {
353 print!("Username: ");
354 io::stdout().flush()?;
355 stdin().read_line(&mut s).expect("Did not enter a correct username");
356 io::stdout().flush()?;
357 if let Some('\n') = s.chars().next_back() { s.pop(); }
358 if let Some('\r') = s.chars().next_back() { s.pop(); }
359 _s_username = s.to_owned();
360 } else {
361 _s_username = username.unwrap_or("not set").to_owned();
362 }
363
364 let mut s_email: String = "".to_owned();
366 if !_s_username.contains("@") {
367 s_email.push_str(&_s_username.to_string());
368 s_email.push_str("@");
369 s_email.push_str(domain);
370 if !use_ntlm {
371 _s_username = s_email.to_string();
372 }
373 } else {
374 s_email = _s_username.to_string().to_lowercase();
375 }
376
377 if use_ntlm && !_s_username.contains("\\") && !_s_username.contains("@") {
379 let domain_upper = domain.split('.').next().unwrap_or(domain).to_uppercase();
380 _s_username = format!("{}\\{}", domain_upper, _s_username);
381 }
382
383 let s_ntlm_password = match hashes {
385 Some(hash) => {
386 let clean = hash.trim();
387 let nt = match clean.split_once(':') {
388 Some((_lm, nt)) => nt,
389 None => clean,
390 };
391 if nt.len() != 32 || !nt.chars().all(|c| c.is_ascii_hexdigit()) {
392 return Err("Invalid NT hash: must be exactly 32 hex characters (e.g. aad3b435b51404eeaad3b435b51404ee)".into());
393 }
394 Some(nt_hash_to_ntlm_password(nt))
395 }
396 None => None,
397 };
398
399 let mut _s_password: String = String::new();
401 if !use_ntlm && !_s_username.contains("not set") && !kerberos && !use_cert {
402 _s_password = match password {
403 Some(p) => p.to_owned(),
404 None => rpassword::prompt_password("Password: ").unwrap_or("not set".to_string()),
405 };
406 } else {
407 _s_password = password.unwrap_or("not set").to_owned();
408 }
409
410 debug!("IP: {}", ip.unwrap_or("not set"));
411 debug!("PORT: {}", match port { Some(p) => p.to_string(), None => "not set".to_owned() });
412 debug!("FQDN: {}", ldapfqdn.unwrap_or("not set"));
413 debug!("Url: {}", s_url);
414 debug!("Domain: {}", domain);
415 debug!("Username: {}", _s_username);
416 debug!("Email: {}", s_email.to_lowercase());
417 if use_cert {
418 debug!("Auth: certificate (Pass-the-Certificate)");
419 } else if use_ntlm {
420 debug!("Auth: NTLM pass-the-hash");
421 } else {
422 debug!("Password: {}", _s_password);
423 }
424 debug!("DC: {:?}", s_dc);
425 debug!("Kerberos: {:?}", kerberos);
426
427 Ok(LdapArgs {
428 s_url: s_url.to_string(),
429 _s_dc: s_dc,
430 _s_email: s_email.to_string().to_lowercase(),
431 s_username: if use_ntlm { _s_username.to_string() } else { s_email.to_string().to_lowercase() },
432 s_password: _s_password.to_string(),
433 s_ntlm_password,
434 })
435}
436
437fn nt_hash_to_ntlm_password(hex_hash: &str) -> String {
440 let upper = hex_hash.to_uppercase();
441 let bytes = upper.as_bytes();
442 let mut password = String::new();
443 for pair in bytes.chunks(2) {
444 let low_byte = pair[0] as u32;
445 let high_byte = if pair.len() > 1 { pair[1] as u32 } else { 0 };
446 let code_point = (high_byte << 8) | low_byte;
447 password.push(char::from_u32(code_point).unwrap_or('\0'));
448 }
449 for _ in 0..256 {
450 password.push('\0');
451 }
452 password
453}
454
455fn prepare_ldap_url(ldaps: bool, ip: Option<&str>, port: Option<u16>, domain: &str) -> String {
457 let protocol = if ldaps || port.unwrap_or(0) == 636 { "ldaps" } else { "ldap" };
458 let target = match ip { Some(ip) => ip, None => domain };
459 match port {
460 Some(port) => format!("{protocol}://{target}:{port}"),
461 None => format!("{protocol}://{target}"),
462 }
463}
464
465pub fn prepare_ldap_dc(domain: &str) -> Vec<String> {
467 let mut dc: String = "".to_owned();
468 let mut naming_context: Vec<String> = Vec::new();
469 if !domain.contains(".") {
470 dc.push_str("DC=");
471 dc.push_str(domain);
472 naming_context.push(dc[..].to_string());
473 } else {
474 naming_context.push(domain_to_dc(domain));
475 }
476 naming_context.push(format!("{}{}", "CN=Configuration,", &dc[..]));
477 naming_context
478}
479
480#[cfg(not(feature = "nogssapi"))]
482async fn gssapi_connection(
483 ldap: &mut ldap3::Ldap,
484 ldapfqdn: &str,
485 domain: &str,
486) -> Result<(), Box<dyn Error>> {
487 ldap.sasl_gssapi_bind(ldapfqdn)
488 .await?
489 .success()
490 .map_err(|e| format!("Kerberos authentication to {} failed: {e}", domain.to_uppercase()))?;
491 info!("Connected to {} Active Directory!", domain.to_uppercase().bold().green());
492 Ok(())
493}
494
495pub async fn get_all_naming_contexts(ldap: &mut ldap3::Ldap) -> Result<Vec<String>, Box<dyn Error>> {
497 let adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
498 Box::new(EntriesOnly::new()),
499 Box::new(PagedResults::new(999)),
500 ];
501 let mut search = ldap.streaming_search_with(
502 adapters,
503 "",
504 Scope::Base,
505 "(objectClass=*)",
506 vec!["namingContexts"],
507 ).await?;
508
509 let mut rs: Vec<SearchEntry> = Vec::new();
510 while let Some(entry) = search.next().await? {
511 rs.push(SearchEntry::construct(entry));
512 }
513 let res = search.finish().await.success();
514
515 let mut naming_contexts: Vec<String> = Vec::new();
516 match res {
517 Ok(_res) => {
518 debug!("All namingContexts collected!");
519 for result in rs {
520 for (_key, value) in &result.attrs {
521 for naming_context in value {
522 debug!("namingContext found: {}", &naming_context.bold().green());
523 naming_contexts.push(naming_context.to_string());
524 }
525 }
526 }
527 naming_contexts.sort_by_key(|cn| {
528 if cn.contains("CN=Schema") { 0 }
529 else if cn.to_lowercase().starts_with("dc=") { 1 }
530 else if cn.contains("CN=Configuration") { 2 }
531 else { 3 }
532 });
533 for (i, nc) in naming_contexts.iter().enumerate() {
534 trace!("NamingContext order [{}]: {}", i, nc);
535 }
536 return Ok(naming_contexts);
537 }
538 Err(err) => {
539 error!("No namingContexts found! Reason: {err}");
540 }
541 }
542 Ok(Vec::new())
543}
544
545#[derive(Debug, Clone, bincode::Encode, bincode::Decode)]
547pub struct LdapSearchEntry {
548 pub dn: String,
549 pub attrs: HashMap<String, Vec<String>>,
550 pub bin_attrs: HashMap<String, Vec<Vec<u8>>>,
551}
552
553impl From<SearchEntry> for LdapSearchEntry {
554 fn from(entry: SearchEntry) -> Self {
555 LdapSearchEntry { dn: entry.dn, attrs: entry.attrs, bin_attrs: entry.bin_attrs }
556 }
557}
558
559impl From<LdapSearchEntry> for SearchEntry {
560 fn from(entry: LdapSearchEntry) -> Self {
561 SearchEntry { dn: entry.dn, attrs: entry.attrs, bin_attrs: entry.bin_attrs }
562 }
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568
569 #[test]
570 fn range_high_parsing() {
571 assert_eq!(range_high("member;range=0-1499"), Some(1499));
572 assert_eq!(range_high("member;range=1500-2999"), Some(2999));
573 assert_eq!(range_high("member;range=3000-*"), None); assert_eq!(range_high("member"), None); assert_eq!(range_high("member;range=bad"), None); }
577
578 #[test]
579 fn nt_hash_encoding_roundtrip() {
580 let hash = "aad3b435b51404eeaad3b435b51404ee";
581 let password = nt_hash_to_ntlm_password(hash);
582 let utf16_bytes: Vec<u8> = password.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
583 assert!(utf16_bytes.len() > 512);
584 let hash_portion = &utf16_bytes[..utf16_bytes.len() - 512];
585 assert_eq!(hash_portion.len(), 32);
586 assert_eq!(hash_portion, hash.to_uppercase().as_bytes());
587 }
588
589 #[test]
590 fn nt_hash_encoding_all_zeros() {
591 let hash = "00000000000000000000000000000000";
592 let password = nt_hash_to_ntlm_password(hash);
593 let utf16_bytes: Vec<u8> = password.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
594 assert!(utf16_bytes.len() > 512);
595 let hash_portion = &utf16_bytes[..utf16_bytes.len() - 512];
596 assert_eq!(hash_portion, b"00000000000000000000000000000000");
597 }
598
599 #[test]
600 fn nt_hash_encoding_all_f() {
601 let hash = "ffffffffffffffffffffffffffffffff";
602 let password = nt_hash_to_ntlm_password(hash);
603 let utf16_bytes: Vec<u8> = password.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
604 assert!(utf16_bytes.len() > 512);
605 let hash_portion = &utf16_bytes[..utf16_bytes.len() - 512];
606 assert_eq!(hash_portion, b"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
607 }
608}