Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions ares-cli/src/orchestrator/result_processing/discovery_polling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use tracing::{debug, info, warn};
use ares_core::models::{Credential, Hash, Host, Share, TrustInfo, User, VulnerabilityInfo};

use super::parsing::resolve_parent_id;
use super::reconcile_low_trust_credential_domain;
use super::LOCKOUT_PATTERNS;
use crate::orchestrator::dispatcher::Dispatcher;

Expand Down Expand Up @@ -65,8 +66,20 @@ async fn poll_discoveries(dispatcher: &Dispatcher) -> Result<()> {
match disc_type {
"credential" => match serde_json::from_value::<Credential>(data.clone()) {
Ok(mut cred) => {
let state = dispatcher.state.read().await;
let extracted_domain = cred.domain.clone();
if let Some(corrected) =
reconcile_low_trust_credential_domain(&mut cred, &state.users)
{
warn!(
username = %cred.username,
extracted_domain = %extracted_domain,
corrected_domain = %corrected,
source = %cred.source,
"Reassigning real-time credential discovery to directory-attested domain from state.users",
);
}
if cred.parent_id.is_none() {
let state = dispatcher.state.read().await;
let (pid, step) = resolve_parent_id(
&state.credentials,
&state.hashes,
Expand All @@ -78,8 +91,8 @@ async fn poll_discoveries(dispatcher: &Dispatcher) -> Result<()> {
);
cred.parent_id = pid;
cred.attack_step = step;
drop(state);
}
drop(state);
let user_domain = format!("{}@{}", cred.username, cred.domain);
match dispatcher
.state
Expand Down
37 changes: 37 additions & 0 deletions ares-cli/src/orchestrator/result_processing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,30 @@ pub(crate) fn reconcile_extracted_credential_domain(
Some(only)
}

fn is_low_trust_realm_inferred_credential_source(source: &str) -> bool {
matches!(
source,
"description_field"
| "autologon_registry"
| "sysvol_script"
| "user_description_leak"
| "netexec_password"
| "ldap_description"
)
}

pub(crate) fn reconcile_low_trust_credential_domain(
cred: &mut ares_core::models::Credential,
users: &[ares_core::models::User],
) -> Option<String> {
if !is_low_trust_realm_inferred_credential_source(&cred.source) {
return None;
}
let corrected = reconcile_extracted_credential_domain(users, &cred.username, &cred.domain)?;
cred.domain = corrected.clone();
Some(corrected)
}

/// `kerberoast_{username}` or `asrep_roast_{domain}` token when the
/// captured hash carries the canonical impacket / hashcat prefix
/// (`$krb5tgs$`, `$krb5asrep$`). Returns `None` for other hash types so
Expand Down Expand Up @@ -1481,7 +1505,20 @@ async fn extract_discoveries(
// Read lock is released before any publish calls (which take write locks).
{
let state = dispatcher.state.read().await;
let mut user_hints = state.users.clone();
user_hints.extend(parsed.users.iter().cloned());

for cred in &mut parsed.credentials {
let extracted_domain = cred.domain.clone();
if let Some(corrected) = reconcile_low_trust_credential_domain(cred, &user_hints) {
warn!(
username = %cred.username,
extracted_domain = %extracted_domain,
corrected_domain = %corrected,
source = %cred.source,
"Reassigning parser-extracted credential to directory-attested domain from state.users",
);
}
if cred.parent_id.is_none() {
let (pid, step) = resolve_parent_id(
&state.credentials,
Expand Down
65 changes: 65 additions & 0 deletions ares-cli/src/orchestrator/result_processing/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2075,3 +2075,68 @@ mod reconcile_extracted_credential_domain {
assert_eq!(got, Some("child.contoso.local".to_string()));
}
}

mod reconcile_low_trust_credential_domain {
use super::super::reconcile_low_trust_credential_domain;
use ares_core::models::{Credential, User};

fn user(username: &str, domain: &str) -> User {
User {
username: username.to_string(),
domain: domain.to_string(),
description: String::new(),
is_admin: false,
source: "kerberos_enum".to_string(),
}
}

fn cred(username: &str, domain: &str, source: &str) -> Credential {
Credential {
id: "c1".to_string(),
username: username.to_string(),
password: "_L0ngCl@w_".to_string(),
domain: domain.to_string(),
source: source.to_string(),
discovered_at: None,
is_admin: false,
parent_id: None,
attack_step: 0,
}
}

#[test]
fn corrects_low_trust_sysvol_realm() {
let users = vec![user("alice", "child.contoso.local")];
let mut cred = cred("alice", "contoso.local", "sysvol_script");

let got = reconcile_low_trust_credential_domain(&mut cred, &users);

assert_eq!(got, Some("child.contoso.local".to_string()));
assert_eq!(cred.domain, "child.contoso.local");
}

#[test]
fn leaves_high_trust_source_unchanged() {
let users = vec![user("alice", "child.contoso.local")];
let mut cred = cred("alice", "contoso.local", "secretsdump");

let got = reconcile_low_trust_credential_domain(&mut cred, &users);

assert_eq!(got, None);
assert_eq!(cred.domain, "contoso.local");
}

#[test]
fn leaves_ambiguous_low_trust_realm_unchanged() {
let users = vec![
user("administrator", "child.contoso.local"),
user("administrator", "contoso.local"),
];
let mut cred = cred("administrator", "contoso.local", "sysvol_script");

let got = reconcile_low_trust_credential_domain(&mut cred, &users);

assert_eq!(got, None);
assert_eq!(cred.domain, "contoso.local");
}
}
Loading