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