Resposta a incidente: EC2 com minerador e credencial da role vazada, da contenção à reconstrução
1. Objetivo Roteiro de resposta a incidente para uma EC2 Linux comprometida, no cenário mais comum que aparece em conta AWS: minerador de criptomoeda (XMRig ou parecido) rodando na instância, muitas vezes com roubo das
1. Objetivo
Roteiro de resposta a incidente para uma EC2 Linux comprometida, no cenário mais comum que aparece em conta AWS: minerador de criptomoeda (XMRig ou parecido) rodando na instância, muitas vezes com roubo das credenciais da instance role via metadata.
A ideia é conter rápido sem destruir evidência, entender como o atacante entrou e o que ele fez com as credenciais, e só depois reconstruir. A ordem dos passos importa. Desligar a instância no susto é o erro mais comum, e ele apaga a memória, que é onde está boa parte da história.
Resultado esperado ao final:
- Instância isolada na rede, fora do Auto Scaling e com proteção contra término.
- Credenciais da role invalidadas.
- Memória e artefatos voláteis coletados com hash.
- Snapshots dos volumes em conta de forense.
- Linha do tempo do ataque e lista de ações feitas com as credenciais roubadas.
- Ambiente reconstruído a partir de imagem limpa.
2. Sinais típicos
| Sinal | Onde aparece |
|---|---|
| CPU em 100% constante sem mudança de tráfego | CloudWatch CPUUtilization
|
CryptoCurrency:EC2/BitcoinTool.B!DNS |
GuardDuty |
UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS |
GuardDuty (credencial da role usada fora da AWS) |
| Conexões de saída para portas 3333, 4444, 5555, 7777, 14444 | VPC Flow Logs |
Processos com nome genérico (kworker, systemd-xxx, .x) em /tmp ou /dev/shm
|
Dentro da instância |
| Instâncias novas em regiões que você não usa | Billing, CloudTrail |
| E-mail da AWS sobre uso abusivo | Caixa do root da conta |
Se apareceu o finding de exfiltração de credencial, o incidente não é só da instância. É da conta. Trate o passo 4 como prioridade.
3. Antes de começar
- [ ] Abra um registro do incidente (ticket, documento) e anote cada comando com horário UTC. Isso vira o relatório depois.
- [ ] Separe uma conta ou VPC de forense, sem rota para a internet e sem peering com produção.
- [ ] Bucket S3 de evidências com versionamento e Object Lock, se possível.
- [ ] Não use a instância comprometida para nada além da coleta.
export AWS_REGION="us-east-1"
export IID="i-<INSTANCE_ID>"
export VPC_ID="vpc-<ID>"
export ROLE_NAME="<ROLE_DA_INSTANCIA>"
export BUCKET_EVID="<BUCKET_EVIDENCIAS>"
export CASO="IR-$(date -u +%Y%m%d)-01"
4. Passo 1: Travar a instância
# Ninguem termina por engano (inclusive voce)
aws ec2 modify-instance-attribute --instance-id "$IID" --disable-api-termination
aws ec2 modify-instance-attribute --instance-id "$IID" --instance-initiated-shutdown-behavior stop
# Marca para todo mundo saber
aws ec2 create-tags --resources "$IID" \
--tags Key=Incidente,Value="$CASO" Key=NaoMexer,Value=forense
Se a instância está num Auto Scaling group, tire de lá antes de qualquer coisa. Senão, quando você isolar a rede, o health check falha e o ASG termina a instância sozinho:
ASG=$(aws autoscaling describe-auto-scaling-instances --instance-ids "$IID" \
--query 'AutoScalingInstances[0].AutoScalingGroupName' --output text)
if [ "$ASG" != "None" ]; then
aws autoscaling detach-instances --instance-ids "$IID" \
--auto-scaling-group-name "$ASG" --no-should-decrement-desired-capacity
fi
Se estiver atrás de load balancer, desregistre dos target groups. O ASG coloca uma instância nova no lugar e o serviço continua.
5. Passo 2: Isolar a rede (do jeito certo)
Aqui tem uma pegadinha que pouca gente conhece: trocar o security group não derruba conexões já rastreadas. Se o atacante tem uma sessão reversa aberta, ela continua viva depois que você coloca um SG sem regras.
O truque documentado no guia de resposta a incidentes da AWS é passar por um SG intermediário que deixa as conexões não rastreadas, e só então aplicar o SG de isolamento:
# SG 1: tudo liberado (0.0.0.0/0 entrada e saida em todos os protocolos = conexao nao rastreada)
SG_UNTRACK=$(aws ec2 create-security-group --group-name "ir-untrack-${CASO}" \
--description "IR passo intermediario" --vpc-id "$VPC_ID" --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG_UNTRACK" \
--ip-permissions 'IpProtocol=-1,IpRanges=[{CidrIp=0.0.0.0/0}]'
# SG 2: isolamento, sem nenhuma regra
SG_ISOL=$(aws ec2 create-security-group --group-name "ir-isolamento-${CASO}" \
--description "IR isolamento" --vpc-id "$VPC_ID" --query GroupId --output text)
aws ec2 revoke-security-group-egress --group-id "$SG_ISOL" \
--ip-permissions 'IpProtocol=-1,IpRanges=[{CidrIp=0.0.0.0/0}]'
# Aplica o 1 e logo depois o 2
aws ec2 modify-instance-attribute --instance-id "$IID" --groups "$SG_UNTRACK"
sleep 5
aws ec2 modify-instance-attribute --instance-id "$IID" --groups "$SG_ISOL"
Se a instância tem mais de uma ENI, repita para cada uma (modify-network-interface-attribute).
E a coleta, se a rede está fechada? O SSM Agent precisa sair para os endpoints do Systems Manager. Se a VPC tem endpoints de interface para
ssm,ssmmessageseec2messages, libere no SG de isolamento só a saída 443 para o SG desses endpoints. Sem endpoint, a alternativa é coletar pelo snapshot e abrir mão da memória.
Desassocie também o Elastic IP, se houver, e anote qual era.
6. Passo 3: Invalidar as credenciais da role
As credenciais que o atacante pegou pelo IMDS continuam válidas até expirar (até 6 horas, dependendo do caso), e funcionam de qualquer lugar do mundo. Remover a role da instância não invalida o que já foi emitido.
Aplique uma deny para tudo emitido antes de agora. É o mesmo que o botão Revoke active sessions do console faz:
AGORA=$(date -u +%Y-%m-%dT%H:%M:%SZ)
cat > deny-old-sessions.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": { "DateLessThan": { "aws:TokenIssueTime": "${AGORA}" } }
}
]
}
EOF
aws iam put-role-policy --role-name "$ROLE_NAME" \
--policy-name "AWSRevokeOlderSessions" \
--policy-document file://deny-old-sessions.json
Isso derruba também as sessões legítimas de outras instâncias que usam a mesma role. Elas pegam credencial nova no IMDS automaticamente, então o impacto costuma ser só alguns erros por alguns minutos.
Se a instância tinha access key em arquivo (~/.aws/credentials, .env, variável de app), desative todas agora:
aws iam list-access-keys --user-name <USUARIO>
aws iam update-access-key --user-name <USUARIO> --access-key-id <AKIA...> --status Inactive
7. Passo 4: O que o atacante fez com as credenciais
Para credenciais de instance role, o nome da sessão é o ID da instância. Isso facilita muito a busca:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=Username,AttributeValue="$IID" \
--start-time "$(date -u -d '-90 days' +%FT%TZ)" \
--max-results 1000 \
--query 'Events[].CloudTrailEvent' --output text \
| jq -r '[.eventTime, .awsRegion, .eventSource, .eventName, .sourceIPAddress, (.errorCode // "ok")] | @tsv' \
| sort > "cloudtrail-${IID}.tsv"
O que procurar no resultado:
-
sourceIPAddressque não é IP da AWS nem da própria instância. Isso é a credencial sendo usada de fora. -
ConsoleLoginouGetFederationToken: o atacante transformou a credencial em sessão de console. -
CreateUser,CreateAccessKey,AttachUserPolicy,CreateLoginProfile: persistência. -
RunInstancesem outras regiões: mais mineradores. -
PutBucketPolicy,GetObjectem massa: exfiltração. - Muitos
AccessDeniedseguidos: reconhecimento, tentando descobrir o que a role pode fazer.
O lookup-events só cobre 90 dias e só eventos de gerenciamento, e roda por região. Se o trail da organização manda para S3, faça a busca no Athena ou no CloudTrail Lake, em todas as regiões.
Varredura rápida de recursos criados em todas as regiões:
for r in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
echo "== $r"
aws ec2 describe-instances --region "$r" \
--query 'Reservations[].Instances[].[InstanceId,State.Name,LaunchTime,InstanceType]' --output text
done
Qualquer usuário IAM, access key ou instância que apareça e ninguém reconhece: desative, anote e não apague ainda. Apagar perde evidência.
8. Passo 5: Coletar dados voláteis
Com a rede isolada e o SSM funcionando, rode a coleta. Ela escreve no disco da própria instância, calcula hash e manda para o bucket de evidências. Isso altera um pouco o estado da máquina, é inevitável, e por isso fica registrado.
coleta.sh:
#!/bin/bash
set -u
CASO="$1"; BUCKET="$2"
OUT="/root/ir-${CASO}"; mkdir -p "$OUT"; cd "$OUT"
date -u +%FT%TZ > 00-inicio.txt
uname -a > 01-uname.txt
uptime > 02-uptime.txt
ps auxwwf > 10-ps.txt
ls -la /proc/*/exe 2>/dev/null > 11-proc-exe.txt
for p in /proc/[0-9]*; do
echo "$(basename "$p") $(tr '\0' ' ' < "$p/cmdline" 2>/dev/null)"
done > 12-cmdlines.txt
ss -tunap > 20-conexoes.txt
ip a; ip r > 21-rede.txt 2>&1
cat /etc/ld.so.preload > 30-ld-preload.txt 2>&1
crontab -l -u root > 31-cron-root.txt 2>&1
ls -la /etc/cron* /var/spool/cron* > 32-cron-dirs.txt 2>&1
systemctl list-units --type=service --all > 33-services.txt
find /etc/systemd /lib/systemd /usr/lib/systemd -newermt "-60 days" -type f > 34-systemd-recentes.txt 2>/dev/null
find /tmp /var/tmp /dev/shm -xdev -type f -exec ls -la {} \; > 40-tmp.txt 2>/dev/null
cat /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys > 41-authorized-keys.txt 2>&1
last -Faiw > 50-last.txt 2>&1
lastb -Faiw > 51-lastb.txt 2>&1
cp -a /var/log ./var-log 2>/dev/null
cp /root/.bash_history /home/*/.bash_history ./ 2>/dev/null
date -u +%FT%TZ > 99-fim.txt
cd /root && tar czf "ir-${CASO}.tgz" "ir-${CASO}"
sha256sum "ir-${CASO}.tgz" | tee "ir-${CASO}.tgz.sha256"
aws s3 cp "ir-${CASO}.tgz" "s3://${BUCKET}/${CASO}/"
aws s3 cp "ir-${CASO}.tgz.sha256" "s3://${BUCKET}/${CASO}/"
A instância precisa de permissão
s3:PutObjectno bucket de evidências. Como a role foi bloqueada no passo 3 (a deny vale só para tokens antigos, os novos funcionam), confirme que a role tem essa permissão ou anexe temporariamente uma policy só para o prefixo do caso.
Envio pelo Run Command:
aws s3 cp coleta.sh "s3://${BUCKET_EVID}/scripts/coleta.sh"
CMD_ID=$(aws ssm send-command --instance-ids "$IID" \
--document-name AWS-RunShellScript \
--parameters "commands=[\"aws s3 cp s3://${BUCKET_EVID}/scripts/coleta.sh /root/coleta.sh\",\"bash /root/coleta.sh ${CASO} ${BUCKET_EVID}\"]" \
--output-s3-bucket-name "$BUCKET_EVID" --output-s3-key-prefix "${CASO}/ssm" \
--query 'Command.CommandId' --output text)
aws ssm get-command-invocation --command-id "$CMD_ID" --instance-id "$IID" \
--query '{status:Status,saida:StandardOutputContent}'
8.1 Memória
Para captura de RAM, o caminho mais simples hoje é o AVML (binário estático, não precisa compilar módulo de kernel como o LiME):
curl -sLo /root/avml https://github.com/microsoft/avml/releases/latest/download/avml
chmod +x /root/avml
/root/avml --compress /root/mem-${CASO}.lime.compressed
sha256sum /root/mem-${CASO}.lime.compressed
aws s3 cp /root/mem-${CASO}.lime.compressed s3://${BUCKET_EVID}/${CASO}/
Como a instância está isolada, ela não baixa nada da internet. Suba o binário para o bucket de evidências antes e copie de lá. A imagem de memória tem o tamanho da RAM, então confirme espaço em disco.
Rootkit de usuário. Se o
/etc/ld.so.preloadnão está vazio, os comandosps,lsessda própria máquina podem estar mentindo. A coleta continua valendo (ela registra o próprio arquivo), mas a análise confiável é a do snapshot montado em outra máquina, e a da memória.
9. Passo 6: Snapshots e envio para a conta de forense
VOLS=$(aws ec2 describe-instances --instance-ids "$IID" \
--query 'Reservations[0].Instances[0].BlockDeviceMappings[].Ebs.VolumeId' --output text)
for v in $VOLS; do
aws ec2 create-snapshot --volume-id "$v" \
--description "${CASO} ${IID} ${v}" \
--tag-specifications "ResourceType=snapshot,Tags=[{Key=Incidente,Value=${CASO}}]" \
--query SnapshotId --output text
done
Compartilhar com a conta de forense:
-
Volume sem criptografia:
modify-snapshot-attributecomcreateVolumePermissionpara a conta de forense. -
Criptografado com a chave padrão
aws/ebs: não dá para compartilhar. Façacopy-snapshotrecriptografando com uma CMK sua, dêkms:Decrypt,kms:CreateGrantekms:DescribeKeydessa CMK para a conta de forense e compartilhe a cópia.
aws ec2 copy-snapshot --source-region "$AWS_REGION" --source-snapshot-id <SNAP_ID> \
--encrypted --kms-key-id <ARN_CMK_FORENSE> --description "${CASO} copia forense"
aws ec2 modify-snapshot-attribute --snapshot-id <SNAP_COPIA> \
--attribute createVolumePermission --operation-type add \
--user-ids <ACCOUNT_FORENSE>
10. Passo 7: Análise do disco
Na conta de forense, suba uma EC2 de análise numa subnet sem internet, crie um volume a partir do snapshot na mesma AZ e anexe. Monte somente leitura e sem replay de journal. Sem noload, o ext4 aplica o journal ao montar e altera o disco que você quer analisar:
lsblk
sudo mkdir -p /mnt/evid
# ext4
sudo mount -o ro,noload,noexec,nosuid,nodev /dev/nvme1n1p1 /mnt/evid
# xfs (Amazon Linux 2/2023 e RHEL usam xfs)
sudo mount -o ro,norecovery,nouuid,noexec,nosuid,nodev /dev/nvme1n1p1 /mnt/evid
O nouuid no xfs é porque o volume analisado costuma ter o mesmo UUID do disco raiz da máquina de análise, se as duas vieram da mesma AMI.
10.1 Linha do tempo
sudo find /mnt/evid -xdev -newermt "<DATA_INICIO_SUSPEITA>" \
-printf '%TY-%Tm-%Td %TH:%TM:%TS %u %m %p\n' 2>/dev/null \
| sort > timeline.txt
grep -E "/tmp/|/dev/shm|/var/tmp|/\.[^/]+$|cron|systemd/system|authorized_keys|ld.so.preload" timeline.txt
10.2 O minerador
# Config do XMRig e derivados
sudo grep -rIl --exclude-dir={proc,sys} -E '"(pool|url)"\s*:\s*".*:[0-9]{4,5}"|stratum\+tcp|donate-level' /mnt/evid 2>/dev/null
# Binarios ELF em diretorios onde nao deveriam existir
sudo find /mnt/evid/tmp /mnt/evid/var/tmp /mnt/evid/dev/shm /mnt/evid/root /mnt/evid/home \
-type f -exec file {} \; 2>/dev/null | grep ELF
# Persistencia
sudo cat /mnt/evid/etc/ld.so.preload 2>/dev/null
sudo ls -la /mnt/evid/etc/cron.d /mnt/evid/var/spool/cron/ /mnt/evid/var/spool/cron/crontabs 2>/dev/null
sudo grep -rl -E "curl|wget|base64 -d|/tmp/|/dev/shm" /mnt/evid/etc/systemd /mnt/evid/etc/cron* /mnt/evid/etc/rc.local 2>/dev/null
Calcule o SHA-256 dos binários suspeitos e consulte em base pública de malware. Não execute nada.
10.3 Por onde entrou
Os vetores que mais aparecem nesse tipo de caso:
| Vetor | Onde olhar |
|---|---|
| SSH com senha fraca |
/var/log/auth.log ou /var/log/secure: muitos Failed password e depois um Accepted password
|
PostgreSQL exposto com COPY ... FROM PROGRAM
|
Log do Postgres procurando FROM PROGRAM, e pg_hba.conf aceitando 0.0.0.0/0
|
| Redis sem senha |
CONFIG SET dir e escrita em authorized_keys ou cron |
| Aplicação web vulnerável | Access log do Nginx/Apache próximo ao horário do primeiro arquivo malicioso |
| Docker API exposta (2375) | Containers e imagens estranhas, /var/lib/docker
|
sudo grep -h "Accepted" /mnt/evid/var/log/auth.log* /mnt/evid/var/log/secure* 2>/dev/null | tail -50
sudo grep -rh -i "from program" /mnt/evid/var/lib/pgsql /mnt/evid/var/log/postgresql 2>/dev/null | head
Cruze o horário do primeiro acesso com o primeiro arquivo malicioso na timeline e com o primeiro evento de fora da AWS no CloudTrail do passo 4. Quando os três batem, você tem a história.
10.4 Roubo de credencial pelo IMDS
Se o GuardDuty acusou exfiltração, procure como o atacante chegou no metadata:
sudo grep -rh "169.254.169.254" /mnt/evid/root/.bash_history /mnt/evid/home/*/.bash_history /mnt/evid/tmp 2>/dev/null
E confira a configuração da instância. IMDSv2 dificulta SSRF, mas não impede quem já tem shell na máquina. Quem tem shell faz o PUT do token e pega a credencial do mesmo jeito.
aws ec2 describe-instances --instance-ids "$IID" \
--query 'Reservations[0].Instances[0].MetadataOptions'
11. Passo 8: Erradicar e reconstruir
Não "limpe" a instância. Não dá para ter certeza de que tudo saiu.
- Suba uma instância nova a partir de AMI oficial ou da sua pipeline de imagem, nunca de AMI criada a partir da comprometida.
- Restaure dados de um backup anterior à data de entrada, validado.
- Troque todos os segredos que existiam na máquina: senhas de banco, tokens de API, chaves SSH, certificados, variáveis de ambiente da aplicação.
- Remova a política de deny do passo 3 só depois de confirmar que não existe credencial persistente criada pelo atacante.
- Apague usuários, chaves e instâncias criados pelo atacante, depois de documentar.
- Mantenha a instância isolada e os snapshots pelo prazo que o jurídico ou o contrato pedirem.
12. Passo 9: Para não acontecer de novo
# IMDSv2 obrigatorio e hop limit 1 (container nao alcanca o IMDS do host)
aws ec2 modify-instance-metadata-options --instance-id <NOVA_INSTANCIA> \
--http-tokens required --http-put-response-hop-limit 1
# Padrao da conta para instancias novas
aws ec2 modify-instance-metadata-defaults --http-tokens required --http-put-response-hop-limit 1
Restrinja o uso da credencial da role ao lugar de onde ela deveria vir. A condição abaixo, numa SCP ou na própria policy, nega uso fora da VPC da instância:
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringNotEquals": { "aws:EC2InstanceSourceVPC": "${aws:SourceVpc}" },
"Null": { "aws:ec2InstanceSourceVPC": "false" },
"BoolIfExists": { "aws:ViaAWSService": "false" }
}
}
Essa regra depende do aws:SourceVpc, que só existe quando a chamada passa por VPC endpoint. Então ela funciona bem em VPC que acessa os serviços por endpoint (STS, S3, SSM etc.). Sem endpoint, use a variação com aws:EC2InstanceSourcePrivateIPv4 e aws:VpcSourceIp, que está na mesma documentação. Teste em uma conta de homologação antes: serviços que chamam outros serviços em seu nome podem cair nessa regra.
Checklist:
- [ ] Nenhum SG com 22, 3389, 5432, 3306, 6379, 2375 ou 9200 aberto para
0.0.0.0/0 - [ ] Acesso administrativo só por SSM Session Manager
- [ ] No PostgreSQL, ninguém além do DBA com
pg_execute_server_programou superuser - [ ] GuardDuty com Runtime Monitoring habilitado em todas as regiões
- [ ] Alarme de
CPUUtilizationalto e prolongado em instâncias que não deveriam ter carga constante - [ ] VPC Flow Logs habilitados e retidos
- [ ] CloudTrail da organização para S3 com retenção longa
- [ ] Alerta de billing por região
13. Referências
- AWS Security Incident Response Guide: https://docs.aws.amazon.com/whitepapers/latest/aws-security-incident-response-guide/welcome.html
- Connection tracking em security groups: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/security-group-connection-tracking.html
- Revogar sessões de role: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_revoke-sessions.html
- Findings do GuardDuty para EC2: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-ec2.html
- AVML: https://github.com/microsoft/avml
- Condition keys de credencial de instância: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.