#[cfg(not(feature = "noargs"))]
use clap::{Arg, ArgAction, value_parser, Command};
#[cfg(feature = "noargs")]
use winreg::{RegKey,{enums::*}};
#[cfg(feature = "noargs")]
use crate::utils::exec::run;
#[cfg(feature = "noargs")]
use regex::Regex;
#[derive(Clone, Debug)]
pub struct Options {
pub domain: String,
pub username: String,
pub password: String,
pub ldapfqdn: String,
pub ip: Option<String>,
pub port: Option<u16>,
pub name_server: String,
pub path: String,
pub collection_method: CollectionMethod,
pub ldaps: bool,
pub dns_tcp: bool,
pub fqdn_resolver: bool,
pub kerberos: bool,
pub zip: bool,
pub verbose: log::LevelFilter,
}
#[derive(Clone, Debug)]
pub enum CollectionMethod {
All,
DCOnly,
}
pub const RUSTHOUND_VERSION: &str = "2.3.2";
#[cfg(not(feature = "noargs"))]
fn cli() -> Command {
let cmd = Command::new("rusthound-ce")
.version(RUSTHOUND_VERSION)
.about("Active Directory data collector for BloodHound Community Edition.\ng0h4n <https://twitter.com/g0h4n_0>")
.arg(Arg::new("v")
.short('v')
.help("Set the level of verbosity")
.action(ArgAction::Count),
)
.next_help_heading("REQUIRED VALUES")
.arg(Arg::new("domain")
.short('d')
.long("domain")
.help("Domain name like: DOMAIN.LOCAL")
.required(true)
.value_parser(value_parser!(String))
)
.next_help_heading("OPTIONAL VALUES")
.arg(Arg::new("ldapusername")
.short('u')
.long("ldapusername")
.help("LDAP username, like: user@domain.local")
.required(false)
.value_parser(value_parser!(String))
)
.arg(Arg::new("ldappassword")
.short('p')
.long("ldappassword")
.help("LDAP password")
.required(false)
.value_parser(value_parser!(String))
)
.arg(Arg::new("ldapfqdn")
.short('f')
.long("ldapfqdn")
.help("Domain Controller FQDN like: DC01.DOMAIN.LOCAL or just DC01")
.required(false)
.value_parser(value_parser!(String))
)
.arg(Arg::new("ldapip")
.short('i')
.long("ldapip")
.help("Domain Controller IP address like: 192.168.1.10")
.required(false)
.value_parser(value_parser!(String))
)
.arg(Arg::new("ldapport")
.short('P')
.long("ldapport")
.help("LDAP port [default: 389]")
.required(false)
.value_parser(value_parser!(String))
)
.arg(Arg::new("name-server")
.short('n')
.long("name-server")
.help("Alternative IP address name server to use for DNS queries")
.required(false)
.value_parser(value_parser!(String))
)
.arg(Arg::new("output")
.short('o')
.long("output")
.help("Output directory where you would like to save JSON files [default: ./]")
.required(false)
.value_parser(value_parser!(String))
)
.next_help_heading("OPTIONAL FLAGS")
.arg(Arg::new("collectionmethod")
.short('c')
.long("collectionmethod")
.help("Which information to collect. Supported: All (LDAP,SMB,HTTP requests), DCOnly (no computer connections, only LDAP requests). (default: All)")
.required(false)
.value_name("COLLECTIONMETHOD")
.value_parser(["All", "DCOnly"])
.num_args(0..=1)
.default_missing_value("All")
)
.arg(Arg::new("ldaps")
.long("ldaps")
.help("Force LDAPS using for request like: ldaps://DOMAIN.LOCAL/")
.required(false)
.action(ArgAction::SetTrue)
.global(false)
)
.arg(Arg::new("kerberos")
.short('k')
.long("kerberos")
.help("Use Kerberos authentication. Grabs credentials from ccache file (KRB5CCNAME) based on target parameters for Linux.")
.required(false)
.action(ArgAction::SetTrue)
.global(false)
)
.arg(Arg::new("dns-tcp")
.long("dns-tcp")
.help("Use TCP instead of UDP for DNS queries")
.required(false)
.action(ArgAction::SetTrue)
.global(false)
)
.arg(Arg::new("zip")
.long("zip")
.short('z')
.help("Compress the JSON files into a zip archive")
.required(false)
.action(ArgAction::SetTrue)
.global(false)
)
.next_help_heading("OPTIONAL MODULES")
.arg(Arg::new("fqdn-resolver")
.long("fqdn-resolver")
.help("Use fqdn-resolver module to get computers IP address")
.required(false)
.action(ArgAction::SetTrue)
.global(false)
);
cmd
}
#[cfg(not(feature = "noargs"))]
pub fn extract_args() -> Options {
let matches = cli().get_matches();
let d = matches.get_one::<String>("domain").map(|s| s.as_str()).unwrap();
let u = matches.get_one::<String>("ldapusername").map(|s| s.as_str()).unwrap_or("not set");
let p = matches.get_one::<String>("ldappassword").map(|s| s.as_str()).unwrap_or("not set");
let f = matches.get_one::<String>("ldapfqdn").map(|s| s.as_str()).unwrap_or("not set");
let ip = matches.get_one::<String>("ldapip").map(|s| s.clone());
let port = match matches.get_one::<String>("ldapport") {
Some(val) => {
match val.parse::<u16>() {
Ok(x) => Some(x),
Err(_) => None,
}
},
None => None
};
let n = matches.get_one::<String>("name-server").map(|s| s.as_str()).unwrap_or("not set");
let path = matches.get_one::<String>("output").map(|s| s.as_str()).unwrap_or("./");
let ldaps = matches.get_one::<bool>("ldaps").map(|s| s.to_owned()).unwrap_or(false);
let dns_tcp = matches.get_one::<bool>("dns-tcp").map(|s| s.to_owned()).unwrap_or(false);
let z = matches.get_one::<bool>("zip").map(|s| s.to_owned()).unwrap_or(false);
let fqdn_resolver = matches.get_one::<bool>("fqdn-resolver").map(|s| s.to_owned()).unwrap_or(false);
let kerberos = matches.get_one::<bool>("kerberos").map(|s| s.to_owned()).unwrap_or(false);
let v = match matches.get_count("v") {
0 => log::LevelFilter::Info,
1 => log::LevelFilter::Debug,
_ => log::LevelFilter::Trace,
};
let collection_method = match matches.get_one::<String>("collectionmethod").map(|s| s.as_str()).unwrap_or("All") {
"All" => CollectionMethod::All,
"DCOnly" => CollectionMethod::DCOnly,
_ => CollectionMethod::All,
};
Options {
domain: d.to_string(),
username: u.to_string(),
password: p.to_string(),
ldapfqdn: f.to_string(),
ip: ip,
port: port,
name_server: n.to_string(),
path: path.to_string(),
collection_method: collection_method,
ldaps: ldaps,
dns_tcp: dns_tcp,
fqdn_resolver: fqdn_resolver,
kerberos: kerberos,
zip: z,
verbose: v,
}
}
#[cfg(feature = "noargs")]
pub fn auto_args() -> Options {
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let cur_ver = hklm.open_subkey("SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters").unwrap();
let domain: String = match cur_ver.get_value("Domain") {
Ok(domain) => domain,
Err(err) => {
panic!("Error: {:?}",err);
}
};
let _fqdn: String = run(&format!("nslookup -query=srv _ldap._tcp.{}",&domain));
let re = Regex::new(r"hostname.*= (?<ldap_fqdn>[0-9a-zA-Z]{1,})").unwrap();
let mut values = re.captures_iter(&_fqdn);
let caps = values.next().unwrap();
let fqdn = caps["ldap_fqdn"].to_string();
let re = Regex::new(r"port.*= (?<ldap_port>[0-9]{3,})").unwrap();
let mut values = re.captures_iter(&_fqdn);
let caps = values.next().unwrap();
let port = match caps["ldap_port"].to_string().parse::<u16>() {
Ok(x) => Some(x),
Err(_) => None
};
let ldaps: bool = {
if let Some(p) = port {
p == 636
} else {
false
}
};
Options {
domain: domain.to_string(),
username: "not set".to_string(),
password: "not set".to_string(),
ldapfqdn: fqdn.to_string(),
ip: None,
port: port,
name_server: "127.0.0.1".to_string(),
path: "./output".to_string(),
collection_method: CollectionMethod::All,
ldaps: ldaps,
dns_tcp: false,
fqdn_resolver: false,
kerberos: true,
zip: true,
verbose: log::LevelFilter::Info,
}
}