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 kerberos: bool,
40 ldapfilter: &str,
41 storage: &mut S,
42) -> Result<usize, Box<dyn Error>> {
43 let ldap_args = ldap_constructor(
45 ldaps, ip, port, domain, ldapfqdn, username, password, kerberos,
46 )?;
47
48 let consettings = LdapConnSettings::new()
50 .set_conn_timeout(std::time::Duration::from_secs(10))
51 .set_no_tls_verify(true);
52 let (conn, mut ldap) = LdapConnAsync::with_settings(consettings, &ldap_args.s_url).await?;
53 ldap3::drive!(conn);
54
55 if !kerberos {
56 debug!("Trying to connect with simple_bind() function (username:password)");
57 let res = ldap
58 .simple_bind(&ldap_args.s_username, &ldap_args.s_password)
59 .await?
60 .success();
61 match res {
62 Ok(_res) => {
63 info!(
64 "Connected to {} Active Directory!",
65 domain.to_uppercase().bold().green()
66 );
67 info!("Starting data collection...");
68 }
69 Err(err) => {
70 error!(
71 "Failed to authenticate to {} Active Directory. Reason: {err}\n",
72 domain.to_uppercase().bold().red()
73 );
74 process::exit(0x0100);
75 }
76 }
77 } else {
78 debug!("Trying to connect with sasl_gssapi_bind() function (kerberos session)");
79 if !&ldapfqdn.contains("not set") {
80 #[cfg(not(feature = "nogssapi"))]
81 gssapi_connection(&mut ldap, &ldapfqdn, &domain).await?;
82 #[cfg(feature = "nogssapi")]
83 {
84 error!("Kerberos auth and GSSAPI not compatible with current os!");
85 process::exit(0x0100);
86 }
87 } else {
88 error!(
89 "Need Domain Controller FQDN to bind GSSAPI connection. Please use '{}'\n",
90 "-f DC01.DOMAIN.LAB".bold()
91 );
92 process::exit(0x0100);
93 }
94 }
95
96 let mut total = 0; let res = match get_all_naming_contexts(&mut ldap).await {
101 Ok(res) => {
102 trace!("naming_contexts: {:?}", &res);
103 res
104 }
105 Err(err) => {
106 error!("No namingContexts found! Reason: {err}\n");
107 process::exit(0x0100);
108 }
109 };
110
111 if res.iter().any(|s| s.contains("Configuration")) {
114 for cn in &res {
115 let ctrls = RawControl {
118 ctype: String::from("1.2.840.113556.1.4.801"),
119 crit: true,
120 val: Some(vec![48, 3, 2, 1, 5]),
121 };
122 ldap.with_controls(ctrls.to_owned());
123
124 info!("Ldap filter : {}", ldapfilter.bold().green());
134 let _s_filter = ldapfilter;
135
136 let adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
138 Box::new(EntriesOnly::new()),
139 Box::new(PagedResults::new(999)),
140 ];
141
142 let mut search = ldap
144 .streaming_search_with(
145 adapters, cn,
147 Scope::Subtree,
148 _s_filter,
149 vec!["*", "nTSecurityDescriptor"],
150 )
153 .await?;
154
155 let pb = ProgressBar::new(1);
157 let mut count = 0;
158 while let Some(entry) = search.next().await? {
159 let entry = SearchEntry::construct(entry);
160 total += 1;
162 count += 1;
164 progress_bar(
165 pb.to_owned(),
166 "LDAP objects retrieved".to_string(),
167 count,
168 "#".to_string(),
169 );
170
171 storage.add(entry.into())?;
172 }
173 pb.finish_and_clear();
174
175 let res = search.finish().await.success();
176 match res {
177 Ok(_res) => info!("All data collected for NamingContext {}", &cn.bold()),
178 Err(err) => {
179 error!("No data collected on {}! Reason: {err}", &cn.bold().red());
180 }
181 }
182 }
183 ldap.unbind().await?;
189 }
190
191 drop(ldap);
195 if total == 0 {
196 error!("No LDAP objects found! Exiting...");
197 process::exit(0x0100);
199 }
200
201 storage.flush()?;
202
203
204 Ok(total)
206}
207
208struct LdapArgs {
210 s_url: String,
211 _s_dc: Vec<String>,
212 _s_email: String,
213 s_username: String,
214 s_password: String,
215}
216
217fn ldap_constructor(
219 ldaps: bool,
220 ip: Option<&str>,
221 port: Option<u16>,
222 domain: &str,
223 ldapfqdn: &str,
224 username: Option<&str>,
225 password: Option<&str>,
226 kerberos: bool,
227) -> Result<LdapArgs, Box<dyn Error>> {
228 let s_url = prepare_ldap_url(ldaps, ip, port, domain);
230
231 let s_dc = prepare_ldap_dc(domain);
233
234 let mut s = String::new();
236 let mut _s_username: String;
237 if username.is_none() && !kerberos {
238 print!("Username: ");
239 io::stdout().flush()?;
240 stdin()
241 .read_line(&mut s)
242 .expect("Did not enter a correct username");
243 io::stdout().flush()?;
244 if let Some('\n') = s.chars().next_back() {
245 s.pop();
246 }
247 if let Some('\r') = s.chars().next_back() {
248 s.pop();
249 }
250 _s_username = s.to_owned();
251 } else {
252 _s_username = username.unwrap_or("not set").to_owned();
253 }
254
255 let mut s_email: String = "".to_owned();
257 if !_s_username.contains("@") {
258 s_email.push_str(&_s_username.to_string());
259 s_email.push_str("@");
260 s_email.push_str(domain);
261 _s_username = s_email.to_string();
262 } else {
263 s_email = _s_username.to_string().to_lowercase();
264 }
265
266 let mut _s_password: String = String::new();
268 if !_s_username.contains("not set") && !kerberos {
269 _s_password = match password {
270 Some(p) => p.to_owned(),
271 None => rpassword::prompt_password("Password: ").unwrap_or("not set".to_string()),
272 };
273 } else {
274 _s_password = password.unwrap_or("not set").to_owned();
275 }
276
277 debug!("IP: {}", match ip {
279 Some(ip) => ip,
280 None => "not set"
281 });
282 debug!("PORT: {}", match port {
283 Some(p) => {
284 p.to_string()
285 },
286 None => "not set".to_owned()
287 });
288 debug!("FQDN: {}", ldapfqdn);
289 debug!("Url: {}", s_url);
290 debug!("Domain: {}", domain);
291 debug!("Username: {}", _s_username);
292 debug!("Email: {}", s_email.to_lowercase());
293 debug!("Password: {}", _s_password);
294 debug!("DC: {:?}", s_dc);
295 debug!("Kerberos: {:?}", kerberos);
296
297 Ok(LdapArgs {
298 s_url: s_url.to_string(),
299 _s_dc: s_dc,
300 _s_email: s_email.to_string().to_lowercase(),
301 s_username: s_email.to_string().to_lowercase(),
302 s_password: _s_password.to_string(),
303 })
304}
305
306fn prepare_ldap_url(
308 ldaps: bool,
309 ip: Option<&str>,
310 port: Option<u16>,
311 domain: &str
312) -> String {
313 let protocol = if ldaps || port.unwrap_or(0) == 636 {
314 "ldaps"
315 } else {
316 "ldap"
317 };
318
319 let target = match ip {
320 Some(ip) => ip,
321 None => domain,
322 };
323
324 match port {
325 Some(port) => {
326 format!("{protocol}://{target}:{port}")
327 }
328 None => {
329 format!("{protocol}://{target}")
330 }
331 }
332}
333
334pub fn prepare_ldap_dc(domain: &str) -> Vec<String> {
336
337 let mut dc: String = "".to_owned();
338 let mut naming_context: Vec<String> = Vec::new();
339
340 if !domain.contains(".") {
342 dc.push_str("DC=");
343 dc.push_str(domain);
344 naming_context.push(dc[..].to_string());
345 }
346 else {
347 naming_context.push(domain_to_dc(domain));
348 }
349
350 naming_context.push(format!("{}{}", "CN=Configuration,", &dc[..]));
352 naming_context
353}
354
355#[cfg(not(feature = "nogssapi"))]
357async fn gssapi_connection(
358 ldap: &mut ldap3::Ldap,
359 ldapfqdn: &str,
360 domain: &str,
361) -> Result<(), Box<dyn Error>> {
362 let res = ldap.sasl_gssapi_bind(ldapfqdn).await?.success();
363 match res {
364 Ok(_res) => {
365 info!("Connected to {} Active Directory!", domain.to_uppercase().bold().green());
366 info!("Starting data collection...");
367 }
368 Err(err) => {
369 error!("Failed to authenticate to {} Active Directory. Reason: {err}\n", domain.to_uppercase().bold().red());
370 process::exit(0x0100);
371 }
372 }
373 Ok(())
374}
375
376pub async fn get_all_naming_contexts(
378 ldap: &mut ldap3::Ldap
379) -> Result<Vec<String>, Box<dyn Error>> {
380 let adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
382 Box::new(EntriesOnly::new()),
383 Box::new(PagedResults::new(999)),
384 ];
385
386 let mut search = ldap.streaming_search_with(
388 adapters,
389 "",
390 Scope::Base,
391 "(objectClass=*)",
392 vec!["namingContexts"],
393 ).await?;
394
395 let mut rs: Vec<SearchEntry> = Vec::new();
397 while let Some(entry) = search.next().await? {
398 let entry = SearchEntry::construct(entry);
399 rs.push(entry);
400 }
401 let res = search.finish().await.success();
402
403 let mut naming_contexts: Vec<String> = Vec::new();
405 match res {
406 Ok(_res) => {
407 debug!("All namingContexts collected!");
408 for result in rs {
409 let result_attrs: HashMap<String, Vec<String>> = result.attrs;
410
411 for (_key, value) in &result_attrs {
412 for naming_context in value {
413 debug!("namingContext found: {}",&naming_context.bold().green());
414 naming_contexts.push(naming_context.to_string());
415 }
416 }
417 }
418 return Ok(naming_contexts)
419 }
420 Err(err) => {
421 error!("No namingContexts found! Reason: {err}");
422 }
423 }
424 Ok(Vec::new())
426}
427
428#[derive(Debug, Clone, bincode::Encode, bincode::Decode)]
430pub struct LdapSearchEntry {
431 pub dn: String,
433 pub attrs: HashMap<String, Vec<String>>,
435 pub bin_attrs: HashMap<String, Vec<Vec<u8>>>,
437}
438
439impl From<SearchEntry> for LdapSearchEntry {
440 fn from(entry: SearchEntry) -> Self {
441 LdapSearchEntry {
442 dn: entry.dn,
443 attrs: entry.attrs,
444 bin_attrs: entry.bin_attrs,
445 }
446 }
447}
448
449impl From<LdapSearchEntry> for SearchEntry {
450 fn from(entry: LdapSearchEntry) -> Self {
451 SearchEntry {
452 dn: entry.dn,
453 attrs: entry.attrs,
454 bin_attrs: entry.bin_attrs,
455 }
456 }
457}