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