Cloud Security Office Hours Banner

Break it, then catch it: a vulnerable AWS lab

Deploy a deliberately-weak AWS environment, attack it yourself three different ways, and watch GuardDuty react. A safe, self-contained purple-team loop in a throwaway account.

Jump to the build Back to Home Lab

· · Vendor-neutral

Time: ~4 hours  ·  Difficulty: Intermediate  ·  Stack: AWS free tier · Terraform · AWS CLI · Pacu · GuardDuty · Stratus Red Team

Authorization and safety. This walkthrough deploys deliberately weak resources in a real AWS account and runs offensive tooling against them. Do it only in a dedicated throwaway lab account that already has the budget guardrails from the home-lab guide, and tear everything down the same day. Never point these tools at an account you do not fully own.

The single best way to understand a cloud attack is to build the vulnerable setup, exploit it yourself, and then watch the defenses react - or fail to. This walkthrough deploys a small, deliberately-weak AWS environment with Terraform, walks three escalating attack paths against it (manual CLI recon, automated enumeration with Pacu, ATT&CK technique detonation with Stratus Red Team), and reads the GuardDuty findings that result. It ends with the question every detection engineer asks - what did the defenses miss? - which is the on-ramp to the CloudTrail-to-SIEM walkthrough.

On this page

  1. What you will build
  2. Prerequisites
  3. Step-by-step
  4. What did the defenses miss?
  5. Common mistakes
  6. Where next

What you will build

Prerequisites

Step-by-step

1. Turn on the sensors first

Enable detection before you attack, so every action is recorded from the start.

# GuardDuty: enable the detector (30-day free trial on a new account)
aws guardduty create-detector --enable --region us-east-1
aws guardduty list-detectors

Now a CloudTrail trail delivering management events to S3. trail.tf:

resource "aws_s3_bucket" "trail" {
  bucket        = "ct-lab-${data.aws_caller_identity.me.account_id}"
  force_destroy = true
}

data "aws_caller_identity" "me" {}

resource "aws_s3_bucket_policy" "trail" {
  bucket = aws_s3_bucket.trail.id
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      { Sid = "AclCheck", Effect = "Allow",
        Principal = { Service = "cloudtrail.amazonaws.com" },
        Action = "s3:GetBucketAcl", Resource = aws_s3_bucket.trail.arn },
      { Sid = "Write", Effect = "Allow",
        Principal = { Service = "cloudtrail.amazonaws.com" },
        Action = "s3:PutObject",
        Resource = "${aws_s3_bucket.trail.arn}/AWSLogs/${data.aws_caller_identity.me.account_id}/*",
        Condition = { StringEquals = { "s3:x-amz-acl" = "bucket-owner-full-control" } } }
    ]
  })
}

resource "aws_cloudtrail" "lab" {
  name                          = "lab-trail"
  s3_bucket_name                = aws_s3_bucket.trail.id
  include_global_service_events = true
  is_multi_region_trail         = true
  depends_on                    = [aws_s3_bucket_policy.trail]
}

2. Deploy the vulnerable stack

vuln.tf - each resource is a real-world mistake you will recognize:

# A public bucket holding fake "loot"
resource "aws_s3_bucket" "loot" {
  bucket        = "exports-lab-${data.aws_caller_identity.me.account_id}"
  force_destroy = true
}
resource "aws_s3_bucket_public_access_block" "loot" {
  bucket                  = aws_s3_bucket.loot.id
  block_public_acls       = false
  block_public_policy     = false
  ignore_public_acls      = false
  restrict_public_buckets = false
}
resource "aws_s3_bucket_policy" "loot" {
  bucket = aws_s3_bucket.loot.id
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{ Effect = "Allow", Principal = "*",
      Action = "s3:GetObject", Resource = "${aws_s3_bucket.loot.arn}/*" }]
  })
}

# A secret hiding in SSM Parameter Store
resource "aws_ssm_parameter" "db_password" {
  name  = "/lab/db_password"
  type  = "SecureString"
  value = "hunter2-not-a-real-secret"
}

# An over-permissive IAM user with a long-lived access key ("the leaked key")
resource "aws_iam_user" "legacy" { name = "legacy-app" }
resource "aws_iam_user_policy" "legacy" {
  name   = "too-much"
  user   = aws_iam_user.legacy.name
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{ Effect = "Allow", Action = ["iam:*","s3:*","ssm:*"], Resource = "*" }]
  })
}
resource "aws_iam_access_key" "legacy" { user = aws_iam_user.legacy.name }

output "leaked_access_key_id"     { value = aws_iam_access_key.legacy.id }
output "leaked_secret_access_key" { value = aws_iam_access_key.legacy.secret, sensitive = true }
terraform init
terraform apply -auto-approve
echo "top-secret export data" > loot.txt
aws s3 cp loot.txt s3://$(terraform output -raw loot_bucket 2>/dev/null || echo exports-lab-CHANGEME)/

3. Attack path 1 - manual recon with the leaked key

Configure a profile with the leaked key (as if you found it in a public git repo), then walk the recon-to-collection chain:

# use the leaked credentials from the Terraform outputs
export AWS_ACCESS_KEY_ID=$(terraform output -raw leaked_access_key_id)
export AWS_SECRET_ACCESS_KEY=$(terraform output -raw leaked_secret_access_key)
unset AWS_PROFILE

aws sts get-caller-identity                         # confirm who the key belongs to
aws iam list-user-policies --user-name legacy-app   # what can it do?
aws iam get-user-policy --user-name legacy-app --policy-name too-much

# find and read the loot
aws s3 ls
aws s3 cp s3://exports-lab-<account-id>/loot.txt -
aws ssm get-parameter --name /lab/db_password --with-decryption \
  --query Parameter.Value --output text

Each command is one link in a real kill chain: credential access (the leaked key), discovery (enumerating what it can do), and collection (reading the bucket and the decrypted secret). This is exactly how the majority of cloud breaches begin - see the breach kill chains for the production versions.

4. Attack path 2 - automated enumeration with Pacu

pip install pacu
pacu
# inside the Pacu shell:
#   import_keys --import-aws-cli   (or set them manually)
#   run iam__enum_permissions
#   run iam__privesc_scan          # maps the wildcard policy to escalation paths
#   run s3__download_bucket

Pacu is the AWS exploitation framework. iam__privesc_scan is the highlight: it takes the permissions you enumerated and tells you how to escalate to admin from them - the same reasoning an attacker (or a good IAM reviewer) applies to an over-permissive policy.

5. Attack path 3 - detonate ATT&CK techniques with Stratus Red Team

brew install stratus-red-team          # or grab a binary from the releases page
stratus list                            # browse the catalog of cloud techniques

# detonate a few - each creates its own prerequisites, performs the technique,
# and can clean itself up afterwards
stratus detonate aws.credential-access.ec2-steal-instance-credentials
stratus detonate aws.defense-evasion.cloudtrail-stop
stratus detonate aws.exfiltration.s3-backdoor-bucket-policy

stratus cleanup --all                   # ALWAYS run this - it removes what it created

Stratus Red Team is purpose-built to test detections: each technique maps to a MITRE ATT&CK ID and produces the exact telemetry a real attacker would. You are generating a labeled dataset of malicious activity in your own account.

6. Read the GuardDuty findings

DETECTOR=$(aws guardduty list-detectors --query 'DetectorIds[0]' --output text)
aws guardduty list-findings --detector-id "$DETECTOR"
aws guardduty get-findings --detector-id "$DETECTOR" \
  --finding-ids <finding-id> --query 'Findings[0].{type:Type,severity:Severity}' 

Expect findings such as Stealth:IAMUser/CloudTrailLoggingDisabled (from the Stratus cloudtrail-stop), UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS (from the credential-theft technique), and Policy:S3/BucketAnonymousAccessGranted (from your public bucket). Findings can take minutes to hours to surface; to see the shape of a finding immediately, generate samples with aws guardduty create-sample-findings --detector-id "$DETECTOR". Read each finding in full - every one is a real attack pattern, annotated, in an environment you control.

What did the defenses miss?

GuardDuty is anomaly- and signature-based, so it will not flag everything you did. The quiet ssm:GetParameter read of the secret, the iam:PutUserPolicy self-escalation Pacu mapped, and the plain enumeration calls often raise no managed finding at all. That gap is not a failure of the lab - it is the reason detection engineers write their own logic against the raw audit trail. Take your list of "things GuardDuty missed" straight into the CloudTrail-to-SIEM walkthrough and build detections for them.

Tear it all down. The most important step. Run it before you close the laptop:

stratus cleanup --all
terraform destroy -auto-approve
# sanity-check the console: no running EC2, no public bucket, no stray access keys
aws iam list-access-keys --user-name legacy-app 2>/dev/null || echo "user gone - good"

Common mistakes

Where next