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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ __pycache__/
# Ansible artifacts
.ansible/
.task/
*AnsiballZ*

# Build artifacts
dreadgoad
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ See [tools/variant_generator/](tools/variant_generator/) for details.
- [Vulnerability catalog](docs/GOAD-vulnerabilities-comprehensive.md) -- all 50+ vulnerabilities with exploitation techniques
- [Validation guide](docs/validation.md) -- automated vulnerability validation
- [Provider guides](docs/mkdocs/docs/providers/) -- VirtualBox, VMware, Proxmox, AWS, Azure, Ludus
- [AWS AMI build & deploy workflow](docs/mkdocs/docs/providers/aws-ami-workflow.md) -- end-to-end warpgate + Terragrunt + Ansible
- [Extension guides](docs/mkdocs/docs/extensions/) -- ELK, Exchange, Wazuh, hardened workstation
- [Architecture diagram](docs/architecture.svg)
- [Upstream GOAD docs](https://orange-cyberdefense.github.io/GOAD/) -- original project documentation
Expand Down
1 change: 1 addition & 0 deletions ansible/ansible.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ vars_plugins_enabled = host_group_vars,dreadnode.goad.lab_config
# Note: fact_caching_connection is set via ANSIBLE_CACHE_PLUGIN_CONNECTION env var in Taskfile
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts_cache
fact_caching_timeout = 86400

# Diff output
Expand Down
1 change: 0 additions & 1 deletion ansible/playbooks/ad-trusts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
domain_password: "{{ lab.domains[domain].domain_password }}"
parent_domain: "{{ '.'.join(domain.split('.')[1:]) | default('') }}"
trust: "{{ lab.domains[domain].trust | default('') }}"
lab: "{{ lab }}"
domains: "{{ lab.domains.keys() }}"
replication: forest
dc_hostname_to_ip: "{{ hostvars[groups['dc'][0]]['dc_hostname_to_ip'] | default({}) }}"
Expand Down
11 changes: 9 additions & 2 deletions cli/cmd/inventory.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,18 +208,25 @@ func runInventoryShow(cmd *cobra.Command, args []string) error {
}

func runInventoryMapping(cmd *cobra.Command, args []string) error {
outputPath, _ := cmd.Flags().GetString("output")
return generateInstanceMapping(context.Background(), outputPath)
}

// generateInstanceMapping queries AWS for instance private IPs and writes the
// mapping to a JSON file that Ansible's network_discovery role uses to avoid
// slow runtime detection over SSM. If outputPath is empty, it defaults to
// /tmp/aws_instance_mapping_<env>.json.
func generateInstanceMapping(ctx context.Context, outputPath string) error {
cfg, err := config.Get()
if err != nil {
return err
}
ctx := context.Background()

parsed, err := inv.Parse(cfg.InventoryPath())
if err != nil {
return err
}

outputPath, _ := cmd.Flags().GetString("output")
if outputPath == "" {
outputPath = filepath.Join(os.TempDir(), fmt.Sprintf("aws_instance_mapping_%s.json", cfg.Env))
}
Expand Down
29 changes: 27 additions & 2 deletions cli/cmd/provision.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"strings"
"time"

"slices"

"github.com/dreadnode/dreadgoad/internal/ansible"
"github.com/dreadnode/dreadgoad/internal/config"
"github.com/dreadnode/dreadgoad/internal/doctor"
Expand Down Expand Up @@ -55,7 +57,6 @@ func init() {
provisionCmd.Flags().Int("max-retries", 0, "Max retry attempts (default: from config)")
provisionCmd.Flags().Int("retry-delay", 0, "Delay between retries in seconds (default: from config)")

// ad-users inherits provision flags
adUsersCmd.Flags().String("plays", "ad-data.yml", "Playbooks to run")
adUsersCmd.Flags().String("limit", "", "Limit execution to specific hosts")
adUsersCmd.Flags().Int("max-retries", 0, "Max retry attempts")
Expand Down Expand Up @@ -141,6 +142,12 @@ func runProvision(cmd *cobra.Command, args []string) error {
return err
}

// Generate instance-to-IP mapping so Ansible can resolve host IPs
// without slow runtime network detection over SSM.
if err := generateInstanceMapping(ctx, ""); err != nil {
slog.Warn("instance mapping generation failed, playbooks will use runtime detection", "error", err)
}

fmt.Println("===============================================")
fmt.Printf("DreadGOAD provisioning started at %s\n", time.Now().Format(time.RFC3339))
fmt.Printf("Environment: %s\n", cfg.Env)
Expand All @@ -155,7 +162,13 @@ func runProvision(cmd *cobra.Command, args []string) error {
}
fmt.Println("-----------------------------------------------")

for _, playbook := range playbooks {
// Clean up stale SSM sessions before starting provisioning to prevent
// connection saturation from orphaned sessions of previous runs.
log := slog.Default()
log.Info("cleaning up stale SSM sessions before provisioning")
ansible.CleanupSSMSessions(ctx, cfg.Env, log)

for i, playbook := range playbooks {
opts := ansible.RetryOptions{
Playbook: playbook,
Env: cfg.Env,
Expand All @@ -173,6 +186,18 @@ func runProvision(cmd *cobra.Command, args []string) error {
if err := ansible.RunPlaybookWithRetry(ctx, opts); err != nil {
return fmt.Errorf("provisioning failed at %s: %w", playbook, err)
}

// Between playbooks: clean up accumulated SSM sessions and wait
// after reboot-inducing playbooks for SSM agents to reconnect.
if i < len(playbooks)-1 {
ansible.CleanupSSMSessions(ctx, cfg.Env, log)

if slices.Contains(config.RebootPlaybooks, playbook) {
log.Info("playbook may have caused reboots, waiting for SSM reconnection",
"playbook", playbook, "delay", "120s")
time.Sleep(120 * time.Second)
}
}
}

fmt.Println("===============================================")
Expand Down
10 changes: 5 additions & 5 deletions cli/internal/ansible/retry.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func RunPlaybookWithRetry(ctx context.Context, opts RetryOptions) error {

if result.TimedOut {
log.Error("playbook timed out (idle timeout)", "playbook", opts.Playbook)
cleanupSSMSessions(ctx, opts.Env, log)
CleanupSSMSessions(ctx, opts.Env, log)
continue
}

Expand Down Expand Up @@ -136,7 +136,7 @@ func retryWithErrorStrategy(ctx context.Context, opts RetryOptions, failResult *

case ErrSSMTransfer:
log.Info("SSM transfer error - fixing ssm-user accounts")
cleanupSSMSessions(ctx, opts.Env, log)
CleanupSSMSessions(ctx, opts.Env, log)
fixSSMUsers(ctx, opts.Env, failResult.FailedHosts, log)
log.Info("waiting for SSM Agent to stabilize", "delay", "30s")
time.Sleep(30 * time.Second)
Expand All @@ -154,7 +154,7 @@ func retryWithErrorStrategy(ctx context.Context, opts RetryOptions, failResult *

case ErrSSMReconnection:
log.Info("SSM reconnection needed - waiting for systems to reboot")
cleanupSSMSessions(ctx, opts.Env, log)
CleanupSSMSessions(ctx, opts.Env, log)
log.Info("waiting for Windows reboot and SSM reconnection", "delay", "120s")
time.Sleep(120 * time.Second)

Expand Down Expand Up @@ -224,7 +224,8 @@ func buildRetryLimit(userLimit, failedHosts string) string {
}
}

func cleanupSSMSessions(ctx context.Context, env string, log *slog.Logger) {
// CleanupSSMSessions terminates stale SSM sessions to prevent connection saturation.
func CleanupSSMSessions(ctx context.Context, env string, log *slog.Logger) {
cfg, err := config.Get()
if err != nil {
log.Warn("could not get config for SSM cleanup", "error", err)
Expand Down Expand Up @@ -322,5 +323,4 @@ func rebootFailedHosts(ctx context.Context, opts RetryOptions, log *slog.Logger)
}
}

// execCommand is a variable for testability.
var execCommand = exec.CommandContext
13 changes: 8 additions & 5 deletions dev-inventory
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ ansible_remote_tmp=C:\Windows\Temp
; miscellaneous
data_path="{{ playbook_dir }}/../../ad/GOAD-variant-1/data"

; AWS instances have a single network adapter (no NAT adapter)
two_adapters=false

; global settings inventory default value
keyboard_layouts=["en-US", "da-DK", "fr-FR"]

Expand All @@ -46,17 +49,17 @@ dns_server_forwarder=1.1.1.1
; ------------------------------------------------
; sevenkingdoms.local
; ------------------------------------------------
dc01 ansible_host=i-0e428dfc02f5007dd dict_key=dc01 dns_domain=dc01 ansible_user=ansible
dc01 ansible_host=i-0e428dfc02f5007dd dict_key=dc01 dns_domain=dc01 ansible_user=ansible dc_ipv4=10.0.4.105 host_ipv4=10.0.4.105
; ------------------------------------------------
; north.sevenkingdoms.local
; ------------------------------------------------
dc02 ansible_host=i-003cb089e4bffd044 dict_key=dc02 dns_domain=dc01 ansible_user=ansible
srv02 ansible_host=i-0e5e90aa3674b019f dict_key=srv02 dns_domain=dc02 ansible_user=ansible
dc02 ansible_host=i-003cb089e4bffd044 dict_key=dc02 dns_domain=dc01 ansible_user=ansible dc_ipv4=10.0.4.40 host_ipv4=10.0.4.40
srv02 ansible_host=i-0e5e90aa3674b019f dict_key=srv02 dns_domain=dc02 ansible_user=ansible dc_ipv4=10.0.4.10 host_ipv4=10.0.4.10
; ------------------------------------------------
; essos.local
; ------------------------------------------------
dc03 ansible_host=i-09be8150780ec08e6 dict_key=dc03 dns_domain=dc03 ansible_user=ansible
srv03 ansible_host=i-00efbd3b68f5483a8 dict_key=srv03 dns_domain=dc03 ansible_user=ansible
dc03 ansible_host=i-09be8150780ec08e6 dict_key=dc03 dns_domain=dc03 ansible_user=ansible dc_ipv4=10.0.4.83 host_ipv4=10.0.4.83
srv03 ansible_host=i-00efbd3b68f5483a8 dict_key=srv03 dns_domain=dc03 ansible_user=ansible dc_ipv4=10.0.4.53 host_ipv4=10.0.4.53

; LAB SCENARIO CONFIGURATION -----------------------------

Expand Down
36 changes: 9 additions & 27 deletions docs/GOAD-vulnerabilities-comprehensive.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Comprehensive GOAD (Game of Active Directory) Vulnerabilities Writeup
# GOAD Vulnerability Catalog

**GOAD** is a vulnerable Active Directory penetration testing lab environment created by Mayfly (Orange Cyberdefense) designed to help security professionals practice realistic Active Directory attack techniques in a safe, controlled environment.
**GOAD** is a vulnerable Active Directory penetration testing lab by Mayfly (Orange Cyberdefense). This document catalogs all known vulnerabilities and attack paths in the lab.

**Lab Architecture:**

Expand Down Expand Up @@ -1344,7 +1344,7 @@ Tywin

**Vulnerability:** Database trust relationships span forest boundaries

- **Attack:** Leverage linked servers to execute commands across forests
- **Attack:** Use linked servers to execute commands across forests
- **Impact:** Cross-forest pivoting and command execution

---
Expand Down Expand Up @@ -1419,9 +1419,9 @@ Tywin

### Token Impersonation

**Vulnerability:** Available tokens on compromised systems can be leveraged
**Vulnerability:** Available tokens on compromised systems can be stolen

- **Method:** Leverage user tokens to execute commands as other users without credentials
- **Method:** Use stolen tokens to execute commands as other users without credentials
- **Token Types:**
- **Delegation tokens:** Created for interactive logins (RDP, console)
- **Impersonation tokens:** Created for non-interactive sessions
Expand Down Expand Up @@ -1583,7 +1583,7 @@ Tywin

### ADCS Attacks

- **Certipy** - Comprehensive ADCS exploitation
- **Certipy** - ADCS enumeration and exploitation
- **Certify** - Certificate template enumeration
- **Coercer** - Authentication coercion
- **Pywhisker / Whisker** - Shadow credentials
Expand Down Expand Up @@ -1677,7 +1677,7 @@ Based on the vulnerabilities in GOAD, here are key defensive measures:
- **Official Documentation:** https://orange-cyberdefense.github.io/GOAD/
- **Creator's Blog (Mayfly):** https://mayfly277.github.io/

### Comprehensive Walkthrough Series (Mayfly)
### Walkthrough Series (Mayfly)

1. Part 1 - Reconnaissance and scan: https://mayfly277.github.io/posts/GOADv2-pwning_part1/
2. Part 2 - Find users: https://mayfly277.github.io/posts/GOADv2-pwning-part2/
Expand Down Expand Up @@ -1708,25 +1708,9 @@ Based on the vulnerabilities in GOAD, here are key defensive measures:

---

## Conclusion
## Coverage

GOAD (Game of Active Directory) is an exceptionally comprehensive vulnerable Active Directory lab that covers virtually all major Active Directory attack vectors, from initial reconnaissance through complete domain and forest compromise. It includes:

- **50+ distinct vulnerabilities and attack techniques**
- **15+ CVEs and exploitation methods**
- **All major ADCS attacks (ESC1-15)**
- **Complete Kerberos attack surface**
- **ACL abuse chains**
- **Delegation exploitation**
- **Cross-domain and cross-forest attacks**
- **Privilege escalation techniques**
- **Lateral movement methods**

The lab is actively maintained and updated with new attack techniques as they are discovered. It provides an excellent training environment for security professionals to practice Active Directory penetration testing in a safe, legal, and comprehensive manner.

This document represents the most thorough compilation of GOAD vulnerabilities available, synthesized from official writeups (Parts 1-14 by Mayfly277), community contributions, and detailed exploitation guides.

**Coverage Summary:**
Compiled from Mayfly277's official writeups (Parts 1-14) and community contributions.

- Part 1: Reconnaissance and scanning
- Part 2: User discovery (ASREPRoast, password spraying)
Expand All @@ -1742,5 +1726,3 @@ This document represents the most thorough compilation of GOAD vulnerabilities a
- Part 12: Trust exploitation (child-to-parent, forest trusts, golden ticket + ExtraSid)
- Part 13: Post-exploitation (token impersonation, RDP hijacking, file coercion)
- Part 14: Advanced ADCS (ESC5/7/9/10/11/13/14/15)

**Last Updated:** March 2026
Loading
Loading