CLOUD PENTEST — Post-Access Exploitation
Recon finds the door; this is what you do inside. A leaked AKIA... key or a metadata token is not a finding on its own — the finding is what that identity can DO. Enumerate the identity, find the privesc path, prove real impact. Never touch data you're not authorized to.
AUTHORIZATION FIRST
[ ] Target cloud account/subscription is explicitly in scope
[ ] You have written authorization (program scope, engagement doc)
[ ] Read-only enumeration before ANY state change
[ ] No exfil of real customer data — prove access with a benign canary/own-account object
[ ] Discovered accounts/roles you can pivot to are NOT auto-authorized — confirm scope
Stop here if any box is unchecked.
0. IDENTIFY THE IDENTITY (do this first, always)
| You have | First call | Tells you |
|---|
| AWS keys | aws sts get-caller-identity | account id, principal ARN, user vs role |
| AWS metadata | curl .../iam/security-credentials/<role> | temp creds + role name |
| GCP SA JSON / token | gcloud auth ... / curl metadata ...token | project, service account, scopes |
| Azure token / MI | IMDS .../identity/oauth2/token | tenant, object id, resource access |
Never assume privilege. Enumerate what this identity actually has before planning.
1. AWS — ENUMERATE THEN ESCALATE
Enumerate (read-only):
aws sts get-caller-identity
aws iam get-account-authorization-details 2>/dev/null # full policy dump if allowed
aws iam list-attached-user-policies --user-name <u>
aws iam list-role-policies / list-attached-role-policies
# No IAM read? Brute the effective perms with enumerate-iam / then map with a policy tool
Classic privesc paths (need the matching permission):
| Permission held | Escalation |
|---|
iam:CreatePolicyVersion | Write a new default *:* version onto an attached policy |
iam:PassRole + ec2:RunInstances / lambda:CreateFunction | Launch compute AS an admin role |
iam:AttachUserPolicy / PutUserPolicy | Attach AdministratorAccess to yourself |
sts:AssumeRole (over-broad trust) | Assume a more privileged role |
iam:CreateAccessKey on another user | Mint keys for a privileged user |
lambda:UpdateFunctionCode on privileged fn | Run code with the function's role |
iam:UpdateAssumeRolePolicy | Rewrite trust policy to let you assume it |
Impact proof (benign): list a bucket you were told is in scope, sts assume-role into the admin role and get-caller-identity to show the new ARN, or read a canary object. Don't touch real PII.
IMDSv1 (no token): GET http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>
IMDSv2 (token): PUT .../latest/api/token (X-aws-ec2-metadata-token-ttl-seconds: 21600)
then GET with X-aws-ec2-metadata-token: <token>
GCP: GET http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
header: Metadata-Flavor: Google
Azure: GET http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/
header: Metadata: true
Got temp creds → jump to section 1 (AWS) / 3 (GCP) / 4 (Azure) and enumerate what THAT role can do. SSRF alone is Medium; SSRF → metadata creds → data/admin is Critical.
3. GCP — IMPERSONATION IS THE PIVOT
gcloud auth activate-service-account --key-file=sa.json # or use the token
gcloud projects get-iam-policy <project>
gcloud iam service-accounts list
| Permission | Escalation |
|---|
iam.serviceAccounts.getAccessToken / actAs | Impersonate a higher-priv SA (--impersonate-service-account) |
iam.serviceAccountKeys.create | Mint a key for a privileged SA |
iam.roles.update on a custom role you hold | Add permissions to yourself |
cloudfunctions.functions.update / deploy | Run code as the function's SA |
storage.objects.get on a sensitive bucket | Read secrets/state |
Owner/Editor at project level = effectively admin. Check for the default Compute SA having Editor — extremely common.
4. AZURE — MANAGED IDENTITY & RBAC
az account show
az role assignment list --assignee <objectId> --all
az resource list
| Path | Escalation |
|---|
| Managed identity with Contributor | Deploy/modify resources, run commands on VMs (az vm run-command) |
Microsoft.Authorization/*/write | Assign yourself Owner |
| Key Vault access policy / RBAC | Read secrets, certs, keys |
| Automation Account / Runbook | Execute as the automation identity |
| Storage account key read | Full blob access |
5. STORAGE — S3 / GCS / BLOB
[ ] List: aws s3 ls s3://<bucket> --no-sign-request (public?)
[ ] Read/Write ACL: get-bucket-acl / put-object test (own canary file only)
[ ] Bucket policy allows *? cross-account?
[ ] Versioning / logging exposes old secrets?
[ ] Website / static hosting → subdomain takeover angle (see web2-vuln-classes)
Writable public bucket serving a live site = Critical (content injection / takeover). Readable bucket with secrets/PII = High/Critical.
6. SECRETS HARVEST (post-access)
[ ] Lambda/Cloud Function/App env vars (often hold API keys, DB creds)
[ ] SSM Parameter Store / Secrets Manager (if perms allow)
[ ] EC2 user-data / instance tags
[ ] Terraform state in the bucket you just read
[ ] CI/CD variables reachable from the identity
Each secret → re-run "identify the identity" for the NEW credential. Chain until you hit admin or crown-jewel data, then stop and write it up.
7. REPORT LINE (per finding)
Entry: <leaked key | SSRF→metadata | public bucket | SA JSON>
Identity: <ARN / SA / object id> — <what it could do>
Escalation: <exact permission → action that raised privilege>
Impact proven: <assumed admin ARN | read canary | listed in-scope resource>
Blast radius: <accounts/services/data reachable>
Fix: <least-privilege policy | IMDSv2 enforce | block-public-access | rotate>
Impact must be demonstrated with a concrete call, using benign/own-account objects. "The key might allow…" is not a finding — show get-caller-identity after the escalation.