Skip to main content

rusthound_ce/modules/
mod.rs

1//! List of RustHound add-on modules
2pub mod adcs;
3pub mod gpo;
4pub mod resolver;
5pub mod sessions;
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(common_args: &Options, ad: &mut ADResults) -> Result<(), Box<dyn Error>> {
17    // [MODULE - RESOLVER] Resolve FQDN to IP address.
18    if common_args.fqdn_resolver {
19        resolver::resolv::resolving_all_fqdn(
20            common_args.dns_tcp,
21            &common_args.name_server,
22            &mut ad.mappings.fqdn_ip,
23            &ad.computers,
24        )
25        .await;
26    }
27
28    // [MODULE - SESSIONS] Just does user session collection
29    // <https://github.com/g0h4n/HasSession-rs>
30    //
31    // - SRVSVC / NetrSessionEnum - inbound SMB sessions (client IP + username).
32    // - WKSSVC / NetrWkstaUserEnum - users with an active logon context on the machine.
33    // - WINREG / HKEY_USERS - SIDs of loaded profile hives (= logged-on users).
34    if common_args.collection_method.does_sessions() {
35        sessions::run(common_args, &ad.users, &mut ad.computers).await?;
36    }
37
38    // [MODULE - ESC8] Web enrollment probe on all enterprise CAs.
39    // Skipped in DCOnly mode (no direct machine connections allowed).
40    // Uses rayon to probe all CAs in parallel (each probe has a 5 s timeout).
41    if !matches!(common_args.collection_method, CollectionMethod::DCOnly)
42        && !matches!(common_args.collection_method, CollectionMethod::LdapOnly)
43        && !ad.enterprisecas.is_empty()
44    {
45        log::info!(
46            "Starting ESC8 web enrollment probe on {} CA(s)...",
47            ad.enterprisecas.len()
48        );
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 computer_scope = gpo::sysvol::ComputerGpoScope::from_gpos(&ad.gpos);
59        let sysvol = match collect_sysvol_targets(common_args, &computer_scope).await {
60            Ok(v) => v,
61            Err(e) => {
62                log::warn!("[gpo] SYSVOL collection failed: {e}");
63                Vec::new()
64            }
65        };
66        if !sysvol.is_empty() {
67            log::info!(
68                "[gpo] mapping {} GPO(s) to GPOChanges / UserRights",
69                sysvol.len()
70            );
71            gpo::apply_gpo(
72                &mut ad.ous,
73                &mut ad.domains,
74                &ad.users,
75                &ad.groups,
76                &mut ad.computers,
77                &sysvol,
78                &ad.mappings.dn_sid,
79            );
80        }
81    }
82
83    // Other modules need to be add here...
84    Ok(())
85}
86
87/// Build the SMB target and credentials, then collect GPO directives off SYSVOL.
88async fn collect_sysvol_targets(
89    common_args: &Options,
90    computer_scope: &gpo::sysvol::ComputerGpoScope,
91) -> anyhow::Result<Vec<gpo::SysvolGpo>> {
92    use crate::transport::smb::{nt_hash_from_str, SmbAuth};
93
94    let user = common_args.username.clone().unwrap_or_default();
95    let password = common_args.password.clone().unwrap_or_default();
96    let nt = common_args.hashes.as_deref().and_then(nt_hash_from_str);
97    let auth = match &nt {
98        Some(h) => SmbAuth::Hash(h),
99        None => SmbAuth::Password(&password),
100    };
101
102    // SMB target: explicit IP first, then the DC FQDN, then the domain value.
103    let dc_host = common_args
104        .ip
105        .as_deref()
106        .filter(|s| !s.is_empty())
107        .or(common_args.ldapfqdn.as_deref())
108        .map(str::to_string)
109        .unwrap_or_else(|| common_args.domain.clone());
110
111    if dc_host.is_empty() {
112        log::warn!(
113            "[gpo] no SMB target (ip/ldapfqdn/domain) available, skipping SYSVOL collection"
114        );
115        return Ok(Vec::new());
116    }
117
118    // domain_fqdn (SYSVOL sub-root) = the domain DNS name.
119    gpo::collect_sysvol(
120        &dc_host,
121        &common_args.domain,
122        &common_args.domain,
123        &user,
124        auth,
125        computer_scope,
126    )
127    .await
128}