Один изменённый ID: как AI-пентест дошёл до полного захвата аккаунта
Реальный энгейджмент, проведённый от начала до конца автономным AI-агентом Sintropyc для пентеста. Он начался с одного перечислимого ID объекта и закончился на приватной админ-панели — а по пути агент отбраковал собственную первую находку как вероятный ложноположительный результат. Все идентифицирующие детали анонимизированы; приложение тестировалось с разрешения владельца.
Суть в одной фразе
Автономный AI-агент протестировал e-commerce SaaS так, как это делал бы реальный атакующий. Он обнаружил, что может читать приватные профили других участников, меняя одно целое число, отбраковал эту находку как вероятный ложный результат, перепроверил её на действительно раздельных аккаунтах, а затем прошёл приложение до самого админа — прочитав приватную панель выручки как клиент, зарегистрированный шестьдесят секунд назад. Каждый шаг — доказанный эффект, а не «возможно» от сканера.
Тестирование проводилось с разрешения владельца приложения, по умолчанию только на чтение, в изолированной песочнице, с безопасными обратимыми payload'ами. Цель, её домен, реальные пользователи и любые секреты анонимизированы или опущены — ничто здесь не указывает на реального человека, учётные данные или систему.
Почему этот баг продолжает побеждать
Broken Object-Level Authorization — BOLA, API-родственник классического IDOR — занимает первое место в OWASP API Security Top 10. Баг скучный и повсеместный: эндпоинт вроде GET /api/members/{id} возвращает объект для любого id, который вы передали, не проверяя, что вам позволено его видеть. Меняете число — читаете чужие данные.
Сканеры с этим справляются плохо. Сканер видит, что GET /api/members/42 вернул 200 OK, и пожимает плечами — 200 и должен возвращаться. Принадлежит ли запись вам или постороннему — это вопрос об идентичности и бизнес-контексте, а не о HTTP-статусах. Именно поэтому этот класс чаще всего находят люди и чаще всего пропускают автоматические инструменты — и именно его наш агент доказывает чаще всего.
Сигнал: один изменённый ID
Целью был storefront-SaaS — SPA, общающийся с JSON API. Агент составил карту API и заметил две вещи, важные для багов авторизации: ID участников были последовательными целыми числами, а объект участника нёс подсказку для восстановления пароля — приватное поле, которое никогда не должно покидать сессию владельца.
Он зарегистрировал Аккаунт A и задал простой вопрос: как A, могу ли я прочитать участника 1? И 10? Оба вернули полные профили, включая поле с подсказкой. На бумаге — учебный IDOR. Сканер или нетерпеливый тестировщик сделал бы скриншот 200 и записал бы HIGH.
Наш агент — нет.
Ложный результат, который агент отбросил
Прежде чем что-либо фиксировать, агент применил стандарт, которому подчиняет каждую заявку об авторизации: межаккаунтное чтение — это реальный пробой, только если две личности являются действительно разными владельцами без легитимного общего доступа к приватному ресурсу. И он заметил дыру в собственных доказательствах — во многих приложениях самозарегистрированные аккаунты незаметно попадают в один и тот же workspace по умолчанию. Если A и «жертва» делят workspace, то чтение этой записи аккаунтом A — не пробой границы, а авторизованный общий доступ. Реальная уязвимость и обычная фича могут выглядеть в ответе байт в байт одинаково.
Поэтому агент понизил и отклонил собственную заявку и поставил себе задачу сложнее: доказать на владельцах, которые однозначно раздельны. Это крупнейший источник ложных срабатываний в тестировании контроля доступа.
Он зарегистрировал второй, независимый аккаунт, убедился, что это разные владельцы без общего членства, и повторил межаккаунтное чтение. Оно сработало. Один аккаунт мог прочитать приватный профиль другого владельца, изменив лишь одно целое число в URL. Теперь это была находка — с severity ровно на уровне показанного.
От чтения до всего магазина
Примитив чтения — это плохо. Захватом это сделал следующий вопрос: если сервер не проверяет, кто может читать объект, проверяет ли он, кто может писать — и что писать? Не проверял.
Эндпоинты регистрации и обновления профиля принимали присланное клиентом поле role и сохраняли его как есть. Агент зарегистрировал свежего клиента и выставил себе роль admin. Чтобы доказать, что эскалация реальна, он воспользовался привилегией: как клиент возрастом в минуты, он открыл приватную админ-панель — общее число участников и выручку — и получил 200 OK. Затем повторил трюк между пользователями: один клиент сменил роль другому клиенту. Ни проверки владения, ни проверки роли и на пути записи. Все изменения были откатаны безопасными обратимыми маркерами.
Двери, у которых вообще не было замка
Агент также попробовал парадные двери без ключа. Несколько эндпоинтов /admin/* и эндпоинт со списком пользователей отдавали данные полностью неаутентифицированному запросу: конфигурацию магазина, панель выручки, а также email и полное имя каждого участника. Ни токена, ни сессии — просто GET. Он также нашёл классический изъян токенов: сервер принимал JWT с alg: none и доверял подделанным claim'ам. Там, где секрет подписи наблюдать не удалось, агент так и сказал и пометил это как выведенное, а не доказанное.
Как убедиться, что ваше приложение так не делает
Фиксы непафосные и работают:
- Авторизуйте каждый доступ к объекту на сервере, по умолчанию — запрет. На каждом маршруте
/{id}проверяйте, что вызывающий вправе видеть или менять именно этот объект. - Никогда не привязывайте клиентский ввод к привилегированным полям. Разрешайте по allow-list только записываемые поля;
role,tier,ownerставит сервер через admin-only пути. - Считайте авторизацию как «владелец против владельца». Тестируйте на двух действительно разных аккаунтах.
- Почините слой токенов. Отклоняйте
alg: none, фиксируйте алгоритм, проверяйте подписи, ротируйте утёкшие секреты. - Считайте каждый эндпоинт публичным, пока не доказана авторизация — особенно
/adminи/internal.
Дело не в баге — дело в доказательстве
Каждая находка здесь доказана эффектом, а не утверждением по сигнатуре. Возвращённая чужая запись. Смена роли, которая сохранилась и открыла дверь. Неаутентифицированный 200 на данных, требующих сессии. А когда доказательства не дотягивали до планки, агент так и говорил и либо перепроверял, либо ограничивал заявку. Доказательство, а не догадка. Severity никогда не выше показанного.