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