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