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