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