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 log::{info, debug, error, trace};
23use std::io::{self, Write, stdin};
24use std::collections::HashMap;
25use std::error::Error;
26use std::process;
27
28/// Function to request all AD values.
29pub async fn ldap_search(
30    ldaps: bool,
31    ip: Option<&str>,
32    port: &Option<u16>,
33    domain: &str,
34    ldapfqdn: &str,
35    username: Option<&str>,
36    password: Option<&str>,
37    kerberos: bool,
38    ldapfilter: &str,
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.is_empty() {
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    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<&str>,
183    port: &Option<u16>,
184    domain: &str,
185    ldapfqdn: &str,
186    username: Option<&str>,
187    password: Option<&str>,
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.is_none() && !kerberos {
200        print!("Username: ");
201        io::stdout().flush()?;
202        stdin()
203            .read_line(&mut s)
204            .expect("Did not enter a correct username");
205        io::stdout().flush()?;
206        if let Some('\n') = s.chars().next_back() {
207            s.pop();
208        }
209        if let Some('\r') = s.chars().next_back() {
210            s.pop();
211        }
212        _s_username = s.to_owned();
213    } else {
214        _s_username = username.unwrap_or("not set").to_owned();
215    }
216
217    // Format username and email
218    let mut s_email: String = "".to_owned();
219    if !_s_username.contains("@") {
220        s_email.push_str(&_s_username.to_string());
221        s_email.push_str("@");
222        s_email.push_str(domain);
223        _s_username = s_email.to_string();
224    } else {
225        s_email = _s_username.to_string().to_lowercase();
226    }
227
228    // Password prompt
229    let mut _s_password: String = String::new();
230    if !_s_username.contains("not set") && !kerberos {
231        _s_password = match password {
232            Some(p) => p.to_owned(),
233            None => rpassword::prompt_password("Password: ").unwrap_or("not set".to_string()),
234        };
235    } else {
236        _s_password = password.unwrap_or("not set").to_owned();
237    }
238
239    // Print infos if verbose mod is set
240    debug!("IP: {}", match ip {
241        Some(ip) => ip,
242        None => "not set"
243    });
244    debug!("PORT: {}", match port {
245        Some(p) => {
246            p.to_string()
247        },
248        None => "not set".to_owned()
249    });
250    debug!("FQDN: {}", ldapfqdn);
251    debug!("Url: {}", s_url);
252    debug!("Domain: {}", domain);
253    debug!("Username: {}", _s_username);
254    debug!("Email: {}", s_email.to_lowercase());
255    debug!("Password: {}", _s_password);
256    debug!("DC: {:?}", s_dc);
257    debug!("Kerberos: {:?}", kerberos);
258
259    Ok(LdapArgs {
260        s_url: s_url.to_string(),
261        _s_dc: s_dc,
262        _s_email: s_email.to_string().to_lowercase(),
263        s_username: s_email.to_string().to_lowercase(),
264        s_password: _s_password.to_string(),
265    })
266}
267
268/// Function to prepare LDAP url.
269fn prepare_ldap_url(
270    ldaps: bool,
271    ip: Option<&str>,
272    port: &Option<u16>,
273    domain: &str
274) -> String {
275    let protocol = if ldaps || port.unwrap_or(0) == 636 {
276        "ldaps"
277    } else {
278        "ldap"
279    };
280
281    let target = match ip {
282        Some(ip) => ip,
283        None => domain,
284    };
285
286    match port {
287        Some(port) => {
288            format!("{protocol}://{target}:{port}")
289        }
290        None => {
291            format!("{protocol}://{target}")
292        }
293    }
294}
295
296/// Function to prepare LDAP DC from DOMAIN.LOCAL
297pub fn prepare_ldap_dc(domain: &str) -> Vec<String> {
298
299    let mut dc: String = "".to_owned();
300    let mut naming_context: Vec<String> = Vec::new();
301
302    // Format DC
303    if !domain.contains(".") {
304        dc.push_str("DC=");
305        dc.push_str(domain);
306        naming_context.push(dc[..].to_string());
307    }
308    else {
309        naming_context.push(domain_to_dc(domain));
310    }
311
312    // For ADCS values
313    naming_context.push(format!("{}{}", "CN=Configuration,", &dc[..])); 
314    naming_context
315}
316
317/// Function to make GSSAPI ldap connection.
318#[cfg(not(feature = "nogssapi"))]
319async fn gssapi_connection(
320    ldap: &mut ldap3::Ldap,
321    ldapfqdn: &str,
322    domain: &str,
323) -> Result<(), Box<dyn Error>> {
324    let res = ldap.sasl_gssapi_bind(ldapfqdn).await?.success();
325    match res {
326        Ok(_res) => {
327            info!("Connected to {} Active Directory!", domain.to_uppercase().bold().green());
328            info!("Starting data collection...");
329        }
330        Err(err) => {
331            error!("Failed to authenticate to {} Active Directory. Reason: {err}\n", domain.to_uppercase().bold().red());
332            process::exit(0x0100);
333        }
334    }
335    Ok(())
336}
337
338/// (Not needed yet) Get all namingContext for DC
339pub async fn get_all_naming_contexts(
340    ldap: &mut ldap3::Ldap
341) -> Result<Vec<String>, Box<dyn Error>> {
342    // Every 999 max value in ldap response (err 4 ldap)
343    let adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
344        Box::new(EntriesOnly::new()),
345        Box::new(PagedResults::new(999)),
346    ];
347
348    // First LDAP request to get all namingContext
349    let mut search = ldap.streaming_search_with(
350        adapters,
351        "", 
352        Scope::Base,
353        "(objectClass=*)",
354        vec!["namingContexts"],
355    ).await?;
356
357    // Prepare LDAP result vector
358    let mut rs: Vec<SearchEntry> = Vec::new();
359    while let Some(entry) = search.next().await? {
360        let entry = SearchEntry::construct(entry);
361        rs.push(entry);
362    }
363    let res = search.finish().await.success();
364
365    // Prepare vector for all namingContexts result
366    let mut naming_contexts: Vec<String> = Vec::new();
367    match res {
368        Ok(_res) => {
369            debug!("All namingContexts collected!");
370            for result in rs {
371                let result_attrs: HashMap<String, Vec<String>> = result.attrs;
372
373                for (_key, value) in &result_attrs {
374                    for naming_context in value {
375                        debug!("namingContext found: {}",&naming_context.bold().green());
376                        naming_contexts.push(naming_context.to_string());
377                    }
378                }
379            }
380            return Ok(naming_contexts)
381        }
382        Err(err) => {
383            error!("No namingContexts found! Reason: {err}");
384        }
385    }
386    // Empty result if no namingContexts found
387    Ok(Vec::new())
388}