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