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