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