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