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