Skip to main content

rusthound_ce/
args.rs

1//! Parsing arguments
2#[cfg(not(feature = "noargs"))]
3use clap::{Arg, ArgAction, value_parser, Command};
4
5#[cfg(feature = "noargs")]
6use winreg::{RegKey,{enums::*}};
7#[cfg(feature = "noargs")]
8use crate::utils::exec::run;
9#[cfg(feature = "noargs")]
10use regex::Regex;
11
12#[derive(Clone, Debug)]
13pub struct Options {
14    pub domain: String,
15    pub username: Option<String>,
16    pub password: Option<String>,
17    pub ldapfqdn: Option<String>,
18    pub ip: Option<String>,
19    pub port: Option<u16>,
20    pub name_server: String,
21    pub path: String,
22    pub collection_method: CollectionMethod,
23    pub ldaps: bool,
24    pub dns_tcp: bool,
25    pub fqdn_resolver: bool,
26    pub hashes: Option<String>,
27    pub kerberos: bool,
28    // Certificate authentication (Pass-the-Certificate / Schannel)
29    pub pfx: Option<String>,
30    pub pfx_pass: Option<String>,
31    pub crt: Option<String>,
32    pub key: Option<String>,
33    pub zip: bool,
34    pub verbose: log::LevelFilter,
35    pub ldap_filter: String,
36
37    pub cache: bool,
38    pub cache_buffer_size: usize,
39    pub resume: bool,
40}
41
42impl Options {
43    /// True when authenticating with a client certificate (no SMB credentials
44    /// are available, so SMB-based modules must be skipped).
45    pub fn uses_cert(&self) -> bool {
46        self.pfx.is_some() || self.crt.is_some()
47    }
48}
49
50#[derive(Clone, Debug, PartialEq)]
51pub enum CollectionMethod {
52    All,            // LDAP + sessions (all three RPC paths) + SMB on SYSVOL
53    DCOnly,         // LDAP only, never contacts a machine + SMB on SYSVOL
54    Session,        // LDAP + SRVSVC + WKSSVC + WINREG 
55    RegistryOnly,   // LDAP + WINREG
56    LdapOnly,       // LDAP
57}
58
59impl CollectionMethod {
60    // Methods that never contact a machine: DCOnly and LdapOnly.
61    pub fn does_sessions(&self) -> bool {
62        !matches!(self, Self::DCOnly | Self::LdapOnly)
63    }
64    pub fn srvsvc(&self)   -> bool { matches!(self, Self::All | Self::Session) }
65    pub fn wkssvc(&self)   -> bool { matches!(self, Self::All | Self::Session) }
66    pub fn registry(&self) -> bool { matches!(self, Self::All | Self::Session | Self::RegistryOnly) }
67    // SYSVOL GPO reading contacts the DC, so LdapOnly stays out of it.
68    pub fn does_gpo(&self)  -> bool { matches!(self, Self::All | Self::DCOnly) }
69}
70
71// Current RustHound version
72pub const RUSTHOUND_VERSION: &str = env!("CARGO_PKG_VERSION");
73
74#[cfg(not(feature = "noargs"))]
75fn cli() -> Command {
76    // Return Command args
77    Command::new("rusthound-ce")
78    .version(RUSTHOUND_VERSION)
79    .about("Active Directory data collector for BloodHound Community Edition.\ng0h4n <https://twitter.com/g0h4n_0>")
80    .arg(Arg::new("v")
81        .short('v')
82        .help("Set the level of verbosity")
83        .action(ArgAction::Count),
84    )
85    .next_help_heading("REQUIRED VALUES")
86    .arg(Arg::new("domain")
87        .short('d')
88        .long("domain")
89            .help("Domain name like: DOMAIN.LOCAL")
90            .required(true)
91            .value_parser(value_parser!(String))
92    )
93    .next_help_heading("OPTIONAL VALUES")
94    .arg(Arg::new("ldapusername")
95        .short('u')
96        .long("ldapusername")
97        .help("LDAP username, like: user@domain.local")
98        .required(false)
99        .value_parser(value_parser!(String))
100    )
101    .arg(Arg::new("ldappassword")
102        .short('p')
103        .long("ldappassword")
104        .help("LDAP password")
105        .required(false)
106        .value_parser(value_parser!(String))
107    )
108    .arg(Arg::new("hashes")
109        .short('H')
110        .long("hashes")
111        .help("NT hash for pass-the-hash authentication (NTLM), accept [NTHASH, :NTHASH, LMHASH:NTHASH]")
112        .required(false)
113        .value_parser(value_parser!(String))
114    )
115    .arg(Arg::new("ldapfqdn")
116        .short('f')
117        .long("ldapfqdn")
118        .help("Domain Controller FQDN like: DC01.DOMAIN.LOCAL or just DC01")
119        .required(false)
120        .value_parser(value_parser!(String))
121    )
122    .arg(Arg::new("ldapip")
123        .short('i')
124        .long("ldapip")
125        .help("Domain Controller IP address like: 192.168.1.10")
126        .required(false)
127        .value_parser(value_parser!(String))
128    )
129    .arg(Arg::new("ldapport")
130        .short('P')
131        .long("ldapport")
132        .help("LDAP port [default: 389, or 636 with --ldaps]")
133        .required(false)
134        .value_parser(value_parser!(String))
135    )
136    .arg(Arg::new("name-server")
137        .short('n')
138        .long("name-server")
139        .help("Alternative IP address name server to use for DNS queries")
140        .required(false)
141        .value_parser(value_parser!(String))
142    )
143    .arg(Arg::new("output")
144        .short('o')
145        .long("output")
146        .help("Output directory where you would like to save JSON files [default: ./]")
147        .required(false)
148        .value_parser(value_parser!(String))
149    )
150    .next_help_heading("CERTIFICATE AUTHENTICATION")
151    .arg(Arg::new("pfx")
152        .long("pfx")
153        .help("PFX/PKCS#12 client certificate for certificate authentication (Pass-the-Certificate). Uses StartTLS by default, or LDAPS with --ldaps")
154        .required(false)
155        .value_parser(value_parser!(String))
156    )
157    .arg(Arg::new("pfx-pass")
158        .long("pfx-pass")
159        .help("Password protecting the PFX file (optional)")
160        .required(false)
161        .value_parser(value_parser!(String))
162    )
163    .arg(Arg::new("crt")
164        .long("crt")
165        .help("PEM client certificate for certificate authentication (use with --key)")
166        .required(false)
167        .value_parser(value_parser!(String))
168    )
169    .arg(Arg::new("key")
170        .long("key")
171        .help("PEM private key for certificate authentication (use with --crt)")
172        .required(false)
173        .value_parser(value_parser!(String))
174    )
175    .next_help_heading("OPTIONAL FLAGS")
176    .arg(Arg::new("collectionmethod")
177        .short('c')
178        .long("collectionmethod")
179        .help("Which information to collect. Supported: All (LDAP, SMB, HTTP), DCOnly (LDAP + SYSVOL, no member-machine connections), Session (user sessions over RPC), RegistryOnly (sessions over WINREG), LdapOnly (LDAP only, no machine or SYSVOL) (default: All)")        .required(false)
180        .value_name("COLLECTIONMETHOD")
181        .value_parser(["All", "DCOnly", "Session", "RegistryOnly", "LdapOnly"])
182        .num_args(0..=1)
183        .default_missing_value("All")
184    )
185    .arg(Arg::new("ldap-filter")
186        .long("ldap-filter")
187        .help("Use custom ldap-filter default is : (objectClass=*)")
188        .required(false)
189        .value_parser(value_parser!(String))
190        .default_missing_value("(objectClass=*)")
191    )
192    .arg(Arg::new("ldaps")
193        .long("ldaps")
194        .help("Force LDAPS using for request like: ldaps://DOMAIN.LOCAL/")
195        .required(false)
196        .action(ArgAction::SetTrue)
197        .global(false)
198    )
199    .arg(Arg::new("kerberos")
200        .short('k')
201        .long("kerberos")
202        .help("Use Kerberos authentication. Grabs credentials from ccache file (KRB5CCNAME) based on target parameters for Linux.")
203        .required(false)
204        .action(ArgAction::SetTrue)
205        .global(false)
206    )
207    .arg(Arg::new("dns-tcp")
208        .long("dns-tcp")
209        .help("Use TCP instead of UDP for DNS queries")
210        .required(false)
211        .action(ArgAction::SetTrue)
212        .global(false)
213    )
214    .arg(Arg::new("zip")
215        .long("zip")
216        .short('z')
217        .help("Compress the JSON files into a zip archive")
218        .required(false)
219        .action(ArgAction::SetTrue)
220        .global(false)
221    )
222    .arg(Arg::new("cache")
223        .long("cache")
224        .help("Cache LDAP search results to disk (reduce memory usage on large domains)")
225        .required(false)
226        .action(ArgAction::SetTrue)
227    )
228    .arg(Arg::new("cache_buffer")
229        .long("cache-buffer")
230        .help("Buffer size to use when caching")
231        .required(false)
232        .value_parser(value_parser!(usize))
233        .default_value("1000")
234    )
235    .arg(Arg::new("resume")
236        .long("resume")
237        .help("Resume the collection from the last saved state")
238        .required(false)
239        .action(ArgAction::SetTrue)
240    )
241    .next_help_heading("OPTIONAL MODULES")
242    .arg(Arg::new("fqdn-resolver")
243        .long("fqdn-resolver")
244        .help("Use fqdn-resolver module to get computers IP address")
245        .required(false)
246        .action(ArgAction::SetTrue)
247        .global(false)
248    )
249}
250
251#[cfg(not(feature = "noargs"))]
252/// Function to extract all argument and put it in 'Options' structure.
253pub fn extract_args() -> Options {
254
255    // Get arguments
256    let matches = cli().get_matches();
257
258    // Now get values
259    let d = matches
260        .get_one::<String>("domain")
261        .map(|s| s.as_str())
262        .unwrap();
263    let username = matches
264        .get_one::<String>("ldapusername")
265        .map(|s| s.to_owned());
266    let password = matches
267        .get_one::<String>("ldappassword")
268        .map(|s| s.to_owned());
269    let hashes = matches
270        .get_one::<String>("hashes")
271        .map(|s| s.to_owned());
272    let f = matches.get_one::<String>("ldapfqdn").cloned();
273    let ip = matches.get_one::<String>("ldapip").cloned();    
274    let port = match matches.get_one::<String>("ldapport") {
275        Some(val) => val.parse::<u16>().ok(),
276        None => None,
277    };
278    let n = matches
279        .get_one::<String>("name-server")
280        .map(|s| s.as_str())
281        .unwrap_or("not set");
282    let path = matches
283        .get_one::<String>("output")
284        .map(|s| s.as_str())
285        .unwrap_or("./");
286    let ldaps = matches
287        .get_one::<bool>("ldaps")
288        .map(|s| s.to_owned())
289        .unwrap_or(false);
290    let dns_tcp = matches
291        .get_one::<bool>("dns-tcp")
292        .map(|s| s.to_owned())
293        .unwrap_or(false);
294    let z = matches
295        .get_one::<bool>("zip")
296        .map(|s| s.to_owned())
297        .unwrap_or(false);
298    let fqdn_resolver = matches
299        .get_one::<bool>("fqdn-resolver")
300        .map(|s| s.to_owned())
301        .unwrap_or(false);
302    let kerberos = matches
303        .get_one::<bool>("kerberos")
304        .map(|s| s.to_owned())
305        .unwrap_or(false);
306
307    // Certificate authentication paths
308    let pfx = matches.get_one::<String>("pfx").cloned();
309    let pfx_pass = matches.get_one::<String>("pfx-pass").cloned();
310    let crt = matches.get_one::<String>("crt").cloned();
311    let key = matches.get_one::<String>("key").cloned();
312
313    let v = match matches.get_count("v") {
314        0 => log::LevelFilter::Info,
315        1 => log::LevelFilter::Debug,
316        _ => log::LevelFilter::Trace,
317    };
318    let collection_method = match matches
319        .get_one::<String>("collectionmethod")
320        .map(|s| s.as_str())
321        .unwrap_or("All")
322    {
323        "All"           => CollectionMethod::All,
324        "DCOnly"        => CollectionMethod::DCOnly,
325        "Session"       => CollectionMethod::Session,
326        "RegistryOnly"  => CollectionMethod::RegistryOnly,
327        "LdapOnly"      => CollectionMethod::LdapOnly,
328        _               => CollectionMethod::All,
329    };
330    let ldap_filter = matches.get_one::<String>("ldap-filter").map(|s| s.as_str()).unwrap_or("(objectClass=*)");
331
332    let cache = matches.get_flag("cache");
333    let cache_buffer_size = matches
334        .get_one::<usize>("cache_buffer")
335        .copied()
336        .unwrap_or(1000);
337    let resume = matches.get_flag("resume");
338
339    // Return all
340    Options {
341        domain: d.to_string(),
342        username,
343        password,
344        hashes,
345        ldapfqdn: f,
346        ip,
347        port,
348        name_server: n.to_string(),
349        path: path.to_string(),
350        collection_method,
351        ldaps,
352        dns_tcp,
353        fqdn_resolver,
354        kerberos,
355        pfx,
356        pfx_pass,
357        crt,
358        key,
359        zip: z,
360        verbose: v,
361        ldap_filter: ldap_filter.to_string(),
362        cache,
363        cache_buffer_size,
364        resume,
365    }
366}
367
368#[cfg(feature = "noargs")]
369/// Function to automatically get all informations needed and put it in 'Options' structure.
370pub fn auto_args() -> Options {
371
372    // Request registry key to get informations
373    let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
374    let cur_ver = hklm.open_subkey("SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters").unwrap();
375    //Computer\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Domain
376    let domain: String = match cur_ver.get_value("Domain") {
377        Ok(domain) => domain,
378        Err(err) => {
379            panic!("Error: {:?}",err);
380        }
381    };
382    
383    // Get LDAP fqdn
384    let _fqdn: String = run(&format!("nslookup -query=srv _ldap._tcp.{}",&domain));
385    let re = Regex::new(r"hostname.*= (?<ldap_fqdn>[0-9a-zA-Z]{1,})").unwrap();
386    let mut values =  re.captures_iter(&_fqdn);
387    let caps = values.next().unwrap();
388    let fqdn = caps["ldap_fqdn"].to_string();
389
390    // Get LDAP port
391    let re = Regex::new(r"port.*= (?<ldap_port>[0-9]{3,})").unwrap();
392    let mut values =  re.captures_iter(&_fqdn);
393    let caps = values.next().unwrap();
394    let port = match caps["ldap_port"].to_string().parse::<u16>() {
395        Ok(x) => Some(x),
396        Err(_) => None
397    };
398    let ldaps: bool = {
399        if let Some(p) = port {
400            p == 636
401        } else {
402            false
403        }
404    };
405
406    // Return all
407    Options {
408        domain: domain.to_string(),
409        username: "not set".to_string(),
410        password: "not set".to_string(),
411        ldapfqdn: Some(fqdn.to_string()),
412        ip: None, 
413        port: port,
414        name_server: "127.0.0.1".to_string(),
415        path: "./output".to_string(),
416        collection_method: CollectionMethod::All,
417        ldaps: ldaps,
418        dns_tcp: false,
419        fqdn_resolver: false,
420        hashes: None,
421        kerberos: true,
422        pfx: None,
423        pfx_pass: None,
424        crt: None,
425        key: None,
426        zip: true,
427        verbose: log::LevelFilter::Info,
428        ldap_filter: "(objectClass=*)".to_string(),
429        cache: false,
430        cache_buffer_size: 1000,
431        resume: false,
432    }
433}