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//! ```
10//! let search = ldap_search(...)
11//! ```
12
13// use crate::errors::Result;
14use crate::banner::progress_bar;
15use crate::utils::format::domain_to_dc;
16
17use colored::Colorize;
18use indicatif::ProgressBar;
19use ldap3::adapters::{Adapter, EntriesOnly};
20use ldap3::{adapters::PagedResults, controls::RawControl, LdapConnAsync, LdapConnSettings};
21use ldap3::{Scope, SearchEntry};
22use std::collections::HashMap;
23use std::error::Error;
24use std::process;
25use std::io::{self, Write, stdin};
26use log::{info, debug, error, trace};
27
28/// Function to request all AD values.
29pub async fn ldap_search(
30    ldaps: bool,
31    ip: &Option<String>,
32    port: &Option<u16>,
33    domain: &String,
34    ldapfqdn: &String,
35    username: &String,
36    password: &String,
37    kerberos: bool,
38    ldapfilter: &String,
39) -> Result<Vec<SearchEntry>, Box<dyn Error>> {
40    // Construct LDAP args
41    let ldap_args = ldap_constructor(ldaps, ip, port, domain, ldapfqdn, username, password, kerberos)?;
42
43    // LDAP connection
44    let consettings = LdapConnSettings::new().set_no_tls_verify(true);
45    let (conn, mut ldap) = LdapConnAsync::with_settings(consettings, &ldap_args.s_url).await?;
46    ldap3::drive!(conn);
47
48    if !kerberos {
49        debug!("Trying to connect with simple_bind() function (username:password)");
50        let res = ldap.simple_bind(&ldap_args.s_username, &ldap_args.s_password).await?.success();
51        match res {
52            Ok(_res) => {
53                info!("Connected to {} Active Directory!", domain.to_uppercase().bold().green());
54                info!("Starting data collection...");
55            },
56            Err(err) => {
57                error!("Failed to authenticate to {} Active Directory. Reason: {err}\n", domain.to_uppercase().bold().red());
58                process::exit(0x0100);
59            }
60        }
61    }
62    else
63    {
64        debug!("Trying to connect with sasl_gssapi_bind() function (kerberos session)");
65        if !&ldapfqdn.contains("not set") {
66            #[cfg(not(feature = "nogssapi"))]
67            gssapi_connection(&mut ldap,&ldapfqdn,&domain).await?;
68            #[cfg(feature = "nogssapi")]{
69                error!("Kerberos auth and GSSAPI not compatible with current os!");
70                process::exit(0x0100);
71            }
72        } else {
73            error!("Need Domain Controller FQDN to bind GSSAPI connection. Please use '{}'\n", "-f DC01.DOMAIN.LAB".bold());
74            process::exit(0x0100);
75        }
76    }
77
78    // Prepare LDAP result vector
79    let mut rs: Vec<SearchEntry> = Vec::new();
80
81    // Request all namingContexts for current DC
82    let res = match get_all_naming_contexts(&mut ldap).await {
83        Ok(res) => {
84            trace!("naming_contexts: {:?}",&res);
85            res
86        },
87        Err(err) => {
88            error!("No namingContexts found! Reason: {err}\n");
89            process::exit(0x0100);
90        }
91    };
92
93    // namingContexts: DC=domain,DC=local
94    // namingContexts: CN=Configuration,DC=domain,DC=local (needed for AD CS datas)
95    if res.iter().any(|s| s.contains("Configuration")) {
96        for cn in &res {
97            // Set control LDAP_SERVER_SD_FLAGS_OID to get nTSecurityDescriptor
98            // https://ldapwiki.com/wiki/LDAP_SERVER_SD_FLAGS_OID
99            let ctrls = RawControl {
100                ctype: String::from("1.2.840.113556.1.4.801"),
101                crit: true,
102                val: Some(vec![48,3,2,1,5]),
103            };
104            ldap.with_controls(ctrls.to_owned());
105    
106            // Prepare filter
107            // let mut _s_filter: &str = "";
108            // if cn.contains("Configuration") {
109            //     _s_filter = "(|(objectclass=pKIEnrollmentService)(objectclass=pkicertificatetemplate)(objectclass=subschema)(objectclass=certificationAuthority)(objectclass=container))";
110            // } else {
111            //     _s_filter = "(objectClass=*)";
112            // }
113            //let _s_filter = "(objectClass=*)";
114            //let _s_filter = "(objectGuid=*)";
115            info!("Ldap filter : {}", ldapfilter.bold().green());
116            let _s_filter = ldapfilter;
117    
118            // Every 999 max value in ldap response (err 4 ldap)
119            let adapters: Vec<Box<dyn Adapter<_,_>>> = vec![
120                Box::new(EntriesOnly::new()),
121                Box::new(PagedResults::new(999)),
122            ];
123    
124            // Streaming search with adaptaters and filters
125            let mut search = ldap.streaming_search_with(
126                adapters, // Adapter which fetches Search results with a Paged Results control.
127                cn, 
128                Scope::Subtree,
129                _s_filter,
130                vec!["*", "nTSecurityDescriptor"], 
131                // 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.
132                // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/932a7a8d-8c93-4448-8093-c79b7d9ba499
133            ).await?;
134    
135            // Wait and get next values
136            let pb = ProgressBar::new(1);
137            let mut count = 0;	
138            while let Some(entry) = search.next().await? {
139                let entry = SearchEntry::construct(entry);
140                //trace!("{:?}", &entry);
141                // Manage progress bar
142                count += 1;
143                progress_bar(pb.to_owned(),"LDAP objects retrieved".to_string(),count,"#".to_string());	
144                // Push all result in rs vec()
145                rs.push(entry);
146            }
147            pb.finish_and_clear();
148    
149            let res = search.finish().await.success();
150            match res {
151                Ok(_res) => info!("All data collected for NamingContext {}",&cn.bold()),
152                Err(err) => {
153                    error!("No data collected on {}! Reason: {err}",&cn.bold().red());
154                }
155            }
156        }
157        // If no result exit program
158        if rs.len() <= 0 {
159            process::exit(0x0100);
160        }
161    
162        // Terminate the connection to the server
163        ldap.unbind().await?;
164    }
165    
166    // Return the vector with the result
167    return Ok(rs);
168}
169
170/// Structure containing the LDAP connection arguments.
171struct LdapArgs {
172    s_url: String,
173    _s_dc: Vec<String>,
174    _s_email: String,
175    s_username: String,
176    s_password: String,
177}
178
179/// Function to prepare LDAP arguments.
180fn ldap_constructor(
181    ldaps: bool,
182    ip: &Option<String>,
183    port: &Option<u16>,
184    domain: &String,
185    ldapfqdn: &String,
186    username: &String,
187    password: &String,
188    kerberos: bool,
189) -> Result<LdapArgs, Box<dyn Error>>  {
190    // Prepare ldap url
191    let s_url = prepare_ldap_url(ldaps, ip, port, domain);
192
193    // Prepare full DC chain
194    let s_dc = prepare_ldap_dc(domain);
195
196    // Username prompt
197    let mut s= String::new();
198    let mut _s_username: String;
199    if username.contains("not set") && !kerberos {
200        print!("Username: ");
201        io::stdout().flush()?;
202        stdin().read_line(&mut s).expect("Did not enter a correct username");
203        io::stdout().flush()?;
204        if let Some('\n')=s.chars().next_back() {
205            s.pop();
206        }
207        if let Some('\r')=s.chars().next_back() {
208            s.pop();
209        }
210        _s_username = s.to_owned();
211    } else {
212        _s_username = username.to_owned();
213    }
214
215    // Format username and email
216    let mut s_email: String = "".to_owned();
217    if !_s_username.contains("@") {
218        s_email.push_str(&_s_username.to_string());
219        s_email.push_str("@");
220        s_email.push_str(domain);
221        _s_username = s_email.to_string();
222    } else {
223        s_email = _s_username.to_string().to_lowercase();
224    }
225
226    // Password prompt
227    let mut _s_password: String = String::new();
228    if !_s_username.contains("not set") && !kerberos {
229        if password.contains("not set") {
230            // Prompt for user password
231            let rpass: String = rpassword::prompt_password("Password: ").unwrap_or("not set".to_string());
232            _s_password = rpass;
233        } else {
234            _s_password = password.to_owned();
235        }
236    } else {
237        _s_password = password.to_owned();
238    }
239
240    // Print infos if verbose mod is set
241    debug!("IP: {}", match ip {
242        Some(ip) => ip.to_owned(),
243        None => "not set".to_owned()
244    });
245    debug!("PORT: {}", match port {
246        Some(p) => {
247            p.to_string()
248        },
249        None => "not set".to_owned()
250    });
251    debug!("FQDN: {}", ldapfqdn);
252    debug!("Url: {}", s_url);
253    debug!("Domain: {}", domain);
254    debug!("Username: {}", _s_username);
255    debug!("Email: {}", s_email.to_lowercase());
256    debug!("Password: {}", _s_password);
257    debug!("DC: {:?}", s_dc);
258    debug!("Kerberos: {:?}", kerberos);
259
260    Ok(LdapArgs {
261        s_url: s_url.to_string(),
262        _s_dc: s_dc,
263        _s_email: s_email.to_string().to_lowercase(),
264        s_username: s_email.to_string().to_lowercase(),
265        s_password: _s_password.to_string(),
266    })
267}
268
269/// Function to prepare LDAP url.
270fn prepare_ldap_url(ldaps: bool, ip: &Option<String>, port: &Option<u16>, domain: &String) -> String {
271    let protocol = if ldaps || port.unwrap_or(0) == 636 {
272        "ldaps"
273    } else {
274        "ldap"
275    };
276
277    let target = match ip {
278        Some(ip) => ip,
279        None => domain
280    };
281
282    match port {
283        Some(port) => {
284            format!("{protocol}://{target}:{port}")
285        }
286        None => {
287            format!("{protocol}://{target}")
288        }
289    }
290}
291
292/// Function to prepare LDAP DC from DOMAIN.LOCAL
293pub fn prepare_ldap_dc(domain: &String) -> Vec<String> {
294
295    let mut dc: String = "".to_owned();
296    let mut naming_context: Vec<String> = Vec::new();
297
298    // Format DC
299    if !domain.contains(".") {
300        dc.push_str("DC=");
301        dc.push_str(&domain);
302        naming_context.push(dc[..].to_string());
303    }
304    else 
305    {
306        naming_context.push(domain_to_dc(domain));
307    }
308
309    // For ADCS values
310    naming_context.push(format!("{}{}","CN=Configuration,",dc[..].to_string())); 
311
312    return naming_context
313}
314
315/// Function to make GSSAPI ldap connection.
316#[cfg(not(feature = "nogssapi"))]
317async fn gssapi_connection(
318    ldap: &mut ldap3::Ldap,
319    ldapfqdn: &String,
320    domain: &String,
321) -> Result<(), Box<dyn Error>> {
322    let res = ldap.sasl_gssapi_bind(ldapfqdn).await?.success();
323    match res {
324        Ok(_res) => {
325            info!("Connected to {} Active Directory!", domain.to_uppercase().bold().green());
326            info!("Starting data collection...");
327        },
328        Err(err) => {
329            error!("Failed to authenticate to {} Active Directory. Reason: {err}\n", domain.to_uppercase().bold().red());
330            process::exit(0x0100);
331        }
332    }
333    Ok(())
334}
335
336/// (Not needed yet) Get all namingContext for DC
337pub async fn get_all_naming_contexts(
338    ldap: &mut ldap3::Ldap
339) -> Result<Vec<String>, Box<dyn Error>> {
340    // Every 999 max value in ldap response (err 4 ldap)
341    let adapters: Vec<Box<dyn Adapter<_,_>>> = vec![
342        Box::new(EntriesOnly::new()),
343        Box::new(PagedResults::new(999)),
344    ];
345
346    // First LDAP request to get all namingContext
347    let mut search = ldap.streaming_search_with(
348        adapters,
349        "", 
350        Scope::Base,
351        "(objectClass=*)",
352        vec!["namingContexts"],
353    ).await?;
354
355    // Prepare LDAP result vector
356    let mut rs: Vec<SearchEntry> = Vec::new();
357    while let Some(entry) = search.next().await? {
358        let entry = SearchEntry::construct(entry);
359        rs.push(entry);
360    }
361    let res = search.finish().await.success();
362
363    // Prepare vector for all namingContexts result
364    let mut naming_contexts: Vec<String> = Vec::new();
365    match res {
366        Ok(_res) => {
367            debug!("All namingContexts collected!");
368            for result in rs {
369                let result_attrs: HashMap<String, Vec<String>> = result.attrs;
370
371                for (_key, value) in &result_attrs {
372                    for naming_context in value {
373                        debug!("namingContext found: {}",&naming_context.bold().green());
374                        naming_contexts.push(naming_context.to_string());
375                    }
376                }
377            }
378            return Ok(naming_contexts)
379        },
380        Err(err) => {
381            error!("No namingContexts found! Reason: {err}");
382        }
383    }
384    // Empty result if no namingContexts found
385    Ok(Vec::new())
386}