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