Skip to main content

rusthound_ce/modules/
mod.rs

1//! List of RustHound add-on modules
2pub mod gpo;
3pub mod resolver;
4pub mod sessions;
5pub mod adcs;
6
7use std::error::Error;
8
9use rayon::prelude::*;
10
11use crate::api::ADResults;
12use crate::args::{CollectionMethod, Options};
13use crate::modules::adcs::probe_enterpriseca_esc8;
14
15/// Function to run all modules requested
16pub async fn run_modules(
17    common_args: &Options,
18    ad: &mut ADResults,
19) -> Result<(), Box<dyn Error>> {
20
21   // [MODULE - RESOLVER] Resolve FQDN to IP address.
22   if common_args.fqdn_resolver {
23        resolver::resolv::resolving_all_fqdn(
24            common_args.dns_tcp,
25            &common_args.name_server,
26            &mut ad.mappings.fqdn_ip,
27            &ad.computers,
28        ).await;
29   }
30
31   // [MODULE - SESSIONS] Just does user session collection 
32   // <https://github.com/g0h4n/HasSession-rs>
33   //
34   // - SRVSVC / NetrSessionEnum - inbound SMB sessions (client IP + username).
35   // - WKSSVC / NetrWkstaUserEnum - users with an active logon context on the machine.
36   // - WINREG / HKEY_USERS - SIDs of loaded profile hives (= logged-on users).
37   if common_args.collection_method.does_sessions() {
38        sessions::run(common_args, &ad.users, &mut ad.computers).await?;
39   }
40
41   // [MODULE - ESC8] Web enrollment probe on all enterprise CAs.
42   // Skipped in DCOnly mode (no direct machine connections allowed).
43   // Uses rayon to probe all CAs in parallel (each probe has a 5 s timeout).
44   if !matches!(common_args.collection_method, CollectionMethod::DCOnly) 
45      && !matches!(common_args.collection_method, CollectionMethod::LdapOnly) 
46      && !ad.enterprisecas.is_empty() 
47   {
48      log::info!("Starting ESC8 web enrollment probe on {} CA(s)...", ad.enterprisecas.len());
49      ad.enterprisecas.par_iter_mut().for_each(|ca| {
50         let esc8 = probe_enterpriseca_esc8(ca.dns_host());
51         ca.apply_esc8(esc8.http_enrollment_endpoints);
52      });
53   }
54
55    // [MODULE - GPO SYSVOL] read GptTmpl.inf / Groups.xml off the DC SYSVOL share.
56    // <#47 Privileges> and <#56 LocalGroup>. DC-side I/O, so it also runs in DCOnly.
57    if common_args.collection_method.does_gpo() {
58      let sysvol = match collect_sysvol_targets(common_args).await {
59         Ok(v) => v,
60         Err(e) => {
61            log::warn!("[gpo] SYSVOL collection failed: {e}");
62            Vec::new()
63         }
64      };
65      if !sysvol.is_empty() {
66         log::info!("[gpo] mapping {} GPO(s) to GPOChanges / UserRights", sysvol.len());
67         gpo::apply_gpo(
68            &mut ad.ous,
69            &mut ad.domains,
70            &ad.users,
71            &ad.groups,
72            &mut ad.computers,
73            &sysvol,
74            &ad.mappings.dn_sid,
75         );
76      }
77   }
78
79   // Other modules need to be add here...
80   Ok(())
81}
82
83/// Build the SMB target and credentials, then collect GPO directives off SYSVOL.
84async fn collect_sysvol_targets(common_args: &Options) -> anyhow::Result<Vec<gpo::SysvolGpo>> {
85    use crate::transport::smb::{nt_hash_from_str, SmbAuth};
86
87    let user = common_args.username.clone().unwrap_or_default();
88    let password = common_args.password.clone().unwrap_or_default();
89    let nt = common_args.hashes.as_deref().and_then(nt_hash_from_str);
90    let auth = match &nt {
91        Some(h) => SmbAuth::Hash(h),
92        None => SmbAuth::Password(&password),
93    };
94
95    // SMB target: explicit IP first, then the DC FQDN, then the domain value.
96   let dc_host = common_args
97      .ip
98      .as_deref()
99      .filter(|s| !s.is_empty())
100      .or(common_args.ldapfqdn.as_deref())
101      .map(str::to_string)
102      .unwrap_or_else(|| common_args.domain.clone());
103
104    if dc_host.is_empty() {
105        log::warn!("[gpo] no SMB target (ip/ldapfqdn/domain) available, skipping SYSVOL collection");
106        return Ok(Vec::new());
107    }
108
109    // domain_fqdn (SYSVOL sub-root) = the domain DNS name.
110    gpo::collect_sysvol(&dc_host, &common_args.domain, &common_args.domain, &user, auth).await
111}