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