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