1use std::collections::HashMap;
32use std::error::Error;
33use std::sync::Arc;
34
35use futures::stream::{self, StreamExt};
36use log::{debug, info, trace, warn};
37use tokio::net::TcpStream;
38use tokio::sync::Semaphore;
39use tokio::time::{timeout, Duration};
40
41use dcerpc::rrp::{RegistryClient, RegistrySession};
42use dcerpc::srvsvc::SrvsvcClient;
43use dcerpc::wkssvc::{WkstaUser, WkstaUserClient};
44use smb2_client::SmbClient;
45use crate::transport::smb::{connect_ipc, open_rpc_pipe, SmbAuth};
46
47use crate::args::{CollectionMethod, Options};
48use crate::objects::common::UserComputerSession;
49use crate::objects::computer::Computer;
50use crate::objects::user::User;
51
52const 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 }
62
63struct HostFindings {
64 computer_sid: String,
65 smb_sessions: Vec<SmbSession>, logged_on: Vec<WkstaUser>, registry: Vec<RegistrySession>, errors: Vec<String>,
69}
70
71pub async fn run(
73 args: &Options,
74 users: &[User], computers: &mut Vec<Computer>,
76) -> Result<(), Box<dyn Error>> {
77 if !args.collection_method.does_sessions() {
79 debug!("[sessions] collection method does not contact hosts - skipping");
80 return Ok(());
81 }
82
83 let sid_index = build_sid_index(users);
85
86 let expiry_days = DEFAULT_EXPIRY_DAYS;
89 let targets: Vec<(String, String)> = computers
90 .iter()
91 .filter(|c| is_active(c, expiry_days))
92 .map(|c| (c.properties().name().clone(), c.object_identifier().clone()))
93 .collect();
94
95 info!("[sessions] {} active target(s) after expiry/enabled filter", targets.len());
96
97 let sem = Arc::new(Semaphore::new(DEFAULT_CONCURRENCY));
99 let domain = args.domain.clone();
100 let user = args.username.clone().unwrap_or_default();
101 let password = args.password.clone().unwrap_or_default();
102 let nt_hash = parse_hash(args.hashes.as_deref()); let method = args.collection_method.clone();
104
105 let findings: Vec<HostFindings> = stream::iter(targets)
106 .map(|(host, computer_sid)| {
107 let (sem, domain, user, password, method) =
108 (sem.clone(), domain.clone(), user.clone(), password.clone(), method.clone());
109 let nt_hash = nt_hash;
110 async move {
111 let _permit = sem.acquire().await.unwrap();
112 enumerate_host(&host, computer_sid, &domain, &user, &password,
113 nt_hash.as_ref(), &method).await
114 }
115 })
116 .buffer_unordered(DEFAULT_CONCURRENCY)
117 .collect()
118 .await;
119
120 let mut total_sessions = 0usize;
122 for hf in &findings {
123 total_sessions += apply_findings(computers, hf, &sid_index, &args.domain);
124 for e in &hf.errors { warn!("{e}"); }
125 }
126 info!("[sessions] {total_sessions} session(s) enumerated in total across {} host(s)",
127 findings.len());
128
129 Ok(())
130}
131
132async fn enumerate_host(
134 host: &str, computer_sid: String,
135 domain: &str, user: &str, password: &str,
136 nt_hash: Option<&[u8; 16]>,
137 method: &CollectionMethod,
138) -> HostFindings {
139 if !is_reachable(host, DEFAULT_PORT_TIMEOUT_MS).await {
141 trace!("[{host}] 445/tcp unreachable - skip");
142 return HostFindings {
143 computer_sid,
144 smb_sessions: Vec::new(), logged_on: Vec::new(), registry: Vec::new(),
145 errors: vec![format!("{host}: 445/tcp unreachable")],
146 };
147 }
148
149 let work = async {
152 let mut smb_sessions = Vec::new();
153 let mut logged_on = Vec::new();
154 let mut registry = Vec::new();
155 let mut errors = Vec::new();
156
157 let fatal: Result<(), String> = async {
160
161 let auth = match nt_hash {
163 Some(h) => SmbAuth::Hash(h),
164 None => SmbAuth::Password(password),
165 };
166 let mut smb = connect_ipc(host, domain, user, auth).await
167 .map_err(|e| format!("{host}: {e}"))?;
168
169 if method.srvsvc() {
170 match srvsvc_sessions(&mut smb, host).await {
171 Ok((_, 5)) => errors.push(format!("[{host}] SRVSVC rc=5 ACCESS_DENIED (hardened / non-admin)")),
172 Ok((s, _)) => smb_sessions = s,
173 Err(e) => errors.push(format!("{host} SRVSVC: {e}")),
174 }
175 }
176 if method.wkssvc() {
177 match enum_wksta(&mut smb, host).await {
178 Ok((_, 5)) => errors.push(format!("[{host}] WKSSVC rc=5 (local admin required)")),
179 Ok((u, _)) => logged_on = dedup_wksta(u),
180 Err(e) => errors.push(format!("{host} WKSSVC: {e}")),
181 }
182 }
183 if method.registry() {
184 match enum_registry(&mut smb, domain, user, password, nt_hash, host).await {
185 Ok(sids) => registry = sids,
186 Err(e) => errors.push(format!("{host} WINREG: {e} (RemoteRegistry stopped?)")),
187 }
188 }
189 Ok(())
190 }.await;
191
192 if let Err(e) = fatal { errors.push(e); }
193 (smb_sessions, logged_on, registry, errors)
194 };
195
196 match timeout(Duration::from_millis(DEFAULT_HOST_TIMEOUT_MS), work).await {
198 Ok((smb_sessions, logged_on, registry, errors)) => HostFindings {
199 computer_sid, smb_sessions, logged_on, registry, errors,
200 },
201 Err(_elapsed) => HostFindings {
202 computer_sid,
203 smb_sessions: Vec::new(), logged_on: Vec::new(), registry: Vec::new(),
204 errors: vec![format!("{host}: per-host timeout")],
205 },
206 }
207}
208
209async fn is_reachable(host: &str, port_timeout_ms: u64) -> bool {
211 matches!(
212 timeout(Duration::from_millis(port_timeout_ms),
213 TcpStream::connect(format!("{host}:445"))).await,
214 Ok(Ok(_))
215 )
216}
217
218fn is_active(c: &Computer, expiry_days: i64) -> bool {
220 if !*c.properties().enabled() { return false; }
221 let pls = c.properties().pwdlastset(); if pls <= 0 { return false; }
223 let now = chrono::Utc::now().timestamp();
224 now - pls < expiry_days * 86_400
225}
226
227async fn srvsvc_sessions(smb: &mut SmbClient, host: &str) -> anyhow::Result<(Vec<SmbSession>, u32)> {
229 let pipe = open_rpc_pipe(smb, host, "srvsvc").await?;
230 let mut srv = SrvsvcClient::bind(smb, pipe).await?;
231 let (sessions, rc) = srv.enum_sessions().await?;
232 Ok((sessions.into_iter()
233 .map(|s| SmbSession { user: s.user, _client: s.client }).collect(), rc))
234}
235
236async fn enum_wksta(smb: &mut SmbClient, host: &str) -> anyhow::Result<(Vec<WkstaUser>, u32)> {
237 let pipe = open_rpc_pipe(smb, host, "wkssvc").await?;
238 let mut wk = WkstaUserClient::bind(smb, pipe).await?;
239 Ok(wk.enum_users().await?)
240}
241
242async fn enum_registry(smb: &mut SmbClient, domain: &str, user: &str,
243 password: &str, nt_hash: Option<&[u8; 16]>, host: &str)
244 -> anyhow::Result<Vec<RegistrySession>>
245{
246 let mut reg = match nt_hash {
247 Some(h) => RegistryClient::connect_hash(smb, domain, user, h, host).await
248 .map_err(|e| anyhow::anyhow!("{e}"))?,
249 None => RegistryClient::connect(smb, domain, user, password, host).await
250 .map_err(|e| anyhow::anyhow!("{e}"))?,
251 };
252 reg.logged_on_sids().await.map_err(|e| anyhow::anyhow!("{e}"))
253}
254
255fn build_sid_index(users: &[User]) -> HashMap<String, String> {
262 let mut idx = HashMap::with_capacity(users.len() * 2);
263 for u in users {
264 let sid = u.object_identifier().clone(); if sid.is_empty() { continue; }
266 let upn = u.properties().name().to_uppercase(); if let Some(sam) = upn.split('@').next() {
268 idx.entry(sam.to_string()).or_insert_with(|| sid.clone());
270 }
271 idx.insert(upn, sid);
272 }
273 idx
274}
275
276fn resolve(raw: &str, idx: &HashMap<String, String>, domain: &str) -> Option<String> {
281 let bare = raw.rsplit(['\\', '/']).next().unwrap_or(raw).trim();
282 if bare.is_empty() || bare == "?" || bare.ends_with('$') {
283 return None;
284 }
285 let sam = bare.to_uppercase();
286 let upn = format!("{sam}@{}", domain.to_uppercase());
287 idx.get(&upn).or_else(|| idx.get(&sam)).cloned()
288}
289
290fn mk_link(user_sid: String, computer_sid: String) -> UserComputerSession {
292 let mut ucs = UserComputerSession::new();
293 *ucs.user_sid_mut() = user_sid;
294 *ucs.computer_sid_mut() = computer_sid;
295 ucs
296}
297
298fn dedup_wksta(users: Vec<WkstaUser>) -> Vec<WkstaUser> {
300 use std::collections::BTreeSet;
301 let mut seen = BTreeSet::new();
302 users.into_iter()
303 .filter(|u| !u.username.ends_with('$'))
304 .filter(|u| seen.insert((u.logon_domain.clone(), u.username.clone())))
305 .collect()
306}
307
308fn apply_findings(
318 computers: &mut [Computer],
319 hf: &HostFindings,
320 sid_index: &HashMap<String, String>,
321 domain: &str,
322) -> usize {
323 let computer = match computers
324 .iter_mut()
325 .find(|c| c.object_identifier() == &hf.computer_sid)
326 {
327 Some(c) => c,
328 None => {
329 warn!("[sessions] no computer object for SID {}", hf.computer_sid);
330 return 0;
331 }
332 };
333 let comp_sid = hf.computer_sid.clone();
334 let fqdn = computer.properties().name().clone(); let mut count = 0usize;
336
337 {
339 let s = computer.sessions_mut(); for sess in &hf.smb_sessions {
341 match resolve(&sess.user, sid_index, domain) {
342 Some(user_sid) => {
343 trace!("[SRVSVC] {} has session on {fqdn}", sess.user);
344 s.results_mut().push(mk_link(user_sid, comp_sid.clone()));
345 count += 1;
346 }
347 None => warn!("[{comp_sid}] unresolved SRVSVC principal '{}'", sess.user),
348 }
349 }
350 *s.collected_mut() = true;
351 }
352
353 {
355 let p = computer.privileged_sessions_mut(); for u in &hf.logged_on {
357 match resolve(&u.username, sid_index, domain) {
358 Some(user_sid) => {
359 trace!("[WKSSVC] {}\\{} has session on {fqdn}", u.logon_domain, u.username);
360 p.results_mut().push(mk_link(user_sid, comp_sid.clone()));
361 count += 1;
362 }
363 None => warn!("[{comp_sid}] unresolved WKSSVC principal '{}\\{}'",
364 u.logon_domain, u.username),
365 }
366 }
367 *p.collected_mut() = true;
368 }
369
370 {
372 let r = computer.registry_sessions_mut(); for reg in &hf.registry {
374 if reg.sid.is_empty() { continue; }
375 trace!("[WINREG] {} has session on {fqdn}", reg.sid);
376 r.results_mut().push(mk_link(reg.sid.clone(), comp_sid.clone()));
377 count += 1;
378 }
379 *r.collected_mut() = true;
380 }
381
382 debug!("[sessions] Total {count} session(s) on {fqdn}");
383 count
384}
385
386fn parse_hash(h: Option<&str>) -> Option<[u8; 16]> {
391 let raw = h?.trim();
392 let nt = raw.rsplit(':').next().unwrap_or(raw).trim();
393 if nt.len() != 32 || !nt.bytes().all(|b| b.is_ascii_hexdigit()) {
394 return None;
395 }
396 let mut out = [0u8; 16];
397 for (i, byte) in out.iter_mut().enumerate() {
398 *byte = u8::from_str_radix(&nt[i * 2..i * 2 + 2], 16).ok()?;
399 }
400 Some(out)
401}