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