IAM misconfigurations
Конфиги IAM, превращающие одну точку опоры в захват аккаунта: iam:PassRole плюс сервис, исполняющий код, — это лестница эскалации привилегий, а сторонний сервис, которому вы дали AssumeRole без ExternalId, — это confused deputy. И то и другое — выданные права, а не баги.
У пентестера не было админских прав. У него был один утёкший CI-ключ для деплоя с политикой, которая команде читалась как «может деплоить наш сервис». На деле она давала iam:PassRole на wildcard плюс lambda:CreateFunction. Через сорок минут он стал root в аккаунте — не эксплуатируя CVE, а используя права ровно так, как они написаны: создать Lambda, передать ей админскую роль, которая уже валялась в аккаунте, вызвать её, прочитать учётки из окружения. Каждый вызов API возвращал 200. CloudTrail логировал каждый как авторизованный, потому что таким он и был. Пробой был не дырой в IAM. Это IAM делал ровно то, что велела политика.
К концу урока ты сможешь распознать два конфига IAM, превращающих единственную точку опоры в захват аккаунта, — цепочки эскалации привилегий и confused deputy — и назвать то единственное свойство каждого, что рвёт цепочку.
Почему конфиги IAM — отдельная категория
Ранее в этом юните ты разобрал модель облачной идентичности, написание least-privilege-политик и то, как работают assume-роли и федерация. Этот урок — о том, где всё это ломается в продакшене: не через эксплуатируемый код, а через права, по отдельности защитимые, а в сумме катастрофические.
Это различие — суть всего. SQL-инъекция — это баг: код делает то, чего автор не задумывал. Конфиг IAM — это выданное право: система делает ровно то, что говорит политика, а политика говорит слишком много. Сканеры, ищущие искажённый ввод, не находят ничего, потому что искажать нечего. CloudTrail показывает чистый 200 на каждом вызове, потому что каждый вызов был авторизован. Ущерб нанесён целиком внутри границ права, которое ты выписал. Поэтому ревью IAM читается как достижимость, а не синтаксис: вопрос никогда не «валидна ли эта политика?» — он «каково транзитивное замыкание всего, до чего этот принципал может дотянуться?».
Два сценария доминируют в реальных инцидентах, и зрелый инженер учится видеть оба как пути, а не утверждения: эскалация привилегий (право, позволяющее принципалу выдать себе больше) и confused deputy (доверенный сервис, действующий по вводу атакующего своей собственной властью, а не властью атакующего).
Эскалация привилегий: когда право позволяет выдать себе больше
Эскалация привилегий — это конфиг, при котором у принципала есть право, которое, используемое по назначению, позволяет ему получить права, которые ему никогда не давали. Самое важное для распознавания — iam:PassRole.
PassRole существует по легитимной причине: когда ты создаёшь Lambda-функцию, EC2-инстанс или ECS-задачу, что-то должно прикрепить исполнительную роль, чтобы воркод мог вызывать API AWS. PassRole — это право передать роль сервису. Ловушка — в области ресурса. Политика с iam:PassRole на ресурсе-wildcard означает не «передай свою узкую роль» — она означает передай любую роль в аккаунте, включая админскую. Соедини это с любым сервисом, исполняющим код (lambda:CreateFunction + lambda:InvokeFunction или ec2:RunInstances), — и у тебя готова целая лестница:
PassRole — заголовок, но семейство большое, и зрелый навык — распознавать паттерн, а не зубрить список. Любое из этих — одношаговая эскалация, если выдано на wildcard:
| Право | Выглядит как | На деле позволяет | Сужение, что останавливает |
|---|---|---|---|
iam:PassRole + run-code | «Деплоить Lambda» | Исполнить код под любой ролью, вкл. админскую | Сузить до одного ARN + iam:PassedToService |
iam:CreatePolicyVersion | «Править наши политики» | Переписать прикреплённую политику на «всё» | Deny на своём boundary; permissions boundary |
iam:AttachUserPolicy | «Управлять доступом команды» | Прикрепить AdministratorAccess себе | Permissions boundary ограничивает эффект |
lambda:UpdateFunctionCode | «Залить хотфикс» | Подменить код в функции с высокопривилегированной ролью | Разделить деплой-идентичность и привилегированные exec-роли |
Защита — не «запретить эти действия»: они несущие. Это сужение по ресурсу плюс permissions boundary. Сузь PassRole до точного ARN роли, нужной воркоду; никогда не wildcard. Затем оберни любую идентичность, способную создавать или прикреплять права, в permissions boundary — потолок, который принципал не может превысить, какую бы политику он себе ни выдал. Boundary — это то, что превращает AttachUserPolicy из «прикрепить админа» в «прикрепить что угодно, ограниченное boundary».
Confused deputy: когда доверенный сервис действует по вводу атакующего
Второй сценарий тоньше, потому что атакующий никогда не касается твоего аккаунта напрямую. Confused deputy — это привилегированный компонент, обманом заставленный злоупотребить своей собственной властью в пользу менее привилегированного вызывающего. Классический облачный случай: ты даёшь стороннему SaaS (инструменту мониторинга, бэкап-вендору) право assume-роли в твоём аккаунте, чтобы он читал твои данные. Вендор принимает одну роль, чтобы обслуживать всех своих клиентов.
Вот ловушка. Вызову assume-роли вендора нужны лишь id целевого аккаунта и имя роли — и то и другое может угадать вредоносный другой клиент того же вендора. Клиент B просит вендора «мониторить аккаунт 1234 / роль MyAuditRole». Вендор, как и спроектировано, принимает эту роль. Теперь вендор — deputy — читает твой аккаунт по поручению клиента B. Вендор не был скомпрометирован; он сделал ровно свою работу. Он был запутан в том, чей запрос он обслуживает.
Фикс — общий секрет, вшитый в доверительную связь: ExternalId. Настраивая вендора, ты договариваешься о пер-тенантном секрете и кладёшь "sts:ExternalId": "твоё-уникальное-значение" условием в trust-политику роли. Теперь вендор должен предъявить твой ExternalId, чтобы принять твою роль, — а клиент B, который может угадать id аккаунта и имя роли, не может угадать секрет, который ему не давали. Тот же принцип обобщается: confused deputy побеждается привязкой решения об авторизации к идентичности исходного запрашивающего, а не только к идентичности deputy. В твоих собственных сервисах эквивалент — пробрасывать и проверять идентичность конечного пользователя до самого ресурса, никогда не позволяя привилегированному бэкенду действовать по сырой, подверженной влиянию атакующего цели.
▸Почему это работает
Почему «роль вендора в IAM — только на чтение» сама по себе недостаточна? Потому что чтение в чужом аккаунте — всё равно пробой: confused deputy не повышает привилегию deputy, он перенаправляет уже имеющуюся привилегию deputy на жертву, которую тот трогать был не должен. ExternalId работает на ином уровне, чем область прав: область отвечает «что deputy может делать?», ExternalId отвечает «для кого ему разрешено это делать?». Нужно и то и другое. Роль только на чтение без ExternalId всё равно сливает данные каждого клиента любому другому клиенту, способному назвать аккаунт.
Как зрелый инженер ревьюит изменение IAM: думай в blast radius
Объединяющая мысленная модель — blast radius (радиус поражения): если долгоживущая учётка ровно этого принципала утечёт атакующему завтра, каково транзитивное замыкание всего, до чего он дотянется? Не «для чего эта роль» — а чем она может стать. Деплой-ключ, способный передать любую роль, имеет blast radius во весь аккаунт, хотя его работа — пушить один сервис.
Этот сдвиг меняет то, как ты читаешь политики. Ты перестаёшь искать синтаксические ошибки и начинаешь трассировать достижимость: позволяет ли какое-то право этому принципалу получить другую идентичность (эскалация)? Позволяет ли какая-то trust-политика внешней стороне одолжить власть этого аккаунта, не доказав, что она — предполагаемая сторона (confused deputy)? Контроли чисто ложатся на эти два вопроса — least-privilege-сужение по ресурсу и permissions boundary ограничивают, чем принципал может стать; ExternalId и проброс идентичности на каждый запрос ограничивают, по чьему поручению deputy может действовать. Default-deny — пол под обоими: каждое право начинается с «нет» и расширяется лишь до названного ресурса по названной причине.
CI-роль деплоя должна создавать Lambda-функции и прикреплять им исполнительную роль. Ревью безопасности отметило iam:PassRole, сужённый до wildcard-ресурса. Выбери верное исправление.
Почему атаки эскалации привилегий IAM и confused deputy обычно ускользают от сканеров ввода и показывают чистые логи CloudTrail?
Сторонний бэкап-вендор принимает роль в твоём аккаунте. Роль — только на чтение и узко сужена. Другой клиент этого вендора подсовывает id твоего аккаунта и имя роли и заставляет вендора прочитать твои бакеты. Чего не хватало?
Упорядочь шаги цепочки эскалации привилегий через PassRole, от исходной точки опоры до захвата аккаунта:
- 1 Атакующий держит низкопривилегированный ключ с iam:PassRole на wildcard-ресурсе
- 2 Вызывает lambda:CreateFunction, чтобы создать новую функцию
- 3 Передаёт админскую роль аккаунта как исполнительную роль функции
- 4 Вызывает функцию, которая теперь исполняется с админскими правами
- 5 Читает учётки / действует как админ — полный захват аккаунта
- 01Пройди по цепочке эскалации привилегий через iam:PassRole и назови единственное изменение сужения, которое её рвёт.
- 02Объясни межаккаунтную атаку confused deputy против стороннего вендора и почему роль только на чтение и узко сужённая недостаточна без ExternalId.
Конфиги IAM — категория отдельная от обычных багов: система делает ровно то, что говорит политика, а политика просто говорит слишком много, поэтому сканеры ввода не находят ничего, а CloudTrail показывает чистый 200 на каждом шаге. Два сценария двигают реальные пробои. Эскалация привилегий — это грант, позволяющий принципалу выдать себе больше: канонический случай — wildcard iam:PassRole в паре с возможностью исполнять код, что позволяет низкопривилегированному ключу создать Lambda, передать ей админскую роль аккаунта, вызвать и захватить аккаунт; фикс — сужение PassRole по ресурсу до одного ARN плюс permissions boundary как потолок. Confused deputy — это доверенный сервис, обманом заставленный использовать свою власть для атакующего: сторонний вендор, принимающий роль для каждого клиента, может быть заставлен прочитать твой аккаунт любым, кто знает id аккаунта и имя роли; фикс — sts:ExternalId, пер-тенантный секрет, вшитый в trust-политику, который атакующий подсунуть не может. Оба сводятся к одному зрелому рефлексу: ревьюй каждое изменение IAM по его blast radius — если эта учётка утечёт завтра, каково транзитивное замыкание всего, чем она может стать или на что быть нацелена? Сужай, чем принципал может стать; привязывай, по чьему поручению deputy может действовать; default-deny под обоими.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.