1use crate::banner::progress_bar;
15use crate::storage::Storage;
16use crate::utils::format::domain_to_dc;
17
18use colored::Colorize;
19use indicatif::ProgressBar;
20use ldap3::adapters::{Adapter, EntriesOnly};
21use ldap3::exop::WhoAmI;
22use ldap3::{adapters::PagedResults, controls::RawControl, LdapConnAsync, LdapConnSettings};
23use ldap3::{Scope, SearchEntry};
24use log::{info, debug, error, trace};
25use std::io::{self, Write, stdin};
26use std::collections::HashMap;
27use std::error::Error;
28use std::process;
29
30#[allow(clippy::too_many_arguments)]
32pub async fn ldap_search<S: Storage<LdapSearchEntry>>(
33 ldaps: bool,
34 ip: Option<&str>,
35 port: Option<u16>,
36 domain: &str,
37 ldapfqdn: Option<&str>,
38 username: Option<&str>,
39 password: Option<&str>,
40 hashes: Option<&str>,
41 kerberos: bool,
42 pfx: Option<&str>,
44 pfx_pass: Option<&str>,
45 crt: Option<&str>,
46 key: Option<&str>,
47 ldapfilter: &str,
48 storage: &mut S,
49) -> Result<usize, Box<dyn Error>> {
50 let use_cert = pfx.is_some() || crt.is_some();
52
53 let starttls = use_cert && !ldaps;
58
59 let effective_ldaps = if use_cert { !starttls } else { ldaps };
62
63 let effective_port = if use_cert {
69 port.or(Some(if starttls { 389 } else { 636 }))
70 } else {
71 port
72 };
73
74 let ldap_args = ldap_constructor(
76 effective_ldaps, ip, effective_port, domain, ldapfqdn, username, password, hashes, kerberos, use_cert,
77 )?;
78
79 let mut consettings = LdapConnSettings::new()
81 .set_conn_timeout(std::time::Duration::from_secs(10))
82 .set_no_tls_verify(true);
83
84 if use_cert {
86 let config = crate::transport::cert::build_client_config(pfx, pfx_pass, crt, key)?;
87 consettings = consettings.set_config(config);
88 if starttls {
89 consettings = consettings.set_starttls(true);
90 }
91 }
92
93 let (conn, mut ldap) = LdapConnAsync::with_settings(consettings, &ldap_args.s_url).await?;
94 ldap3::drive!(conn);
95
96 if use_cert {
97 if starttls {
102 debug!("Trying certificate authentication (StartTLS + SASL EXTERNAL)");
103 match ldap.sasl_external_bind().await.and_then(|r| r.success()) {
104 Ok(_) => {}
105 Err(err) => {
106 error!(
107 "Certificate SASL EXTERNAL bind failed on {} Active Directory. \
108 Some DCs refuse it over StartTLS; try LDAPS. Reason: {err}\n",
109 domain.to_uppercase().bold().red()
110 );
111 process::exit(0x0100);
112 }
113 }
114 } else {
115 debug!("Trying certificate authentication (LDAPS, implicit Schannel mapping)");
116 }
117
118 match ldap.extended(WhoAmI).await.map(|r| r.success()) {
120 Ok(Ok((exop, _))) => {
121 let who = exop
122 .val
123 .as_ref()
124 .map(|v| String::from_utf8_lossy(v).to_string())
125 .unwrap_or_default();
126 if who.is_empty() {
127 error!(
128 "Certificate not mapped by {} Active Directory (empty whoami). \
129 Check the certificate SID and the DC enforcement mode (KB5014754).\n",
130 domain.to_uppercase().bold().red()
131 );
132 process::exit(0x0100);
133 }
134 info!(
135 "Connected to {} Active Directory via certificate as {}!",
136 domain.to_uppercase().bold().green(),
137 who.bold().green()
138 );
139 info!("Starting data collection...");
140 }
141 Ok(Err(err)) => {
142 error!(
143 "Certificate authentication failed on {} Active Directory. Reason: {err}\n",
144 domain.to_uppercase().bold().red()
145 );
146 process::exit(0x0100);
147 }
148 Err(err) => {
149 error!(
150 "Certificate authentication request failed on {} Active Directory. Reason: {err}\n",
151 domain.to_uppercase().bold().red()
152 );
153 process::exit(0x0100);
154 }
155 }
156 } else if let Some(ref ntlm_password) = ldap_args.s_ntlm_password {
157 debug!("Trying to connect with sasl_ntlm_bind() function (NTLM pass-the-hash)");
158 let res = ldap
159 .sasl_ntlm_bind(&ldap_args.s_username, ntlm_password)
160 .await?
161 .success();
162 match res {
163 Ok(_res) => {
164 info!(
165 "Connected to {} Active Directory via NTLM!",
166 domain.to_uppercase().bold().green()
167 );
168 info!("Starting data collection...");
169 }
170 Err(err) => {
171 error!(
172 "Failed to authenticate to {} Active Directory via NTLM. Reason: {err}\n",
173 domain.to_uppercase().bold().red()
174 );
175 process::exit(0x0100);
176 }
177 }
178 } else if !kerberos {
179 debug!("Trying to connect with simple_bind() function (username:password)");
180 let res = ldap
181 .simple_bind(&ldap_args.s_username, &ldap_args.s_password)
182 .await?
183 .success();
184 match res {
185 Ok(_res) => {
186 info!(
187 "Connected to {} Active Directory!",
188 domain.to_uppercase().bold().green()
189 );
190 info!("Starting data collection...");
191 }
192 Err(err) => {
193 error!(
194 "Failed to authenticate to {} Active Directory. Reason: {err}\n",
195 domain.to_uppercase().bold().red()
196 );
197 process::exit(0x0100);
198 }
199 }
200 } else {
201 debug!("Trying to connect with sasl_gssapi_bind() function (kerberos session)");
202 if let Some(fqdn) = ldapfqdn.filter(|f| !f.is_empty()) {
203 #[cfg(not(feature = "nogssapi"))]
204 gssapi_connection(&mut ldap, fqdn, &domain).await?;
205 #[cfg(feature = "nogssapi")]
206 {
207 error!("Kerberos auth and GSSAPI not compatible with current os!");
208 process::exit(0x0100);
209 }
210 } else {
211 error!(
212 "Need Domain Controller FQDN to bind GSSAPI connection. Please use '{}'\n",
213 "-f DC01.DOMAIN.LAB".bold()
214 );
215 process::exit(0x0100);
216 }
217 }
218
219 let mut total = 0; let res = match get_all_naming_contexts(&mut ldap).await {
224 Ok(res) => {
225 trace!("naming_contexts: {:?}", &res);
226 res
227 }
228 Err(err) => {
229 error!("No namingContexts found! Reason: {err}\n");
230 process::exit(0x0100);
231 }
232 };
233
234 if res.iter().any(|s| s.contains("Configuration")) {
237 for cn in &res {
238 let sd_flags = RawControl {
241 ctype: String::from("1.2.840.113556.1.4.801"),
242 crit: true,
243 val: Some(vec![48, 3, 2, 1, 5]), };
245
246 let show_deleted = RawControl {
249 ctype: String::from("1.2.840.113556.1.4.417"),
250 crit: false, val: None,
252 };
253
254 let controls = vec![sd_flags, show_deleted];
255 ldap.with_controls(controls.to_owned());
256
257 info!("Ldap filter : {}", ldapfilter.bold().green());
258 let _s_filter = ldapfilter;
259
260 let adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
262 Box::new(EntriesOnly::new()),
263 Box::new(PagedResults::new(999)),
264 ];
265
266 let mut search = ldap
268 .streaming_search_with(
269 adapters, cn,
271 Scope::Subtree,
272 _s_filter,
273 vec!["*", "nTSecurityDescriptor"],
274 )
277 .await?;
278
279 let pb = ProgressBar::new(1);
281 let mut count = 0;
282 while let Some(entry) = search.next().await? {
283 let entry = SearchEntry::construct(entry);
284 total += 1;
286 count += 1;
288 progress_bar(
289 pb.to_owned(),
290 "LDAP objects retrieved".to_string(),
291 count,
292 "#".to_string(),
293 );
294
295 storage.add(entry.into())?;
296 }
297 pb.finish_and_clear();
298
299 let res = search.finish().await.success();
300 match res {
301 Ok(_res) => info!("All data collected for NamingContext {}", &cn.bold()),
302 Err(err) => {
303 error!("No data collected on {}! Reason: {err}", &cn.bold().red());
304 }
305 }
306 }
307
308 ldap.unbind().await?;
309 }
310
311 drop(ldap);
315 if total == 0 {
316 error!("No LDAP objects found! Exiting...");
317 process::exit(0x0100);
318 }
319
320 storage.flush()?;
321
322 Ok(total)
324}
325
326struct LdapArgs {
328 s_url: String,
329 _s_dc: Vec<String>,
330 _s_email: String,
331 s_username: String,
332 s_password: String,
333 s_ntlm_password: Option<String>,
334}
335
336#[allow(clippy::too_many_arguments)]
338fn ldap_constructor(
339 ldaps: bool,
340 ip: Option<&str>,
341 port: Option<u16>,
342 domain: &str,
343 ldapfqdn: Option<&str>,
344 username: Option<&str>,
345 password: Option<&str>,
346 hashes: Option<&str>,
347 kerberos: bool,
348 use_cert: bool,
350) -> Result<LdapArgs, Box<dyn Error>> {
351 let s_url = prepare_ldap_url(ldaps, ip, port, domain);
353
354 let s_dc = prepare_ldap_dc(domain);
356
357 let use_ntlm = hashes.is_some();
358
359 let mut s = String::new();
361 let mut _s_username: String;
362 if username.is_none() && !kerberos && !use_cert {
363 print!("Username: ");
364 io::stdout().flush()?;
365 stdin()
366 .read_line(&mut s)
367 .expect("Did not enter a correct username");
368 io::stdout().flush()?;
369 if let Some('\n') = s.chars().next_back() {
370 s.pop();
371 }
372 if let Some('\r') = s.chars().next_back() {
373 s.pop();
374 }
375 _s_username = s.to_owned();
376 } else {
377 _s_username = username.unwrap_or("not set").to_owned();
378 }
379
380 let mut s_email: String = "".to_owned();
382 if !_s_username.contains("@") {
383 s_email.push_str(&_s_username.to_string());
384 s_email.push_str("@");
385 s_email.push_str(domain);
386 if !use_ntlm {
387 _s_username = s_email.to_string();
388 }
389 } else {
390 s_email = _s_username.to_string().to_lowercase();
391 }
392
393 if use_ntlm && !_s_username.contains("\\") && !_s_username.contains("@") {
395 let domain_upper = domain.split('.').next().unwrap_or(domain).to_uppercase();
396 _s_username = format!("{}\\{}", domain_upper, _s_username);
397 }
398
399 let s_ntlm_password = match hashes {
401 Some(hash) => {
402 let clean = hash.trim();
403 let nt = match clean.split_once(':') {
405 Some((_lm, nt)) => nt,
406 None => clean,
407 };
408 if nt.len() != 32 || !nt.chars().all(|c| c.is_ascii_hexdigit()) {
409 error!("Invalid NT hash: must be exactly 32 hex characters (e.g. aad3b435b51404eeaad3b435b51404ee)");
410 process::exit(0x0100);
411 }
412 Some(nt_hash_to_ntlm_password(nt))
413 }
414 None => None,
415 };
416
417 let mut _s_password: String = String::new();
419 if !use_ntlm && !_s_username.contains("not set") && !kerberos && !use_cert {
420 _s_password = match password {
421 Some(p) => p.to_owned(),
422 None => rpassword::prompt_password("Password: ").unwrap_or("not set".to_string()),
423 };
424 } else {
425 _s_password = password.unwrap_or("not set").to_owned();
426 }
427
428 debug!("IP: {}", match ip {
430 Some(ip) => ip,
431 None => "not set"
432 });
433 debug!("PORT: {}", match port {
434 Some(p) => {
435 p.to_string()
436 },
437 None => "not set".to_owned()
438 });
439 debug!("FQDN: {}", ldapfqdn.unwrap_or("not set"));
440 debug!("Url: {}", s_url);
441 debug!("Domain: {}", domain);
442 debug!("Username: {}", _s_username);
443 debug!("Email: {}", s_email.to_lowercase());
444 if use_cert {
445 debug!("Auth: certificate (Pass-the-Certificate)");
446 } else if use_ntlm {
447 debug!("Auth: NTLM pass-the-hash");
448 } else {
449 debug!("Password: {}", _s_password);
450 }
451 debug!("DC: {:?}", s_dc);
452 debug!("Kerberos: {:?}", kerberos);
453
454 Ok(LdapArgs {
455 s_url: s_url.to_string(),
456 _s_dc: s_dc,
457 _s_email: s_email.to_string().to_lowercase(),
458 s_username: if use_ntlm {
459 _s_username.to_string()
460 } else {
461 s_email.to_string().to_lowercase()
462 },
463 s_password: _s_password.to_string(),
464 s_ntlm_password,
465 })
466}
467
468fn nt_hash_to_ntlm_password(hex_hash: &str) -> String {
471 let upper = hex_hash.to_uppercase();
472 let bytes = upper.as_bytes();
473
474 let mut password = String::new();
475
476 for pair in bytes.chunks(2) {
477 let low_byte = pair[0] as u32;
478 let high_byte = if pair.len() > 1 { pair[1] as u32 } else { 0 };
479 let code_point = (high_byte << 8) | low_byte;
480 password.push(char::from_u32(code_point).unwrap_or('\0'));
481 }
482
483 for _ in 0..256 {
485 password.push('\0');
486 }
487
488 password
489}
490
491fn prepare_ldap_url(
493 ldaps: bool,
494 ip: Option<&str>,
495 port: Option<u16>,
496 domain: &str
497) -> String {
498 let protocol = if ldaps || port.unwrap_or(0) == 636 {
499 "ldaps"
500 } else {
501 "ldap"
502 };
503
504 let target = match ip {
505 Some(ip) => ip,
506 None => domain,
507 };
508
509 match port {
510 Some(port) => {
511 format!("{protocol}://{target}:{port}")
512 }
513 None => {
514 format!("{protocol}://{target}")
515 }
516 }
517}
518
519pub fn prepare_ldap_dc(domain: &str) -> Vec<String> {
521
522 let mut dc: String = "".to_owned();
523 let mut naming_context: Vec<String> = Vec::new();
524
525 if !domain.contains(".") {
527 dc.push_str("DC=");
528 dc.push_str(domain);
529 naming_context.push(dc[..].to_string());
530 }
531 else {
532 naming_context.push(domain_to_dc(domain));
533 }
534
535 naming_context.push(format!("{}{}", "CN=Configuration,", &dc[..]));
537 naming_context
538}
539
540#[cfg(not(feature = "nogssapi"))]
542async fn gssapi_connection(
543 ldap: &mut ldap3::Ldap,
544 ldapfqdn: &str,
545 domain: &str,
546) -> Result<(), Box<dyn Error>> {
547 let res = ldap.sasl_gssapi_bind(ldapfqdn).await?.success();
548 match res {
549 Ok(_res) => {
550 info!("Connected to {} Active Directory!", domain.to_uppercase().bold().green());
551 info!("Starting data collection...");
552 }
553 Err(err) => {
554 error!("Failed to authenticate to {} Active Directory. Reason: {err}\n", domain.to_uppercase().bold().red());
555 process::exit(0x0100);
556 }
557 }
558 Ok(())
559}
560
561pub async fn get_all_naming_contexts(
563 ldap: &mut ldap3::Ldap
564) -> Result<Vec<String>, Box<dyn Error>> {
565 let adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
567 Box::new(EntriesOnly::new()),
568 Box::new(PagedResults::new(999)),
569 ];
570
571 let mut search = ldap.streaming_search_with(
573 adapters,
574 "",
575 Scope::Base,
576 "(objectClass=*)",
577 vec!["namingContexts"],
578 ).await?;
579
580 let mut rs: Vec<SearchEntry> = Vec::new();
582 while let Some(entry) = search.next().await? {
583 let entry = SearchEntry::construct(entry);
584 rs.push(entry);
585 }
586 let res = search.finish().await.success();
587
588 let mut naming_contexts: Vec<String> = Vec::new();
590 match res {
591 Ok(_res) => {
592 debug!("All namingContexts collected!");
593 for result in rs {
594 let result_attrs: HashMap<String, Vec<String>> = result.attrs;
595
596 for (_key, value) in &result_attrs {
597 for naming_context in value {
598 debug!("namingContext found: {}",&naming_context.bold().green());
599 naming_contexts.push(naming_context.to_string());
600 }
601 }
602 }
603
604 naming_contexts.sort_by_key(|cn| {
606 if cn.contains("CN=Schema") { 0 }
607 else if cn.to_lowercase().starts_with("dc=") { 1 }
608 else if cn.contains("CN=Configuration") { 2 }
609 else { 3 }
610 });
611
612 for (i, nc) in naming_contexts.iter().enumerate() {
614 trace!("NamingContext order [{}]: {}", i, nc);
615 }
616
617 return Ok(naming_contexts)
618 }
619 Err(err) => {
620 error!("No namingContexts found! Reason: {err}");
621 }
622 }
623 Ok(Vec::new())
625}
626
627#[derive(Debug, Clone, bincode::Encode, bincode::Decode)]
629pub struct LdapSearchEntry {
630 pub dn: String,
632 pub attrs: HashMap<String, Vec<String>>,
634 pub bin_attrs: HashMap<String, Vec<Vec<u8>>>,
636}
637
638impl From<SearchEntry> for LdapSearchEntry {
639 fn from(entry: SearchEntry) -> Self {
640 LdapSearchEntry {
641 dn: entry.dn,
642 attrs: entry.attrs,
643 bin_attrs: entry.bin_attrs,
644 }
645 }
646}
647
648impl From<LdapSearchEntry> for SearchEntry {
649 fn from(entry: LdapSearchEntry) -> Self {
650 SearchEntry {
651 dn: entry.dn,
652 attrs: entry.attrs,
653 bin_attrs: entry.bin_attrs,
654 }
655 }
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661
662 #[test]
663 fn nt_hash_encoding_roundtrip() {
664 let hash = "aad3b435b51404eeaad3b435b51404ee";
665 let password = nt_hash_to_ntlm_password(hash);
666
667 let utf16_bytes: Vec<u8> = password
668 .encode_utf16()
669 .flat_map(|u| u.to_le_bytes())
670 .collect();
671
672 assert!(utf16_bytes.len() > 512);
673
674 let hash_portion = &utf16_bytes[..utf16_bytes.len() - 512];
675 assert_eq!(hash_portion.len(), 32);
676
677 let expected_hex = hash.to_uppercase();
678 let expected_bytes = expected_hex.as_bytes();
679 assert_eq!(hash_portion, expected_bytes);
680 }
681
682 #[test]
683 fn nt_hash_encoding_all_zeros() {
684 let hash = "00000000000000000000000000000000";
685 let password = nt_hash_to_ntlm_password(hash);
686
687 let utf16_bytes: Vec<u8> = password
688 .encode_utf16()
689 .flat_map(|u| u.to_le_bytes())
690 .collect();
691
692 assert!(utf16_bytes.len() > 512);
693 let hash_portion = &utf16_bytes[..utf16_bytes.len() - 512];
694 assert_eq!(hash_portion, b"00000000000000000000000000000000");
695 }
696
697 #[test]
698 fn nt_hash_encoding_all_f() {
699 let hash = "ffffffffffffffffffffffffffffffff";
700 let password = nt_hash_to_ntlm_password(hash);
701
702 let utf16_bytes: Vec<u8> = password
703 .encode_utf16()
704 .flat_map(|u| u.to_le_bytes())
705 .collect();
706
707 assert!(utf16_bytes.len() > 512);
708 let hash_portion = &utf16_bytes[..utf16_bytes.len() - 512];
709 assert_eq!(hash_portion, b"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
710 }
711}