Guardduty, фишинг. Часть 1 из 2.
Никого не трогаю. Прилетает алёрт от AWS GuardDuty.
Backdoor:EC2/C&CActivity.B!DNS
Продакшн нода в EKS делает DNS-запрос к
x****.cn - по базам GuardDuty это известный фишинговый/DGA домен. Серьёзность - High. В слаке шум, все смотрят на меня.
Блин, почему в мою смену секьюрити инциденты 😬
Окей. Поехали.
Cмотрим что за нода
aws ec2 describe-instances \
--instance-ids i-0xxxxxxxxxxxxxxxxx \
--region us-east-1 \
--query 'Reservations[0].Instances[0].{IP:PrivateIpAddress,State:State.Name,LaunchTime:LaunchTime}'
Нода уже terminated. Karpenter убил её раньше, чем я добрался до расследования.
Живого форензика (digital forensics) нет.
Прекрасное начало.
kubectl get nodes -o wide
# ноды нет
Ладно, живыми данными не поработать, идём в логи.
Cмотрим что вообще крутилось на ноде
Хорошо, что у нас включено всё логирование control plane EKS. Идём в CloudWatch, смотрим логи шедулера - кто и когда был назначен на эту ноду.
aws logs start-query \
--log-group-name /aws/eks/my-cluster/cluster \
--start-time $(date -v-83H +%s) \
--end-time $(date +%s) \
--query-string 'fields @timestamp, @message | filter @logStream like "scheduler" | filter @message like "ip-10-0-25-132" | sort @timestamp asc | limit 50' \
--region us-east-1
Первая попытка падает с
MalformedQueryException. Оказывается, ретеншн у лог группы EKS - 84 часа. Нода жила 4 дня. Часть ранних данных уже недоступна. Запоминаем это на потом.За те данные, что есть, видим волну деплоя примерно за час до алёрта: новые ReplicaSet хеши, несколько сервисов прокатились роллинг апдейтом. Подозрительно по тайминингу. Ухожу копать туда.
Cмотрим образы, которые пушились незадолго до инцидента
aws ecr describe-images \
--repository-name my-ecr-repo \
--region us-east-1 \
--query 'sort_by(imageDetails, &imagePushedAt)[-10:].[imagePushedAt,imageTags[0],imageDigest]' \
--output table
Вижу два образа, которые запушились за ~30-60 минут до алёрта GuardDuty. Первая гипотеза: кто-то накатил заражённый образ.
Лезу в git, смотрю коммиты.
Один коммит - это Dependabot, обновил grpc. Проверяю go.sum - стандартные пакеты, нормальные хеши. Второй коммит - двухфайловый вайтспейс-фикс в SQL query builder, strings.TrimSpace в одном файле.
Всё это вообще не то. Red herring, мать ее.
Исключаем DaemonSet
kubectl get daemonsets -A
DaemonSet крутится на всех нодах кластера - их там под 44 штуки. Если бы DaemonSet был заражён, GuardDuty бы полыхал по всем нодам разом. Сработало только на одну. Значит, не DaemonSet.
Базовый образ Dockerfile смотрю - официальный golang bookworm, мультистейдж, всё чисто.
Смотрю дополнительные файндинги GuardDuty
aws guardduty list-findings \
--detector-id $(aws guardduty list-detectors --query 'DetectorIds[0]' --output text) \
--finding-criteria '{"Criterion":{"type":{"Eq":["Backdoor:EC2/C&CActivity.B!DNS","Trojan:EC2/DNSDataExfiltration","DefenseEvasion:EC2/UnusualDoHActivity"]}}}' \
--region us-east-1
Нашлось ещё пара файндингов - не игнорируем, но пока продолжаем копать основное.
Наконец-то заглядываю в логи приложения (когда же я, бл, научусь это смотреть первым 🤡)
Открываю OpenSearch, ищу по домену
x********.cn , в индексе EKS-логов.8 записей. Все - из одного контейнера. Смотрю на таймстемпы:
12:51:19 - PaymentWebhookProcessor: starting to parse note
12:51:19 - PaymentWebhookProcessor: no images found in note
...
13:21:38 - PaymentWebhookProcessor: starting to parse note <- ВОООТ ОНО
13:21:38 - PaymentWebhookProc...
13:21:38 UTC - точь-в-точь время алёрта GuardDuty.
Попался!