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: &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 !&ldapfqdn.contains("not set") {
103 #[cfg(not(feature = "nogssapi"))]
104 gssapi_connection(&mut ldap, &ldapfqdn, &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 ctrls = RawControl {
141 ctype: String::from("1.2.840.113556.1.4.801"),
142 crit: true,
143 val: Some(vec![48, 3, 2, 1, 5]),
144 };
145 ldap.with_controls(ctrls.to_owned());
146
147 info!("Ldap filter : {}", ldapfilter.bold().green());
157 let _s_filter = ldapfilter;
158
159 let adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
161 Box::new(EntriesOnly::new()),
162 Box::new(PagedResults::new(999)),
163 ];
164
165 let mut search = ldap
167 .streaming_search_with(
168 adapters, cn,
170 Scope::Subtree,
171 _s_filter,
172 vec!["*", "nTSecurityDescriptor"],
173 )
176 .await?;
177
178 let pb = ProgressBar::new(1);
180 let mut count = 0;
181 while let Some(entry) = search.next().await? {
182 let entry = SearchEntry::construct(entry);
183 total += 1;
185 count += 1;
187 progress_bar(
188 pb.to_owned(),
189 "LDAP objects retrieved".to_string(),
190 count,
191 "#".to_string(),
192 );
193
194 storage.add(entry.into())?;
195 }
196 pb.finish_and_clear();
197
198 let res = search.finish().await.success();
199 match res {
200 Ok(_res) => info!("All data collected for NamingContext {}", &cn.bold()),
201 Err(err) => {
202 error!("No data collected on {}! Reason: {err}", &cn.bold().red());
203 }
204 }
205 }
206 ldap.unbind().await?;
212 }
213
214 drop(ldap);
218 if total == 0 {
219 error!("No LDAP objects found! Exiting...");
220 process::exit(0x0100);
222 }
223
224 storage.flush()?;
225
226
227 Ok(total)
229}
230
231struct LdapArgs {
233 s_url: String,
234 _s_dc: Vec<String>,
235 _s_email: String,
236 s_username: String,
237 s_password: String,
238 s_ntlm_password: Option<String>,
239}
240
241fn ldap_constructor(
243 ldaps: bool,
244 ip: Option<&str>,
245 port: Option<u16>,
246 domain: &str,
247 ldapfqdn: &str,
248 username: Option<&str>,
249 password: Option<&str>,
250 hashes: Option<&str>,
251 kerberos: bool,
252) -> Result<LdapArgs, Box<dyn Error>> {
253 let s_url = prepare_ldap_url(ldaps, ip, port, domain);
255
256 let s_dc = prepare_ldap_dc(domain);
258
259 let use_ntlm = hashes.is_some();
260
261 let mut s = String::new();
263 let mut _s_username: String;
264 if username.is_none() && !kerberos {
265 print!("Username: ");
266 io::stdout().flush()?;
267 stdin()
268 .read_line(&mut s)
269 .expect("Did not enter a correct username");
270 io::stdout().flush()?;
271 if let Some('\n') = s.chars().next_back() {
272 s.pop();
273 }
274 if let Some('\r') = s.chars().next_back() {
275 s.pop();
276 }
277 _s_username = s.to_owned();
278 } else {
279 _s_username = username.unwrap_or("not set").to_owned();
280 }
281
282 let mut s_email: String = "".to_owned();
284 if !_s_username.contains("@") {
285 s_email.push_str(&_s_username.to_string());
286 s_email.push_str("@");
287 s_email.push_str(domain);
288 if !use_ntlm {
289 _s_username = s_email.to_string();
290 }
291 } else {
292 s_email = _s_username.to_string().to_lowercase();
293 }
294
295 if use_ntlm && !_s_username.contains("\\") && !_s_username.contains("@") {
297 let domain_upper = domain.split('.').next().unwrap_or(domain).to_uppercase();
298 _s_username = format!("{}\\{}", domain_upper, _s_username);
299 }
300
301 let s_ntlm_password = match hashes {
303 Some(hash) => {
304 let clean = hash.trim();
305 let nt = match clean.split_once(':') {
307 Some((_lm, nt)) => nt,
308 None => clean,
309 };
310 if nt.len() != 32 || !nt.chars().all(|c| c.is_ascii_hexdigit()) {
311 error!("Invalid NT hash: must be exactly 32 hex characters (e.g. aad3b435b51404eeaad3b435b51404ee)");
312 process::exit(0x0100);
313 }
314 Some(nt_hash_to_ntlm_password(nt))
315 }
316 None => None,
317 };
318
319 let mut _s_password: String = String::new();
321 if !use_ntlm && !_s_username.contains("not set") && !kerberos {
322 _s_password = match password {
323 Some(p) => p.to_owned(),
324 None => rpassword::prompt_password("Password: ").unwrap_or("not set".to_string()),
325 };
326 } else {
327 _s_password = password.unwrap_or("not set").to_owned();
328 }
329
330 debug!("IP: {}", match ip {
332 Some(ip) => ip,
333 None => "not set"
334 });
335 debug!("PORT: {}", match port {
336 Some(p) => {
337 p.to_string()
338 },
339 None => "not set".to_owned()
340 });
341 debug!("FQDN: {}", ldapfqdn);
342 debug!("Url: {}", s_url);
343 debug!("Domain: {}", domain);
344 debug!("Username: {}", _s_username);
345 debug!("Email: {}", s_email.to_lowercase());
346 if use_ntlm {
347 debug!("Auth: NTLM pass-the-hash");
348 } else {
349 debug!("Password: {}", _s_password);
350 }
351 debug!("DC: {:?}", s_dc);
352 debug!("Kerberos: {:?}", kerberos);
353
354 Ok(LdapArgs {
355 s_url: s_url.to_string(),
356 _s_dc: s_dc,
357 _s_email: s_email.to_string().to_lowercase(),
358 s_username: if use_ntlm {
359 _s_username.to_string()
360 } else {
361 s_email.to_string().to_lowercase()
362 },
363 s_password: _s_password.to_string(),
364 s_ntlm_password,
365 })
366}
367
368fn nt_hash_to_ntlm_password(hex_hash: &str) -> String {
371 let upper = hex_hash.to_uppercase();
372 let bytes = upper.as_bytes();
373
374 let mut password = String::new();
375
376 for pair in bytes.chunks(2) {
377 let low_byte = pair[0] as u32;
378 let high_byte = if pair.len() > 1 { pair[1] as u32 } else { 0 };
379 let code_point = (high_byte << 8) | low_byte;
380 password.push(char::from_u32(code_point).unwrap_or('\0'));
381 }
382
383 for _ in 0..256 {
385 password.push('\0');
386 }
387
388 password
389}
390
391fn prepare_ldap_url(
393 ldaps: bool,
394 ip: Option<&str>,
395 port: Option<u16>,
396 domain: &str
397) -> String {
398 let protocol = if ldaps || port.unwrap_or(0) == 636 {
399 "ldaps"
400 } else {
401 "ldap"
402 };
403
404 let target = match ip {
405 Some(ip) => ip,
406 None => domain,
407 };
408
409 match port {
410 Some(port) => {
411 format!("{protocol}://{target}:{port}")
412 }
413 None => {
414 format!("{protocol}://{target}")
415 }
416 }
417}
418
419pub fn prepare_ldap_dc(domain: &str) -> Vec<String> {
421
422 let mut dc: String = "".to_owned();
423 let mut naming_context: Vec<String> = Vec::new();
424
425 if !domain.contains(".") {
427 dc.push_str("DC=");
428 dc.push_str(domain);
429 naming_context.push(dc[..].to_string());
430 }
431 else {
432 naming_context.push(domain_to_dc(domain));
433 }
434
435 naming_context.push(format!("{}{}", "CN=Configuration,", &dc[..]));
437 naming_context
438}
439
440#[cfg(not(feature = "nogssapi"))]
442async fn gssapi_connection(
443 ldap: &mut ldap3::Ldap,
444 ldapfqdn: &str,
445 domain: &str,
446) -> Result<(), Box<dyn Error>> {
447 let res = ldap.sasl_gssapi_bind(ldapfqdn).await?.success();
448 match res {
449 Ok(_res) => {
450 info!("Connected to {} Active Directory!", domain.to_uppercase().bold().green());
451 info!("Starting data collection...");
452 }
453 Err(err) => {
454 error!("Failed to authenticate to {} Active Directory. Reason: {err}\n", domain.to_uppercase().bold().red());
455 process::exit(0x0100);
456 }
457 }
458 Ok(())
459}
460
461pub async fn get_all_naming_contexts(
463 ldap: &mut ldap3::Ldap
464) -> Result<Vec<String>, Box<dyn Error>> {
465 let adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
467 Box::new(EntriesOnly::new()),
468 Box::new(PagedResults::new(999)),
469 ];
470
471 let mut search = ldap.streaming_search_with(
473 adapters,
474 "",
475 Scope::Base,
476 "(objectClass=*)",
477 vec!["namingContexts"],
478 ).await?;
479
480 let mut rs: Vec<SearchEntry> = Vec::new();
482 while let Some(entry) = search.next().await? {
483 let entry = SearchEntry::construct(entry);
484 rs.push(entry);
485 }
486 let res = search.finish().await.success();
487
488 let mut naming_contexts: Vec<String> = Vec::new();
490 match res {
491 Ok(_res) => {
492 debug!("All namingContexts collected!");
493 for result in rs {
494 let result_attrs: HashMap<String, Vec<String>> = result.attrs;
495
496 for (_key, value) in &result_attrs {
497 for naming_context in value {
498 debug!("namingContext found: {}",&naming_context.bold().green());
499 naming_contexts.push(naming_context.to_string());
500 }
501 }
502 }
503
504 naming_contexts.sort_by_key(|cn| {
506 if cn.contains("CN=Schema") { 0 }
507 else if cn.to_lowercase().starts_with("dc=") { 1 }
508 else if cn.contains("CN=Configuration") { 2 }
509 else { 3 }
510 });
511
512 for (i, nc) in naming_contexts.iter().enumerate() {
514 trace!("NamingContext order [{}]: {}", i, nc);
515 }
516
517 return Ok(naming_contexts)
518 }
519 Err(err) => {
520 error!("No namingContexts found! Reason: {err}");
521 }
522 }
523 Ok(Vec::new())
525}
526
527#[derive(Debug, Clone, bincode::Encode, bincode::Decode)]
529pub struct LdapSearchEntry {
530 pub dn: String,
532 pub attrs: HashMap<String, Vec<String>>,
534 pub bin_attrs: HashMap<String, Vec<Vec<u8>>>,
536}
537
538impl From<SearchEntry> for LdapSearchEntry {
539 fn from(entry: SearchEntry) -> Self {
540 LdapSearchEntry {
541 dn: entry.dn,
542 attrs: entry.attrs,
543 bin_attrs: entry.bin_attrs,
544 }
545 }
546}
547
548impl From<LdapSearchEntry> for SearchEntry {
549 fn from(entry: LdapSearchEntry) -> Self {
550 SearchEntry {
551 dn: entry.dn,
552 attrs: entry.attrs,
553 bin_attrs: entry.bin_attrs,
554 }
555 }
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561
562 #[test]
563 fn nt_hash_encoding_roundtrip() {
564 let hash = "aad3b435b51404eeaad3b435b51404ee";
565 let password = nt_hash_to_ntlm_password(hash);
566
567 let utf16_bytes: Vec<u8> = password
568 .encode_utf16()
569 .flat_map(|u| u.to_le_bytes())
570 .collect();
571
572 assert!(utf16_bytes.len() > 512);
573
574 let hash_portion = &utf16_bytes[..utf16_bytes.len() - 512];
575 assert_eq!(hash_portion.len(), 32);
576
577 let expected_hex = hash.to_uppercase();
578 let expected_bytes = expected_hex.as_bytes();
579 assert_eq!(hash_portion, expected_bytes);
580 }
581
582 #[test]
583 fn nt_hash_encoding_all_zeros() {
584 let hash = "00000000000000000000000000000000";
585 let password = nt_hash_to_ntlm_password(hash);
586
587 let utf16_bytes: Vec<u8> = password
588 .encode_utf16()
589 .flat_map(|u| u.to_le_bytes())
590 .collect();
591
592 assert!(utf16_bytes.len() > 512);
593 let hash_portion = &utf16_bytes[..utf16_bytes.len() - 512];
594 assert_eq!(hash_portion, b"00000000000000000000000000000000");
595 }
596
597 #[test]
598 fn nt_hash_encoding_all_f() {
599 let hash = "ffffffffffffffffffffffffffffffff";
600 let password = nt_hash_to_ntlm_password(hash);
601
602 let utf16_bytes: Vec<u8> = password
603 .encode_utf16()
604 .flat_map(|u| u.to_le_bytes())
605 .collect();
606
607 assert!(utf16_bytes.len() > 512);
608 let hash_portion = &utf16_bytes[..utf16_bytes.len() - 512];
609 assert_eq!(hash_portion, b"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
610 }
611}