1use crate::args::Options;
11use crate::banner::progress_bar;
12use crate::storage::Storage;
13use crate::utils::format::domain_to_dc;
14
15use colored::Colorize;
16use indicatif::ProgressBar;
17use ldap3::adapters::{Adapter, EntriesOnly};
18use ldap3::exop::WhoAmI;
19use ldap3::{adapters::PagedResults, controls::RawControl, LdapConnAsync, LdapConnSettings};
20use ldap3::{Scope, SearchEntry};
21use log::{info, debug, error, trace};
22use std::io::{self, Write, stdin};
23use std::collections::HashMap;
24use std::error::Error;
25
26pub async fn ldap_auth(options: &Options) -> Result<ldap3::Ldap, Box<dyn Error>> {
38 let use_cert = options.pfx.is_some() || options.crt.is_some();
39
40 let starttls = use_cert && !options.ldaps;
42 let effective_ldaps = if use_cert { !starttls } else { options.ldaps };
43 let effective_port = if use_cert {
44 options.port.or(Some(if starttls { 389 } else { 636 }))
45 } else {
46 options.port
47 };
48
49 let ldap_args = ldap_constructor(
50 effective_ldaps,
51 options.ip.as_deref(),
52 effective_port,
53 &options.domain,
54 options.ldapfqdn.as_deref(),
55 options.username.as_deref(),
56 options.password.as_deref(),
57 options.hashes.as_deref(),
58 options.kerberos,
59 use_cert,
60 )?;
61
62 let mut consettings = LdapConnSettings::new()
63 .set_conn_timeout(std::time::Duration::from_secs(10))
64 .set_no_tls_verify(true);
65 if use_cert {
66 let config = crate::transport::cert::build_client_config(
67 options.pfx.as_deref(),
68 options.pfx_pass.as_deref(),
69 options.crt.as_deref(),
70 options.key.as_deref(),
71 )?;
72 consettings = consettings.set_config(config);
73 if starttls {
74 consettings = consettings.set_starttls(true);
75 }
76 }
77
78 let (conn, mut ldap) = LdapConnAsync::with_settings(consettings, &ldap_args.s_url).await?;
79 ldap3::drive!(conn);
80
81 let domain = &options.domain;
82
83 if use_cert {
84 if starttls {
87 debug!("Certificate authentication (StartTLS + SASL EXTERNAL)");
88 ldap.sasl_external_bind()
89 .await
90 .and_then(|r| r.success())
91 .map_err(|e| format!("certificate SASL EXTERNAL bind failed (try --ldaps): {e}"))?;
92 } else {
93 debug!("Certificate authentication (LDAPS, implicit Schannel mapping)");
94 }
95 let who = ldap
96 .extended(WhoAmI)
97 .await
98 .map(|r| r.success())
99 .map_err(|e| format!("certificate whoami request failed: {e}"))?
100 .map_err(|e| format!("certificate whoami failed: {e}"))?
101 .0
102 .val
103 .as_ref()
104 .map(|v| String::from_utf8_lossy(v).to_string())
105 .unwrap_or_default();
106 if who.is_empty() {
107 return Err(format!(
108 "certificate not mapped by {} (empty whoami); check the cert SID and DC enforcement (KB5014754)",
109 domain.to_uppercase()
110 )
111 .into());
112 }
113 info!(
114 "Connected to {} Active Directory via certificate as {}!",
115 domain.to_uppercase().bold().green(),
116 who.bold().green()
117 );
118 } else if let Some(ref ntlm_password) = ldap_args.s_ntlm_password {
119 debug!("NTLM pass-the-hash (sasl_ntlm_bind)");
120 ldap.sasl_ntlm_bind(&ldap_args.s_username, ntlm_password)
121 .await?
122 .success()
123 .map_err(|e| format!("NTLM authentication to {} failed: {e}", domain.to_uppercase()))?;
124 info!("Connected to {} Active Directory via NTLM!", domain.to_uppercase().bold().green());
125 } else if !options.kerberos {
126 debug!("Simple bind (username:password)");
127 ldap.simple_bind(&ldap_args.s_username, &ldap_args.s_password)
128 .await?
129 .success()
130 .map_err(|e| format!("authentication to {} failed: {e}", domain.to_uppercase()))?;
131 info!("Connected to {} Active Directory!", domain.to_uppercase().bold().green());
132 } else {
133 debug!("Kerberos (sasl_gssapi_bind)");
134 let fqdn = options
135 .ldapfqdn
136 .as_deref()
137 .filter(|f| !f.is_empty())
138 .ok_or("Kerberos requires the Domain Controller FQDN (set options.ldapfqdn, e.g. DC01.DOMAIN.LOCAL)")?;
139 #[cfg(not(feature = "nogssapi"))]
140 {
141 gssapi_connection(&mut ldap, fqdn, domain).await?;
142 }
143 #[cfg(feature = "nogssapi")]
144 {
145 let _ = fqdn;
146 return Err("Kerberos/GSSAPI is not available in this build (nogssapi feature)".into());
147 }
148 }
149
150 Ok(ldap)
151}
152
153pub(crate) async fn collect_from_ldap_into<S: Storage<LdapSearchEntry>>(
158 ldap: &mut ldap3::Ldap,
159 ldapfilter: &str,
160 storage: &mut S,
161) -> Result<usize, Box<dyn Error>> {
162 let mut total = 0usize;
163
164 let res = get_all_naming_contexts(ldap).await?;
165 trace!("naming_contexts: {:?}", &res);
166
167 if !res.iter().any(|s| s.contains("Configuration")) {
168 return Err("no Configuration namingContext found (is the target a Domain Controller?)".into());
169 }
170
171 for cn in &res {
172 let sd_flags = RawControl {
174 ctype: String::from("1.2.840.113556.1.4.801"),
175 crit: true,
176 val: Some(vec![48, 3, 2, 1, 5]), };
178 let show_deleted = RawControl {
180 ctype: String::from("1.2.840.113556.1.4.417"),
181 crit: false,
182 val: None,
183 };
184 ldap.with_controls(vec![sd_flags, show_deleted]);
185
186 info!("Ldap filter : {}", ldapfilter.bold().green());
187
188 let adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
189 Box::new(EntriesOnly::new()),
190 Box::new(PagedResults::new(999)),
191 ];
192
193 let mut search = ldap
194 .streaming_search_with(
195 adapters,
196 cn,
197 Scope::Subtree,
198 ldapfilter,
199 vec!["*", "nTSecurityDescriptor", "msDS-User-Account-Control-Computed"],
200 )
201 .await?;
202
203 let pb = ProgressBar::new(1);
204 let mut count = 0;
205 while let Some(entry) = search.next().await? {
206 let entry = SearchEntry::construct(entry);
207 total += 1;
208 count += 1;
209 progress_bar(
210 pb.to_owned(),
211 "LDAP objects retrieved".to_string(),
212 count,
213 "#".to_string(),
214 );
215 storage.add(entry.into())?;
216 }
217 pb.finish_and_clear();
218
219 match search.finish().await.success() {
220 Ok(_) => info!("All data collected for NamingContext {}", &cn.bold()),
221 Err(err) => error!("No data collected on {}! Reason: {err}", &cn.bold().red()),
222 }
223 }
224
225 storage.flush()?;
226 Ok(total)
227}
228
229struct LdapArgs {
231 s_url: String,
232 _s_dc: Vec<String>,
233 _s_email: String,
234 s_username: String,
235 s_password: String,
236 s_ntlm_password: Option<String>,
237}
238
239#[allow(clippy::too_many_arguments)]
241fn ldap_constructor(
242 ldaps: bool,
243 ip: Option<&str>,
244 port: Option<u16>,
245 domain: &str,
246 ldapfqdn: Option<&str>,
247 username: Option<&str>,
248 password: Option<&str>,
249 hashes: Option<&str>,
250 kerberos: bool,
251 use_cert: bool,
252) -> Result<LdapArgs, Box<dyn Error>> {
253 let s_url = prepare_ldap_url(ldaps, ip, port, domain);
254 let s_dc = prepare_ldap_dc(domain);
255 let use_ntlm = hashes.is_some();
256
257 let mut s = String::new();
259 let mut _s_username: String;
260 if username.is_none() && !kerberos && !use_cert {
261 print!("Username: ");
262 io::stdout().flush()?;
263 stdin().read_line(&mut s).expect("Did not enter a correct username");
264 io::stdout().flush()?;
265 if let Some('\n') = s.chars().next_back() { s.pop(); }
266 if let Some('\r') = s.chars().next_back() { s.pop(); }
267 _s_username = s.to_owned();
268 } else {
269 _s_username = username.unwrap_or("not set").to_owned();
270 }
271
272 let mut s_email: String = "".to_owned();
274 if !_s_username.contains("@") {
275 s_email.push_str(&_s_username.to_string());
276 s_email.push_str("@");
277 s_email.push_str(domain);
278 if !use_ntlm {
279 _s_username = s_email.to_string();
280 }
281 } else {
282 s_email = _s_username.to_string().to_lowercase();
283 }
284
285 if use_ntlm && !_s_username.contains("\\") && !_s_username.contains("@") {
287 let domain_upper = domain.split('.').next().unwrap_or(domain).to_uppercase();
288 _s_username = format!("{}\\{}", domain_upper, _s_username);
289 }
290
291 let s_ntlm_password = match hashes {
293 Some(hash) => {
294 let clean = hash.trim();
295 let nt = match clean.split_once(':') {
296 Some((_lm, nt)) => nt,
297 None => clean,
298 };
299 if nt.len() != 32 || !nt.chars().all(|c| c.is_ascii_hexdigit()) {
300 return Err("Invalid NT hash: must be exactly 32 hex characters (e.g. aad3b435b51404eeaad3b435b51404ee)".into());
301 }
302 Some(nt_hash_to_ntlm_password(nt))
303 }
304 None => None,
305 };
306
307 let mut _s_password: String = String::new();
309 if !use_ntlm && !_s_username.contains("not set") && !kerberos && !use_cert {
310 _s_password = match password {
311 Some(p) => p.to_owned(),
312 None => rpassword::prompt_password("Password: ").unwrap_or("not set".to_string()),
313 };
314 } else {
315 _s_password = password.unwrap_or("not set").to_owned();
316 }
317
318 debug!("IP: {}", ip.unwrap_or("not set"));
319 debug!("PORT: {}", match port { Some(p) => p.to_string(), None => "not set".to_owned() });
320 debug!("FQDN: {}", ldapfqdn.unwrap_or("not set"));
321 debug!("Url: {}", s_url);
322 debug!("Domain: {}", domain);
323 debug!("Username: {}", _s_username);
324 debug!("Email: {}", s_email.to_lowercase());
325 if use_cert {
326 debug!("Auth: certificate (Pass-the-Certificate)");
327 } else if use_ntlm {
328 debug!("Auth: NTLM pass-the-hash");
329 } else {
330 debug!("Password: {}", _s_password);
331 }
332 debug!("DC: {:?}", s_dc);
333 debug!("Kerberos: {:?}", kerberos);
334
335 Ok(LdapArgs {
336 s_url: s_url.to_string(),
337 _s_dc: s_dc,
338 _s_email: s_email.to_string().to_lowercase(),
339 s_username: if use_ntlm { _s_username.to_string() } else { s_email.to_string().to_lowercase() },
340 s_password: _s_password.to_string(),
341 s_ntlm_password,
342 })
343}
344
345fn nt_hash_to_ntlm_password(hex_hash: &str) -> String {
348 let upper = hex_hash.to_uppercase();
349 let bytes = upper.as_bytes();
350 let mut password = String::new();
351 for pair in bytes.chunks(2) {
352 let low_byte = pair[0] as u32;
353 let high_byte = if pair.len() > 1 { pair[1] as u32 } else { 0 };
354 let code_point = (high_byte << 8) | low_byte;
355 password.push(char::from_u32(code_point).unwrap_or('\0'));
356 }
357 for _ in 0..256 {
358 password.push('\0');
359 }
360 password
361}
362
363fn prepare_ldap_url(ldaps: bool, ip: Option<&str>, port: Option<u16>, domain: &str) -> String {
365 let protocol = if ldaps || port.unwrap_or(0) == 636 { "ldaps" } else { "ldap" };
366 let target = match ip { Some(ip) => ip, None => domain };
367 match port {
368 Some(port) => format!("{protocol}://{target}:{port}"),
369 None => format!("{protocol}://{target}"),
370 }
371}
372
373pub fn prepare_ldap_dc(domain: &str) -> Vec<String> {
375 let mut dc: String = "".to_owned();
376 let mut naming_context: Vec<String> = Vec::new();
377 if !domain.contains(".") {
378 dc.push_str("DC=");
379 dc.push_str(domain);
380 naming_context.push(dc[..].to_string());
381 } else {
382 naming_context.push(domain_to_dc(domain));
383 }
384 naming_context.push(format!("{}{}", "CN=Configuration,", &dc[..]));
385 naming_context
386}
387
388#[cfg(not(feature = "nogssapi"))]
390async fn gssapi_connection(
391 ldap: &mut ldap3::Ldap,
392 ldapfqdn: &str,
393 domain: &str,
394) -> Result<(), Box<dyn Error>> {
395 ldap.sasl_gssapi_bind(ldapfqdn)
396 .await?
397 .success()
398 .map_err(|e| format!("Kerberos authentication to {} failed: {e}", domain.to_uppercase()))?;
399 info!("Connected to {} Active Directory!", domain.to_uppercase().bold().green());
400 Ok(())
401}
402
403pub async fn get_all_naming_contexts(ldap: &mut ldap3::Ldap) -> Result<Vec<String>, Box<dyn Error>> {
405 let adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
406 Box::new(EntriesOnly::new()),
407 Box::new(PagedResults::new(999)),
408 ];
409 let mut search = ldap.streaming_search_with(
410 adapters,
411 "",
412 Scope::Base,
413 "(objectClass=*)",
414 vec!["namingContexts"],
415 ).await?;
416
417 let mut rs: Vec<SearchEntry> = Vec::new();
418 while let Some(entry) = search.next().await? {
419 rs.push(SearchEntry::construct(entry));
420 }
421 let res = search.finish().await.success();
422
423 let mut naming_contexts: Vec<String> = Vec::new();
424 match res {
425 Ok(_res) => {
426 debug!("All namingContexts collected!");
427 for result in rs {
428 for (_key, value) in &result.attrs {
429 for naming_context in value {
430 debug!("namingContext found: {}", &naming_context.bold().green());
431 naming_contexts.push(naming_context.to_string());
432 }
433 }
434 }
435 naming_contexts.sort_by_key(|cn| {
436 if cn.contains("CN=Schema") { 0 }
437 else if cn.to_lowercase().starts_with("dc=") { 1 }
438 else if cn.contains("CN=Configuration") { 2 }
439 else { 3 }
440 });
441 for (i, nc) in naming_contexts.iter().enumerate() {
442 trace!("NamingContext order [{}]: {}", i, nc);
443 }
444 return Ok(naming_contexts);
445 }
446 Err(err) => {
447 error!("No namingContexts found! Reason: {err}");
448 }
449 }
450 Ok(Vec::new())
451}
452
453#[derive(Debug, Clone, bincode::Encode, bincode::Decode)]
455pub struct LdapSearchEntry {
456 pub dn: String,
457 pub attrs: HashMap<String, Vec<String>>,
458 pub bin_attrs: HashMap<String, Vec<Vec<u8>>>,
459}
460
461impl From<SearchEntry> for LdapSearchEntry {
462 fn from(entry: SearchEntry) -> Self {
463 LdapSearchEntry { dn: entry.dn, attrs: entry.attrs, bin_attrs: entry.bin_attrs }
464 }
465}
466
467impl From<LdapSearchEntry> for SearchEntry {
468 fn from(entry: LdapSearchEntry) -> Self {
469 SearchEntry { dn: entry.dn, attrs: entry.attrs, bin_attrs: entry.bin_attrs }
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476
477 #[test]
478 fn nt_hash_encoding_roundtrip() {
479 let hash = "aad3b435b51404eeaad3b435b51404ee";
480 let password = nt_hash_to_ntlm_password(hash);
481 let utf16_bytes: Vec<u8> = password.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
482 assert!(utf16_bytes.len() > 512);
483 let hash_portion = &utf16_bytes[..utf16_bytes.len() - 512];
484 assert_eq!(hash_portion.len(), 32);
485 assert_eq!(hash_portion, hash.to_uppercase().as_bytes());
486 }
487
488 #[test]
489 fn nt_hash_encoding_all_zeros() {
490 let hash = "00000000000000000000000000000000";
491 let password = nt_hash_to_ntlm_password(hash);
492 let utf16_bytes: Vec<u8> = password.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
493 assert!(utf16_bytes.len() > 512);
494 let hash_portion = &utf16_bytes[..utf16_bytes.len() - 512];
495 assert_eq!(hash_portion, b"00000000000000000000000000000000");
496 }
497
498 #[test]
499 fn nt_hash_encoding_all_f() {
500 let hash = "ffffffffffffffffffffffffffffffff";
501 let password = nt_hash_to_ntlm_password(hash);
502 let utf16_bytes: Vec<u8> = password.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
503 assert!(utf16_bytes.len() > 512);
504 let hash_portion = &utf16_bytes[..utf16_bytes.len() - 512];
505 assert_eq!(hash_portion, b"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
506 }
507}