EC2 IAM role STS credentials compromise via IMDS
Exploring how temporary credentials obtained through AWS Instance Metadata Service can be extracted and used both inside and outside EC2 instances, and analyzing their visibility in CloudTrail logs.
Background
AWS Identity and Access Management (IAM) roles are a key security feature in AWS that allow services, applications, or users to assume temporary permissions to interact with AWS resources. IAM roles are commonly used to grant access without hardcoding credentials.
AWS Elastic Compute Cloud (EC2) is a web service that let’s AWS customers to create and manage virtual machines, also called instances.
To leverage a IAM role within an EC2 instance, the role needs to be first attached to the instance. When the instance is launched, it will assume that role and obtain temporary security credentials without the necessity of leveraging static credentials. These credentials are generated by AWS STS (security token service), and going forward I will be refering to these as “STS credentials”.
IAM role operation within EC2 is supported by Instance Metadata Service (IMDS). IMDS is an interface that is leveraged by EC2 workloads to retrieve metadata of the instance, including, for example, instance ID, AMI ID, and networking information. IMDS also allows to retrieve STS credentials for the IAM role attached to the EC2 instance. STS credentials can be retrieved in 2 ways:
IMDSv1, a legacy version that accepts simple HTTP GET requests. This version was lacking security controls, so Amazon introduced IMDSv2.
IMDSv2, the updated version of IMDS, which leverages session-based authentication and reduces risks of certain abuse techniques, e.g. server-side request forgery (SSRF) that existed in IMDSv1.
In an event of threat actor gaining access to an EC2 instance, they can compromise the currently active STS credentials of the attached IAM role, and leverage that with a malicious intent.
Lab setup and overview
To conduct tests, I used AWS free tier to configure the testing environment. This setup involved:
Creating a VPC and enabling VPC flow logs.
Configuring CloudTrail to log events and write them into an S3 bucket.
Creating an IAM role that will be compromised.
Setting up an EC2 instance with the IAM role attached, while configuring IMDSv2 as optional to allow testing both IMDSv1 and IMDSv2 versions.
Once the environment was ready, I SSH’ed into the EC2 instance and performed the following experiments:
Extracted STS credentials:
Used IMDSv1 and IMDSv2 to retrieve the STS credentials for the attached IAM role.
Tested AWS CLI access:
Configured AWS CLI on the EC2 instance to use the extracted STS credentials and ran various test scenarios.
Configured AWS CLI on my local system with the same credentials to replicate the test scenarios outside the instance.
With the compromised STS credentials, I executed the following actions:
Enumerated S3 bucket with CloudTrail logs using “s3 ls” command.
Enumerated EC2 Instances using “describe-instances” command.
Verified identity for the STS credentials using “get-caller-identity” command.
Summary of findings
In my tests, when both IMDSv1 and IMDSv2 are available on an EC2 instance, there will be one set of credentials created for each of them. This is interesting behavior, however there would not be any way to distinguish which one was generated for IMDSv1 and which one for IMDSv2, without actually querying them on the EC2 instance while credentials are still active.
STS credential extraction via IMDS would not be visible in CloudTrail or VPC logs. Host-based forensics of the EC2 instance would be required to detect the initial credential extraction.
All API actions that leveraged extracted credentials were logged with the same identity (including instance ID from where the STS credentials were extracted) regardless of location. The fact of instance ID presence within the role session name, demonstrates that AWS maintains the original context of where the credentials were initially issued, even when those credentials are used from completely different environments. This is significant because it means that while the credentials can be used anywhere, the logging still ties all actions back to the original EC2 instance from which they were obtained.
While identity details will remain consistent, both source IP addresses and user agents can be used to distinguish between actions that happened within the EC2 instance and from an external IP.
CloudTrail comprehensively logs all API calls with these credentials, including successes, failures, and identity verification attempts.
Experiments deep dive
STS credentials extraction
IMDSv1
The IP address 169.254.169.254 is important in relation to IMDS because it serves as the dedicated link-local address through which EC2 instances can query metadata, including retrieving STS credentials. This means EC2 instance does not require Internet connection in order to query instance metadata, including STS credentials of the attached IAM role.
Easiest way to query IMDSv1 and extract the STS credentials generated for attached IAM role is to run a curl command pointing to 169.254.169.254. Code snippet presented below does just that, retrieves the STS credentials for “iam-role-blog-research-20250217” IAM role.
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/iam-role-blog-research-20250217
The STS credentials were returned in a form of JSON:
{
"Code" : "Success",
"LastUpdated" : "2025-02-17T17:57:35Z",
"Type" : "AWS-HMAC",
"AccessKeyId" : "ASIA2UC3DTBG65TLTQAU",
"SecretAccessKey" : "6aS1bKIcB...Ocz3FJP+/",
"Token" : "IQoJb3JpZ2luX2VjEFEYCIQ...8FOFSoiYkSWOnc5Nw==",
"Expiration" : "2025-02-18T00:31:41Z"
}
IMDSv2
IMDSv2 works little differently, and STS credentials can be retrieved with the following sequence of commands:
Retrieve IMDS session token. This should not be confused with STS credentials. IMDS session token is what differentiates IMDSv2 from IMDSv1. This security measure was specifically introduced to prevent SSRF (server-side request forgery) attacks. The token is retrieved via PUT request towards "http://169.254.169.254/latest/api/token". The command would look like this, where we set environmental variable TOKEN to the output of PUT request:
TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"`
TOKEN variable now can be leveraged to query IMDS and export the STS credentials via curl command:
curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/iam-role-blog-research-20250217
The STS credentials retrieved via IMDSv2 will be presented in exactly the same JSON format as what we saw in IMDSv1 example, however the actual credentials will be different:
{ "Code" : "Success", "LastUpdated" : "2025-02-17T17:56:52Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIA2UC3DTBGQTU3AUTM", "SecretAccessKey" : "gPznXQzVxajv6...BMyMslV", "Token" : "IQoJb3JpZ2luX2VjEFIaCXVzLWVhc3Qt...DGsgN9CmeRzA==", "Expiration" : "2025-02-18T00:31:41Z" }
Analysis
Now when we extracted the STS credentials of the attached IAM role, I would like to understand if any of the events related to STS credentials extraction would appear in AWS logs. I leveraged this tool to extract CloudTrail and VPC logs that were captured for my lab environment. In the tool I leveraged the STS credentials I previously extracted from the EC2 instance via IMDSv1. Analysis results:
The VPC logs would not be helpful here, which is expected since there were no network connections established with dedicated link-local IP address 169.254.169.254.
The ClourTrail logs will not show the events of threat actor retrieving STS credentials with neither IMDSv1 nor IMDSv2. However, we will see the AssumeRole events that are triggered either at EC2 instance startup or after the STS credentials were expired. One interesting discovery I made is STS will generate two separate sets of credentials - one for IMDSv1 and another for IMDSv2. The AssumeRole events will be generated for both credentials simultaneously (in my case the recorded timestamp was “2025-02-17T17:56:41Z”, the time when I launched the instance).
In order to investigate the STS credentials extraction via IMDS, you would need to leverage host-based forensic artifacts of the affected EC2 instance.
Leveraging compromised STS credentials
For both experiments I leveraged the STS credentials that I extracted before, specifically the ones with the AccessKeyID “ASIA2UC3DTBG65TLTQAU” produced via IMDSv1.
Testing STS credentials within an AWS EC2 instance
First I configured the AWS CLI within an EC2 instance to utilize the extracted STS credentials:
aws configure set aws_access_key_id "ASIA2UC3DTBG65TLTQAU"
aws configure set aws_secret_access_key "6aS1bKIcBGKkxm..."
aws configure set aws_session_token "IQoJb3JpZ2luX2VjEFIaCXVzLW..."
aws configure set region us-east-1
Then I launched the test commands, that would (1) list S3 bucket with CloudTrail logs, (2) enumerate EC2 Instances, and (3) verify identity for the STS credentials.
aws s3 ls
aws ec2 describe-instances
aws sts get-caller-identity
I observed that with the STS credentials I previously extracted via IMDSv1:
I could successfully list S3 buckets.
I received an "UnauthorizedOperation" error when attempting to list EC2 instances for the account (this is expected as my IAM role did not have the necessary permissions to perform the ec2:DescribeInstances action).
I could retrieve the identity information showing I was using the assumed IAM role.
Testing the same credentials outside of AWS
I exported the exact same temporary credentials obtained via IMDSv1, and created a “stolen-creds-research” profile on my local machine in AWS CLI:
aws configure --profile stolen-creds-research
AWS Access Key ID: ASIA2UC3DTBG65TLTQAU
AWS Secret Access Key: 6aS1bKIcBGKkxmsrJYe/
Default region name: us-east-1
Default output format: json
aws configure set aws_session_token "IQoJb3JpZ2luX2VjEF..." --profile stolen-creds-research
Then, using the created profile, I repeated the same operations as in the previous test:
aws s3 ls --profile stolen-creds-research
aws ec2 describe-instances --profile stolen-creds-research
aws sts get-caller-identity --profile stolen-creds-research
The results were identical to those from within the EC2 instance, demonstrating that temporary credentials maintain the same permissions regardless of where they're used.
CloudTrail logs analysis
I extracted and analyzed the CloudTrail logs to find evidence of my testing operations.
All actions, whether performed within AWS or externally, were recorded in logs with the compromised AccessKeyId (ASIA2UC3DTBG65TLTQAU). In addition to that, the actions were recorded with the same identity, including the instance ID from where the credentials were extracted!
arn:aws:sts::730335516749:assumed-role/iam-role-blog-research-20250217/i-0242db56002b56caf
While the principal remains the same, the source IP address changes depending on where the credentials are used:
EC2 Instance: 18.204.198.227 (AWS IP)
External Machine: 74.64.xx.xx (my local IP)
The logs include the AWS CLI version and operating system, which differ between environments:
EC2: aws-cli/2.17.18 md/awscrt#0.19.19 ua/2.0 os/linux#6.1.127-135.201.amzn2023.x86_64
External: aws-cli/2.24.5 md/awscrt#0.23.8 ua/2.0 os/macos#22.6.0 md/arch#x86_64
AWS CloudTrail thoroughly logs all API calls, including:
Successful operations (S3 bucket listing)
Failed operations with detailed error messages (the error I got after running “aws ec2 describe-instances”)
Identity verification actions (GetCallerIdentity)
References
“AWS Instance Metadata Service: A Quick Refresher” by Syed Hasan (https://syedhasan010.medium.com/aws-instance-metadata-service-a-quick-refresher-4b61ed9af23a)
AWS documentation: “Access instance metadata for an EC2 instance” (https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html)
“Daily Blog #751: Sunday Funday 2/16/25” by David Cowen (https://www.hecfblog.com/2025/02/daily-blog-751-sunday-funday-21625.html)
Tools used
CloudTrail and VPC flow log collector (https://github.com/ilyakobzar/dfir-tools/blob/main/cloudtrail_vpc_log_collector.py)
Great discovery and awesome write-up, Ilya!
This is sick. Thanks for taking the time to share your research!