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
What you will build
- GuardDuty and CloudTrail enabled as your sensors.
- A deliberately-vulnerable Terraform stack: a public bucket, a leaked access key, an over-permissive user, an SSM secret, and a wide-open security group.
- Three attack runs against it, from manual recon to automated ATT&CK detonation.
- A read-through of the resulting GuardDuty findings, mapped to what you did.
- A clear list of what GuardDuty caught and what it did not - your detection-engineering to-do list.
Prerequisites
- A dedicated lab AWS account with the budget guardrails already in place.
- AWS CLI and Terraform configured with an admin profile for the lab account.
- Python 3.9+ (for Pacu) and Homebrew or a binary install for Stratus Red Team.
- Comfort with the enumeration commands from the LocalStack lab - this is the real-account sequel.
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
- Running this outside a dedicated lab account. Deliberately-vulnerable resources plus offensive tooling belong nowhere near shared or production accounts.
- Leaving the public bucket or leaked key live. A real access key with
iam:*in a public repo is a genuine incident. Destroy it the same day. - Forgetting
stratus cleanup. Stratus leaves prerequisite infrastructure (instances, roles) running and billable if you skip it. - Reading "no GuardDuty finding" as "not detectable." It means "build your own detection" - which is the entire next walkthrough.
Where next
- Build a CloudTrail-to-SIEM pipeline - write detections for everything GuardDuty missed.
- Breach kill chains - recreate a real incident using these same techniques.
- Recreate the Capital One breach - the portfolio-grade version of this exercise.
- Cloud pentesting and incident response.
