rusthound_ce/
ldap.rs

1//! Run a LDAP enumeration and parse results
2//!
3//! This module will prepare your connection and request the LDAP server to retrieve all the information needed to create the json files.
4//!
5//! rusthound sends only one request to the LDAP server, if the result of this one is higher than the limit of the LDAP server limit it will be split in several requests to avoid having an error 4 (LDAP_SIZELIMIT_EXCEED).
6//!
7//! Example in rust
8//!
9//! ```ignore
10//! let search = ldap_search(...)
11//! ```
12
13// use crate::errors::Result;
14use 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/// Function to request all AD values.
30#[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    // Construct LDAP args
44    let ldap_args = ldap_constructor(
45        ldaps, ip, port, domain, ldapfqdn, username, password, kerberos,
46    )?;
47
48    // LDAP connection
49    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    // // Prepare LDAP result vector
97    let mut total = 0; // for progress bar
98
99    // Request all namingContexts for current DC
100    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    // namingContexts: DC=domain,DC=local
112    // namingContexts: CN=Configuration,DC=domain,DC=local (needed for AD CS datas)
113    if res.iter().any(|s| s.contains("Configuration")) {
114        for cn in &res {
115            // Set control LDAP_SERVER_SD_FLAGS_OID to get nTSecurityDescriptor
116            // https://ldapwiki.com/wiki/LDAP_SERVER_SD_FLAGS_OID
117            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            // Prepare filter
125            // let mut _s_filter: &str = "";
126            // if cn.contains("Configuration") {
127            //     _s_filter = "(|(objectclass=pKIEnrollmentService)(objectclass=pkicertificatetemplate)(objectclass=subschema)(objectclass=certificationAuthority)(objectclass=container))";
128            // } else {
129            //     _s_filter = "(objectClass=*)";
130            // }
131            //let _s_filter = "(objectClass=*)";
132            //let _s_filter = "(objectGuid=*)";
133            info!("Ldap filter : {}", ldapfilter.bold().green());
134            let _s_filter = ldapfilter;
135
136            // Every 999 max value in ldap response (err 4 ldap)
137            let adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
138                Box::new(EntriesOnly::new()),
139                Box::new(PagedResults::new(999)),
140            ];
141
142            // Streaming search with adaptaters and filters
143            let mut search = ldap
144                .streaming_search_with(
145                    adapters, // Adapter which fetches Search results with a Paged Results control.
146                    cn,
147                    Scope::Subtree,
148                    _s_filter,
149                    vec!["*", "nTSecurityDescriptor"],
150                    // Without the presence of this control, the server returns an SD only when the SD attribute name is explicitly mentioned in the requested attribute list.
151                    // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/932a7a8d-8c93-4448-8093-c79b7d9ba499
152                )
153                .await?;
154
155            // Wait and get next values
156            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                //trace!("{:?}", &entry);
161                total += 1;
162                // Manage progress bar
163                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        // // If no result exit program
184        // if rs.is_empty() {
185        //     process::exit(0x0100);
186        // }
187
188        ldap.unbind().await?;
189    }
190
191    // drop ldap before final flush,
192    // otherwise it will warn about an i/o error
193    // "LDAP connection error: I/O error: Connection reset by peer (os error 54)"
194    drop(ldap);
195    if total == 0 {
196        error!("No LDAP objects found! Exiting...");
197        // std::fs::remove_file(cache_path)?; // TODO: return error so we can cleanup cache
198        process::exit(0x0100);
199    }
200
201    storage.flush()?;
202
203
204    // Return the vector with the result
205    Ok(total)
206}
207
208/// Structure containing the LDAP connection arguments.
209struct LdapArgs {
210    s_url: String,
211    _s_dc: Vec<String>,
212    _s_email: String,
213    s_username: String,
214    s_password: String,
215}
216
217/// Function to prepare LDAP arguments.
218fn 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    // Prepare ldap url
229    let s_url = prepare_ldap_url(ldaps, ip, port, domain);
230
231    // Prepare full DC chain
232    let s_dc = prepare_ldap_dc(domain);
233
234    // Username prompt
235    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    // Format username and email
256    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    // Password prompt
267    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    // Print infos if verbose mod is set
278    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
306/// Function to prepare LDAP url.
307fn 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
334/// Function to prepare LDAP DC from DOMAIN.LOCAL
335pub 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    // Format DC
341    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    // For ADCS values
351    naming_context.push(format!("{}{}", "CN=Configuration,", &dc[..])); 
352    naming_context
353}
354
355/// Function to make GSSAPI ldap connection.
356#[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
376/// (Not needed yet) Get all namingContext for DC
377pub async fn get_all_naming_contexts(
378    ldap: &mut ldap3::Ldap
379) -> Result<Vec<String>, Box<dyn Error>> {
380    // Every 999 max value in ldap response (err 4 ldap)
381    let adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
382        Box::new(EntriesOnly::new()),
383        Box::new(PagedResults::new(999)),
384    ];
385
386    // First LDAP request to get all namingContext
387    let mut search = ldap.streaming_search_with(
388        adapters,
389        "", 
390        Scope::Base,
391        "(objectClass=*)",
392        vec!["namingContexts"],
393    ).await?;
394
395    // Prepare LDAP result vector
396    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    // Prepare vector for all namingContexts result
404    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    // Empty result if no namingContexts found
425    Ok(Vec::new())
426}
427
428// New type to implement Serialize and Deserialize for SearchEntry
429#[derive(Debug, Clone, bincode::Encode, bincode::Decode)]
430pub struct LdapSearchEntry {
431    /// Entry DN.
432    pub dn: String,
433    /// Attributes.
434    pub attrs: HashMap<String, Vec<String>>,
435    /// Binary-valued attributes.
436    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}