1use std::collections::HashMap;
30use std::error::Error;
31use std::sync::Arc;
32
33use futures::stream::{self, StreamExt};
34use log::{debug, info, trace, warn};
35use tokio::sync::Semaphore;
36use tokio::time::{timeout, Duration};
37
38use dcerpc::rrp::{RegistryClient, RegistrySession};
39use dcerpc::srvsvc::SrvsvcClient;
40use dcerpc::wkssvc::{WkstaUser, WkstaUserClient};
41use smb2_client::SmbClient;
42use crate::transport::smb::{connect_ipc, is_reachable, open_rpc_pipe, smb_user, SmbAuth};
43
44use crate::args::{CollectionMethod, Options};
45use crate::objects::common::UserComputerSession;
46use crate::objects::computer::Computer;
47use crate::objects::user::User;
48
49const DEFAULT_CONCURRENCY: usize = 10; const DEFAULT_PORT_TIMEOUT_MS: u64 = 1_500; const DEFAULT_HOST_TIMEOUT_MS: u64 = 8_000; const DEFAULT_EXPIRY_DAYS: i64 = 60; struct SmbSession { user: String, _client: String }
59
60struct HostFindings {
61 computer_sid: String,
62 smb_sessions: Vec<SmbSession>, logged_on: Vec<WkstaUser>, registry: Vec<RegistrySession>, errors: Vec<String>,
66}
67
68pub async fn run(
70 args: &Options,
71 users: &[User], computers: &mut Vec<Computer>,
73) -> Result<(), Box<dyn Error>> {
74 if !args.collection_method.does_session() {
76 debug!("[sessions] collection method does not contact hosts - skipping");
77 return Ok(());
78 }
79
80 trace!("[sessions] building SID index from {} user(s)", users.len());
82 let sid_index = build_sid_index(users);
83 trace!("[sessions] SID index built");
84
85 let expiry_days = DEFAULT_EXPIRY_DAYS;
88 let targets: Vec<(String, String)> = computers
89 .iter()
90 .filter(|c| c.is_active(expiry_days))
91 .map(|c| (c.properties().name().clone(), c.object_identifier().clone()))
92 .collect();
93
94 info!("[sessions] {} active target(s) after expiry/enabled filter", targets.len());
95 for (h, _) in &targets {
96 trace!("[sessions] target host: {h}");
97 }
98
99 let sem = Arc::new(Semaphore::new(DEFAULT_CONCURRENCY));
101 let domain = args.domain.clone();
102 let user = smb_user(args.username.as_deref().unwrap_or_default());
103 let password = args.password.clone().unwrap_or_default();
104 let nt_hash = parse_hash(args.hashes.as_deref()); let method = args.collection_method.clone();
106
107 let kerberos_ccache: Option<String> = if args.kerberos {
109 std::env::var("KRB5CCNAME").ok()
110 } else {
111 None
112 };
113 let kdc: String = args
115 .ldapfqdn
116 .clone()
117 .filter(|s| !s.is_empty())
118 .or_else(|| args.ip.clone())
119 .unwrap_or_else(|| args.domain.clone());
120
121 trace!("[sessions] starting concurrent enumeration (concurrency={DEFAULT_CONCURRENCY})");
122 let findings: Vec<HostFindings> = stream::iter(targets)
123 .map(|(host, computer_sid)| {
124 let (sem, domain, user, password, method) =
125 (sem.clone(), domain.clone(), user.clone(), password.clone(), method.clone());
126 let nt_hash = nt_hash;
127 let kerberos_ccache = kerberos_ccache.clone();
128 let kdc = kdc.clone();
129 async move {
130 trace!("[sessions] [{host}] task scheduled, awaiting concurrency permit");
131 let _permit = sem.acquire().await.unwrap();
132 trace!("[sessions] [{host}] permit acquired, entering enumerate_host");
133 let hf = enumerate_host(
134 &host, computer_sid, &domain, &user, &password,
135 nt_hash.as_ref(), kerberos_ccache.as_deref(), &kdc, &method,
136 ).await;
137 trace!("[sessions] [{host}] enumerate_host returned ({} error(s))", hf.errors.len());
138 hf
139 }
140 })
141 .buffer_unordered(DEFAULT_CONCURRENCY)
142 .collect()
143 .await;
144
145 trace!("[sessions] all {} host task(s) completed, folding findings", findings.len());
146
147 let mut total_sessions = 0usize;
149 for hf in &findings {
150 total_sessions += apply_findings(computers, hf, &sid_index, &args.domain);
151 for e in &hf.errors { warn!("{e}"); }
152 }
153 info!("[sessions] {total_sessions} session(s) enumerated in total across {} host(s)",
154 findings.len());
155
156 Ok(())
157}
158
159#[allow(clippy::too_many_arguments)]
161async fn enumerate_host(
162 host: &str, computer_sid: String,
163 domain: &str, user: &str, password: &str,
164 nt_hash: Option<&[u8; 16]>,
165 kerberos_ccache: Option<&str>,
166 kdc: &str,
167 method: &CollectionMethod,
168) -> HostFindings {
169 trace!("[sessions] [{host}] enumerate_host: begin, running 445 pre-check");
170 if !is_reachable(host, DEFAULT_PORT_TIMEOUT_MS).await {
172 trace!("[{host}] 445/tcp unreachable - skip");
173 return HostFindings {
174 computer_sid,
175 smb_sessions: Vec::new(), logged_on: Vec::new(), registry: Vec::new(),
176 errors: vec![format!("{host}: 445/tcp unreachable")],
177 };
178 }
179
180 let work = async {
183 let mut smb_sessions = Vec::new();
184 let mut logged_on = Vec::new();
185 let mut registry = Vec::new();
186 let mut errors = Vec::new();
187
188 let fatal: Result<(), String> = async {
191 let mut smb = if let Some(ccache) = kerberos_ccache {
193 let spn = format!("cifs/{host}");
195 let (gss_blob, session_key) =
196 crate::transport::kerberos::kerberos_material_for(ccache, &spn, kdc)
197 .await
198 .map_err(|e| format!("{host} krb: {e}"))?;
199 let auth = SmbAuth::Kerberos { gss_blob: &gss_blob, session_key: &session_key };
200 connect_ipc(host, domain, user, auth).await.map_err(|e| format!("{host}: {e}"))?
201 } else {
202 let auth = match nt_hash {
203 Some(h) => SmbAuth::Hash(h),
204 None => SmbAuth::Password(password),
205 };
206 connect_ipc(host, domain, user, auth).await.map_err(|e| format!("{host}: {e}"))?
207 };
208
209 if method.srvsvc() {
210 match srvsvc_sessions(&mut smb, host).await {
211 Ok((_, 5)) => errors.push(format!("[{host}] SRVSVC rc=5 ACCESS_DENIED (hardened / non-admin)")),
212 Ok((s, _)) => smb_sessions = s,
213 Err(e) => errors.push(format!("{host} SRVSVC: {e}")),
214 }
215 }
216 if method.wkssvc() {
217 match enum_wksta(&mut smb, host).await {
218 Ok((_, 5)) => errors.push(format!("[{host}] WKSSVC rc=5 (local admin required)")),
219 Ok((u, _)) => logged_on = dedup_wksta(u),
220 Err(e) => errors.push(format!("{host} WKSSVC: {e}")),
221 }
222 }
223 if method.registry() {
224 match enum_registry(&mut smb, domain, user, password, nt_hash, host).await {
225 Ok(sids) => registry = sids,
226 Err(e) => errors.push(format!("{host} WINREG: {e} (RemoteRegistry stopped?)")),
227 }
228 }
229 Ok(())
230 }.await;
231
232 if let Err(e) = fatal { errors.push(e); }
233 (smb_sessions, logged_on, registry, errors)
234 };
235
236 match timeout(Duration::from_millis(DEFAULT_HOST_TIMEOUT_MS), work).await {
238 Ok((smb_sessions, logged_on, registry, errors)) => HostFindings {
239 computer_sid, smb_sessions, logged_on, registry, errors,
240 },
241 Err(_elapsed) => HostFindings {
242 computer_sid,
243 smb_sessions: Vec::new(), logged_on: Vec::new(), registry: Vec::new(),
244 errors: vec![format!("{host}: per-host timeout")],
245 },
246 }
247}
248
249async fn srvsvc_sessions(smb: &mut SmbClient, host: &str) -> anyhow::Result<(Vec<SmbSession>, u32)> {
254 let pipe = open_rpc_pipe(smb, host, "srvsvc").await?;
255 let mut srv = SrvsvcClient::bind(smb, pipe).await?;
256 let (sessions, rc) = srv.enum_sessions().await?;
257 Ok((sessions.into_iter()
258 .map(|s| SmbSession { user: s.user, _client: s.client }).collect(), rc))
259}
260
261async fn enum_wksta(smb: &mut SmbClient, host: &str) -> anyhow::Result<(Vec<WkstaUser>, u32)> {
262 let pipe = open_rpc_pipe(smb, host, "wkssvc").await?;
263 let mut wk = WkstaUserClient::bind(smb, pipe).await?;
264 Ok(wk.enum_users().await?)
265}
266
267async fn enum_registry(smb: &mut SmbClient, domain: &str, user: &str,
268 password: &str, nt_hash: Option<&[u8; 16]>, host: &str)
269 -> anyhow::Result<Vec<RegistrySession>>
270{
271 let mut reg = match nt_hash {
272 Some(h) => RegistryClient::connect_hash(smb, domain, user, h, host).await
273 .map_err(|e| anyhow::anyhow!("{e}"))?,
274 None => RegistryClient::connect(smb, domain, user, password, host).await
275 .map_err(|e| anyhow::anyhow!("{e}"))?,
276 };
277 reg.logged_on_sids().await.map_err(|e| anyhow::anyhow!("{e}"))
278}
279
280fn build_sid_index(users: &[User]) -> HashMap<String, String> {
287 let mut idx = HashMap::with_capacity(users.len() * 2);
288 for u in users {
289 let sid = u.object_identifier().clone();
290 if sid.is_empty() { continue; }
291 let upn = u.properties().name().to_uppercase(); if let Some(sam) = upn.split('@').next() {
293 idx.entry(sam.to_string()).or_insert_with(|| sid.clone());
294 }
295 idx.insert(upn, sid);
296 }
297 idx
298}
299
300fn resolve(raw: &str, idx: &HashMap<String, String>, domain: &str) -> Option<String> {
305 let bare = raw.rsplit(['\\', '/']).next().unwrap_or(raw).trim();
306 if bare.is_empty() || bare == "?" || bare.ends_with('$') {
307 return None;
308 }
309 let sam = bare.to_uppercase();
310 let upn = format!("{sam}@{}", domain.to_uppercase());
311 idx.get(&upn).or_else(|| idx.get(&sam)).cloned()
312}
313
314fn mk_link(user_sid: String, computer_sid: String) -> UserComputerSession {
316 let mut ucs = UserComputerSession::new();
317 *ucs.user_sid_mut() = user_sid;
318 *ucs.computer_sid_mut() = computer_sid;
319 ucs
320}
321
322fn dedup_wksta(users: Vec<WkstaUser>) -> Vec<WkstaUser> {
324 use std::collections::BTreeSet;
325 let mut seen = BTreeSet::new();
326 users.into_iter()
327 .filter(|u| !u.username.ends_with('$'))
328 .filter(|u| seen.insert((u.logon_domain.clone(), u.username.clone())))
329 .collect()
330}
331
332fn apply_findings(
342 computers: &mut [Computer],
343 hf: &HostFindings,
344 sid_index: &HashMap<String, String>,
345 domain: &str,
346) -> usize {
347 let computer = match computers
348 .iter_mut()
349 .find(|c| c.object_identifier() == &hf.computer_sid)
350 {
351 Some(c) => c,
352 None => {
353 warn!("[sessions] no computer object for SID {}", hf.computer_sid);
354 return 0;
355 }
356 };
357 let comp_sid = hf.computer_sid.clone();
358 let fqdn = computer.properties().name().clone(); let mut count = 0usize;
360
361 {
363 let s = computer.sessions_mut();
364 for sess in &hf.smb_sessions {
365 match resolve(&sess.user, sid_index, domain) {
366 Some(user_sid) => {
367 trace!("[SRVSVC] {} has session on {fqdn}", sess.user);
368 s.results_mut().push(mk_link(user_sid, comp_sid.clone()));
369 count += 1;
370 }
371 None => warn!("[{comp_sid}] unresolved SRVSVC principal '{}'", sess.user),
372 }
373 }
374 *s.collected_mut() = true;
375 }
376
377 {
379 let p = computer.privileged_sessions_mut();
380 for u in &hf.logged_on {
381 match resolve(&u.username, sid_index, domain) {
382 Some(user_sid) => {
383 trace!("[WKSSVC] {}\\{} has session on {fqdn}", u.logon_domain, u.username);
384 p.results_mut().push(mk_link(user_sid, comp_sid.clone()));
385 count += 1;
386 }
387 None => warn!("[{comp_sid}] unresolved WKSSVC principal '{}\\{}'",
388 u.logon_domain, u.username),
389 }
390 }
391 *p.collected_mut() = true;
392 }
393
394 {
396 let r = computer.registry_sessions_mut();
397 for reg in &hf.registry {
398 if reg.sid.is_empty() { continue; }
399 trace!("[WINREG] {} has session on {fqdn}", reg.sid);
400 r.results_mut().push(mk_link(reg.sid.clone(), comp_sid.clone()));
401 count += 1;
402 }
403 *r.collected_mut() = true;
404 }
405
406 debug!("[sessions] Total {count} session(s) on {fqdn}");
407 count
408}
409
410fn parse_hash(h: Option<&str>) -> Option<[u8; 16]> {
415 let raw = h?.trim();
416 let nt = raw.rsplit(':').next().unwrap_or(raw).trim();
417 if nt.len() != 32 || !nt.bytes().all(|b| b.is_ascii_hexdigit()) {
418 return None;
419 }
420 let mut out = [0u8; 16];
421 for (i, byte) in out.iter_mut().enumerate() {
422 *byte = u8::from_str_radix(&nt[i * 2..i * 2 + 2], 16).ok()?;
423 }
424 Some(out)
425}