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::{adapters::PagedResults, controls::RawControl, LdapConnAsync, LdapConnSettings};
22use ldap3::{Scope, SearchEntry};
23use log::{info, debug, error, trace};
24use std::io::{self, Write, stdin};
25use std::collections::HashMap;
26use std::error::Error;
27use std::process;
28
29#[allow(clippy::too_many_arguments)]
31pub async fn ldap_search<S: Storage<LdapSearchEntry>>(
32 ldaps: bool,
33 ip: Option<&str>,
34 port: Option<u16>,
35 domain: &str,
36 ldapfqdn: Option<&str>,
37 username: Option<&str>,
38 password: Option<&str>,
39 hashes: Option<&str>,
40 kerberos: bool,
41 ldapfilter: &str,
42 storage: &mut S,
43) -> Result<usize, Box<dyn Error>> {
44 let ldap_args = ldap_constructor(
46 ldaps, ip, port, domain, ldapfqdn, username, password, hashes, kerberos,
47 )?;
48
49 let consettings = LdapConnSettings::new()
51 .set_conn_timeout(std::time::Duration::from_secs(10))
52 .set_no_tls_verify(true);
53 let (conn, mut ldap) = LdapConnAsync::with_settings(consettings, &ldap_args.s_url).await?;
54 ldap3::drive!(conn);
55
56 if let Some(ref ntlm_password) = ldap_args.s_ntlm_password {
57 debug!("Trying to connect with sasl_ntlm_bind() function (NTLM pass-the-hash)");
58 let res = ldap
59 .sasl_ntlm_bind(&ldap_args.s_username, ntlm_password)
60 .await?
61 .success();
62 match res {
63 Ok(_res) => {
64 info!(
65 "Connected to {} Active Directory via NTLM!",
66 domain.to_uppercase().bold().green()
67 );
68 info!("Starting data collection...");
69 }
70 Err(err) => {
71 error!(
72 "Failed to authenticate to {} Active Directory via NTLM. Reason: {err}\n",
73 domain.to_uppercase().bold().red()
74 );
75 process::exit(0x0100);
76 }
77 }
78 } else if !kerberos {
79 debug!("Trying to connect with simple_bind() function (username:password)");
80 let res = ldap
81 .simple_bind(&ldap_args.s_username, &ldap_args.s_password)
82 .await?
83 .success();
84 match res {
85 Ok(_res) => {
86 info!(
87 "Connected to {} Active Directory!",
88 domain.to_uppercase().bold().green()
89 );
90 info!("Starting data collection...");
91 }
92 Err(err) => {
93 error!(
94 "Failed to authenticate to {} Active Directory. Reason: {err}\n",
95 domain.to_uppercase().bold().red()
96 );
97 process::exit(0x0100);
98 }
99 }
100 } else {
101 debug!("Trying to connect with sasl_gssapi_bind() function (kerberos session)");
102 if let Some(fqdn) = ldapfqdn.filter(|f| !f.is_empty()) {
103 #[cfg(not(feature = "nogssapi"))]
104 gssapi_connection(&mut ldap, fqdn, &domain).await?;
105 #[cfg(feature = "nogssapi")]
106 {
107 error!("Kerberos auth and GSSAPI not compatible with current os!");
108 process::exit(0x0100);
109 }
110 } else {
111 error!(
112 "Need Domain Controller FQDN to bind GSSAPI connection. Please use '{}'\n",
113 "-f DC01.DOMAIN.LAB".bold()
114 );
115 process::exit(0x0100);
116 }
117 }
118
119 let mut total = 0; let res = match get_all_naming_contexts(&mut ldap).await {
124 Ok(res) => {
125 trace!("naming_contexts: {:?}", &res);
126 res
127 }
128 Err(err) => {
129 error!("No namingContexts found! Reason: {err}\n");
130 process::exit(0x0100);
131 }
132 };
133
134 if res.iter().any(|s| s.contains("Configuration")) {
137 for cn in &res {
138 let sd_flags = RawControl {
141 ctype: String::from("1.2.840.113556.1.4.801"),
142 crit: true,
143 val: Some(vec![48, 3, 2, 1, 5]), };
145
146 let show_deleted = RawControl {
149 ctype: String::from("1.2.840.113556.1.4.417"),
150 crit: false, val: None,
152 };
153
154 let controls = vec![sd_flags, show_deleted];
155 ldap.with_controls(controls.to_owned());
156
157 info!("Ldap filter : {}", ldapfilter.bold().green());
167 let _s_filter = ldapfilter;
168
169 let adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
171 Box::new(EntriesOnly::new()),
172 Box::new(PagedResults::new(999)),
173 ];
174
175 let mut search = ldap
177 .streaming_search_with(
178 adapters, cn,
180 Scope::Subtree,
181 _s_filter,
182 vec!["*", "nTSecurityDescriptor"],
183 )
186 .await?;
187
188 let pb = ProgressBar::new(1);
190 let mut count = 0;
191 while let Some(entry) = search.next().await? {
192 let entry = SearchEntry::construct(entry);
193 total += 1;
195 count += 1;
197 progress_bar(
198 pb.to_owned(),
199 "LDAP objects retrieved".to_string(),
200 count,
201 "#".to_string(),
202 );
203
204 storage.add(entry.into())?;
205 }
206 pb.finish_and_clear();
207
208 let res = search.finish().await.success();
209 match res {
210 Ok(_res) => info!("All data collected for NamingContext {}", &cn.bold()),
211 Err(err) => {
212 error!("No data collected on {}! Reason: {err}", &cn.bold().red());
213 }
214 }
215 }
216 ldap.unbind().await?;
222 }
223
224 drop(ldap);
228 if total == 0 {
229 error!("No LDAP objects found! Exiting...");
230 process::exit(0x0100);
232 }
233
234 storage.flush()?;
235
236
237 Ok(total)
239}
240
241struct LdapArgs {
243 s_url: String,
244 _s_dc: Vec<String>,
245 _s_email: String,
246 s_username: String,
247 s_password: String,
248 s_ntlm_password: Option<String>,
249}
250
251fn ldap_constructor(
253 ldaps: bool,
254 ip: Option<&str>,
255 port: Option<u16>,
256 domain: &str,
257 ldapfqdn: Option<&str>,
258 username: Option<&str>,
259 password: Option<&str>,
260 hashes: Option<&str>,
261 kerberos: bool,
262) -> Result<LdapArgs, Box<dyn Error>> {
263 let s_url = prepare_ldap_url(ldaps, ip, port, domain);
265
266 let s_dc = prepare_ldap_dc(domain);
268
269 let use_ntlm = hashes.is_some();
270
271 let mut s = String::new();
273 let mut _s_username: String;
274 if username.is_none() && !kerberos {
275 print!("Username: ");
276 io::stdout().flush()?;
277 stdin()
278 .read_line(&mut s)
279 .expect("Did not enter a correct username");
280 io::stdout().flush()?;
281 if let Some('\n') = s.chars().next_back() {
282 s.pop();
283 }
284 if let Some('\r') = s.chars().next_back() {
285 s.pop();
286 }
287 _s_username = s.to_owned();
288 } else {
289 _s_username = username.unwrap_or("not set").to_owned();
290 }
291
292 let mut s_email: String = "".to_owned();
294 if !_s_username.contains("@") {
295 s_email.push_str(&_s_username.to_string());
296 s_email.push_str("@");
297 s_email.push_str(domain);
298 if !use_ntlm {
299 _s_username = s_email.to_string();
300 }
301 } else {
302 s_email = _s_username.to_string().to_lowercase();
303 }
304
305 if use_ntlm && !_s_username.contains("\\") && !_s_username.contains("@") {
307 let domain_upper = domain.split('.').next().unwrap_or(domain).to_uppercase();
308 _s_username = format!("{}\\{}", domain_upper, _s_username);
309 }
310
311 let s_ntlm_password = match hashes {
313 Some(hash) => {
314 let clean = hash.trim();
315 let nt = match clean.split_once(':') {
317 Some((_lm, nt)) => nt,
318 None => clean,
319 };
320 if nt.len() != 32 || !nt.chars().all(|c| c.is_ascii_hexdigit()) {
321 error!("Invalid NT hash: must be exactly 32 hex characters (e.g. aad3b435b51404eeaad3b435b51404ee)");
322 process::exit(0x0100);
323 }
324 Some(nt_hash_to_ntlm_password(nt))
325 }
326 None => None,
327 };
328
329 let mut _s_password: String = String::new();
331 if !use_ntlm && !_s_username.contains("not set") && !kerberos {
332 _s_password = match password {
333 Some(p) => p.to_owned(),
334 None => rpassword::prompt_password("Password: ").unwrap_or("not set".to_string()),
335 };
336 } else {
337 _s_password = password.unwrap_or("not set").to_owned();
338 }
339
340 debug!("IP: {}", match ip {
342 Some(ip) => ip,
343 None => "not set"
344 });
345 debug!("PORT: {}", match port {
346 Some(p) => {
347 p.to_string()
348 },
349 None => "not set".to_owned()
350 });
351 debug!("FQDN: {}", ldapfqdn.unwrap_or("not set"));
352 debug!("Url: {}", s_url);
353 debug!("Domain: {}", domain);
354 debug!("Username: {}", _s_username);
355 debug!("Email: {}", s_email.to_lowercase());
356 if use_ntlm {
357 debug!("Auth: NTLM pass-the-hash");
358 } else {
359 debug!("Password: {}", _s_password);
360 }
361 debug!("DC: {:?}", s_dc);
362 debug!("Kerberos: {:?}", kerberos);
363
364 Ok(LdapArgs {
365 s_url: s_url.to_string(),
366 _s_dc: s_dc,
367 _s_email: s_email.to_string().to_lowercase(),
368 s_username: if use_ntlm {
369 _s_username.to_string()
370 } else {
371 s_email.to_string().to_lowercase()
372 },
373 s_password: _s_password.to_string(),
374 s_ntlm_password,
375 })
376}
377
378fn nt_hash_to_ntlm_password(hex_hash: &str) -> String {
381 let upper = hex_hash.to_uppercase();
382 let bytes = upper.as_bytes();
383
384 let mut password = String::new();
385
386 for pair in bytes.chunks(2) {
387 let low_byte = pair[0] as u32;
388 let high_byte = if pair.len() > 1 { pair[1] as u32 } else { 0 };
389 let code_point = (high_byte << 8) | low_byte;
390 password.push(char::from_u32(code_point).unwrap_or('\0'));
391 }
392
393 for _ in 0..256 {
395 password.push('\0');
396 }
397
398 password
399}
400
401fn prepare_ldap_url(
403 ldaps: bool,
404 ip: Option<&str>,
405 port: Option<u16>,
406 domain: &str
407) -> String {
408 let protocol = if ldaps || port.unwrap_or(0) == 636 {
409 "ldaps"
410 } else {
411 "ldap"
412 };
413
414 let target = match ip {
415 Some(ip) => ip,
416 None => domain,
417 };
418
419 match port {
420 Some(port) => {
421 format!("{protocol}://{target}:{port}")
422 }
423 None => {
424 format!("{protocol}://{target}")
425 }
426 }
427}
428
429pub fn prepare_ldap_dc(domain: &str) -> Vec<String> {
431
432 let mut dc: String = "".to_owned();
433 let mut naming_context: Vec<String> = Vec::new();
434
435 if !domain.contains(".") {
437 dc.push_str("DC=");
438 dc.push_str(domain);
439 naming_context.push(dc[..].to_string());
440 }
441 else {
442 naming_context.push(domain_to_dc(domain));
443 }
444
445 naming_context.push(format!("{}{}", "CN=Configuration,", &dc[..]));
447 naming_context
448}
449
450#[cfg(not(feature = "nogssapi"))]
452async fn gssapi_connection(
453 ldap: &mut ldap3::Ldap,
454 ldapfqdn: &str,
455 domain: &str,
456) -> Result<(), Box<dyn Error>> {
457 let res = ldap.sasl_gssapi_bind(ldapfqdn).await?.success();
458 match res {
459 Ok(_res) => {
460 info!("Connected to {} Active Directory!", domain.to_uppercase().bold().green());
461 info!("Starting data collection...");
462 }
463 Err(err) => {
464 error!("Failed to authenticate to {} Active Directory. Reason: {err}\n", domain.to_uppercase().bold().red());
465 process::exit(0x0100);
466 }
467 }
468 Ok(())
469}
470
471pub async fn get_all_naming_contexts(
473 ldap: &mut ldap3::Ldap
474) -> Result<Vec<String>, Box<dyn Error>> {
475 let adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
477 Box::new(EntriesOnly::new()),
478 Box::new(PagedResults::new(999)),
479 ];
480
481 let mut search = ldap.streaming_search_with(
483 adapters,
484 "",
485 Scope::Base,
486 "(objectClass=*)",
487 vec!["namingContexts"],
488 ).await?;
489
490 let mut rs: Vec<SearchEntry> = Vec::new();
492 while let Some(entry) = search.next().await? {
493 let entry = SearchEntry::construct(entry);
494 rs.push(entry);
495 }
496 let res = search.finish().await.success();
497
498 let mut naming_contexts: Vec<String> = Vec::new();
500 match res {
501 Ok(_res) => {
502 debug!("All namingContexts collected!");
503 for result in rs {
504 let result_attrs: HashMap<String, Vec<String>> = result.attrs;
505
506 for (_key, value) in &result_attrs {
507 for naming_context in value {
508 debug!("namingContext found: {}",&naming_context.bold().green());
509 naming_contexts.push(naming_context.to_string());
510 }
511 }
512 }
513
514 naming_contexts.sort_by_key(|cn| {
516 if cn.contains("CN=Schema") { 0 }
517 else if cn.to_lowercase().starts_with("dc=") { 1 }
518 else if cn.contains("CN=Configuration") { 2 }
519 else { 3 }
520 });
521
522 for (i, nc) in naming_contexts.iter().enumerate() {
524 trace!("NamingContext order [{}]: {}", i, nc);
525 }
526
527 return Ok(naming_contexts)
528 }
529 Err(err) => {
530 error!("No namingContexts found! Reason: {err}");
531 }
532 }
533 Ok(Vec::new())
535}
536
537#[derive(Debug, Clone, bincode::Encode, bincode::Decode)]
539pub struct LdapSearchEntry {
540 pub dn: String,
542 pub attrs: HashMap<String, Vec<String>>,
544 pub bin_attrs: HashMap<String, Vec<Vec<u8>>>,
546}
547
548impl From<SearchEntry> for LdapSearchEntry {
549 fn from(entry: SearchEntry) -> Self {
550 LdapSearchEntry {
551 dn: entry.dn,
552 attrs: entry.attrs,
553 bin_attrs: entry.bin_attrs,
554 }
555 }
556}
557
558impl From<LdapSearchEntry> for SearchEntry {
559 fn from(entry: LdapSearchEntry) -> Self {
560 SearchEntry {
561 dn: entry.dn,
562 attrs: entry.attrs,
563 bin_attrs: entry.bin_attrs,
564 }
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571
572 #[test]
573 fn nt_hash_encoding_roundtrip() {
574 let hash = "aad3b435b51404eeaad3b435b51404ee";
575 let password = nt_hash_to_ntlm_password(hash);
576
577 let utf16_bytes: Vec<u8> = password
578 .encode_utf16()
579 .flat_map(|u| u.to_le_bytes())
580 .collect();
581
582 assert!(utf16_bytes.len() > 512);
583
584 let hash_portion = &utf16_bytes[..utf16_bytes.len() - 512];
585 assert_eq!(hash_portion.len(), 32);
586
587 let expected_hex = hash.to_uppercase();
588 let expected_bytes = expected_hex.as_bytes();
589 assert_eq!(hash_portion, expected_bytes);
590 }
591
592 #[test]
593 fn nt_hash_encoding_all_zeros() {
594 let hash = "00000000000000000000000000000000";
595 let password = nt_hash_to_ntlm_password(hash);
596
597 let utf16_bytes: Vec<u8> = password
598 .encode_utf16()
599 .flat_map(|u| u.to_le_bytes())
600 .collect();
601
602 assert!(utf16_bytes.len() > 512);
603 let hash_portion = &utf16_bytes[..utf16_bytes.len() - 512];
604 assert_eq!(hash_portion, b"00000000000000000000000000000000");
605 }
606
607 #[test]
608 fn nt_hash_encoding_all_f() {
609 let hash = "ffffffffffffffffffffffffffffffff";
610 let password = nt_hash_to_ntlm_password(hash);
611
612 let utf16_bytes: Vec<u8> = password
613 .encode_utf16()
614 .flat_map(|u| u.to_le_bytes())
615 .collect();
616
617 assert!(utf16_bytes.len() > 512);
618 let hash_portion = &utf16_bytes[..utf16_bytes.len() - 512];
619 assert_eq!(hash_portion, b"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
620 }
621}