Эффект домино: одна ошибка, один токен, один эндпоинт — и вся система пала.

Pablo_P

Carder
Messages
56
Reaction score
13
Points
8
Все началось с дикой скуки и чашки отвратительного остывшего кофе. На дворе стоял тот самый серый вторник, когда энтузиазм уже иссяк, а до пятницы — как до Луны. Моей целью был очередной амбициозный проект, который позиционировал себя как "AI-ассистент для управления корпоративными знаниями". Звучит пафосно, да? На деле — еще одна навороченная система управления проектами, но с прикрученным сбоку чат-ботом. ИИ пузыря нету, говорят они))

Программа у них была частная, по инвайтам. Для тех, кто не в теме: это значит, что компания не кричит о своей Bug Bounty на весь мир, а приглашает исследователей лично. Это всегда интригует. Либо они так уверены в своей безопасности, что отсеивают новичков, либо, наоборот, боятся выставлять свое детище на всеобщее обозрение.

Первые несколько часов — это была тоска зеленая. Я копошился в их веб-приложении, проверяя самые очевидные вещи. Это называется "собирать низко висящие фрукты". Ты сначала ищешь самые распространенные и глупые ошибки.
  • SQL-инъекции? Это когда ты пытаешься обмануть базу данных, подсунув ей в поле для ввода (например, в логин или поиск) не текст, а команду. Скажем, ' OR 1=1 --. Если разработчики просто склеивают строки, не проверяя их, база может выполнить твою команду. Но тут все было глухо — они использовали параметризованные запросы, что является правильной защитой.
  • XSS (Межсайтовый скриптинг)? Тут твоя цель — заставить сайт выполнить твой собственный JavaScript-код в браузере другого пользователя. Если получится, можно украсть его сессионные куки и зайти под его аккаунтом. Я пытался вставлять <script>alert(1)</script> во все поля, но их CSP (Content Security Policy) — по сути, набор правил для браузера — блокировала все на корню.
Я переключился на их JWT-токены. JWT — это такой цифровой пропуск, который сервер выдает тебе после логина. Ты потом показываешь этот пропуск при каждом запросе, чтобы доказать, что ты — это ты. Я пробовал стандартные атаки, вроде подмены алгоритма подписи на none, но и тут все было сделано на совесть.

Я уже был готов бросить это дело, как вдруг заметил одну мелкую деталь. В приложении была функция «Поделиться доской для публичного просмотра». Система генерировала ссылку вида https://example.com/boards/view/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d. Эта длинная абракадабра — UUID, универсальный уникальный идентификатор. Его фишка в том, что он настолько длинный и случайный, что угадать или перебрать UUID другого объекта практически невозможно. Поэтому сам по себе он безопасен. Но он выдал мне кое-что важное: я понял, как они идентифицируют объекты в своей системе.

А что у нас еще работает с объектами? Экспорт данных. Я нашел в истории Burp Suite (это мой главный инструмент, как швейцарский нож для хакера, он позволяет перехватывать и изменять все запросы между моим браузером и сервером) запрос: POST /api/v2/projects/export. В теле запроса лежал JSON: {"project_uuid": "f1g2h3i4-j5k6-..."}. Мой UUID.

И тут в голове что-то щелкнуло. Возникла гипотеза, основанная на одной из самых частых уязвимостей в вебе — IDOR (Insecure Direct Object Reference), или небезопасная прямая ссылка на объект. Говоря по-простому, это когда приложение проверяет, что ты хочешь сделать, но забывает проверить, имеешь ли ты на это право. Классический пример: у тебя есть ссылка .../getInvoice?id=123. Если ты поменяешь 123 на 124 и получишь чужой счет — это IDOR.

Я скопировал UUID публичной доски со своего второго аккаунта и подставил его в запрос на экспорт. Я был уверен, что получу ошибку 403 Forbidden (Доступ запрещен).

Отправляю запрос. И сервер отвечает... 202 Accepted.

Это не "да" и не "нет". Код 202 означает: "Я тебя понял, запрос принял, сейчас начну делать, а за результатом зайдешь попозже". Это асинхронная задача. Очень хитрый момент, потому что многие автоматические сканеры безопасности видят ответ, в котором нет твоих данных, и думают, что все в порядке. Я полез в старую документацию к их API, которую когда-то нашел на GitHub. Так и есть: для проверки статуса задачи есть эндпоинт /api/v2/tasks/{task_id}.

Я начал опрашивать этот эндпоинт. Сначала {"status": "processing"}, а через секунд тридцать — {"status": "completed", "result_url": "..."}.

Я прошел по ссылке. И скачал zip-архив с полным дампом чужого проекта. Это был успех. Но что-то подсказывало, что это только начало.

Я распаковал архив. Внутри была куча JSON-файлов. Чтобы не просматривать их вручную, я использовал grep — утилиту командной строки для поиска текста в файлах. Это как Ctrl+F, но на стероидах. Я искал любые слова, которые могут указывать на секреты: key, secret, token, password. И нашел. В файле integration_metadata.json лежал JWT-токен. Я скопировал его и вставил в онлайн-декодер jwt.io.

Для тех, кто не знает: JWT состоит из трех частей, разделенных точками. Заголовок, полезная нагрузка (payload) и подпись. Важно то, что первые две части не зашифрованы, а просто закодированы в Base64. Это как письмо в прозрачном конверте — любой может прочитать, что внутри. Зашифрована только подпись, которая гарантирует, что данные не подменили.

Я посмотрел на payload:
{"iss": "cognisphere-internal", "sub": "sync-service-worker-01", "role": "internal_read_only", "user_id": "system-internal-bot", "exp": 167...}
Токен был просрочен, то есть использовать его как пропуск уже нельзя. Но он дал мне бесценную информацию: user_id внутреннего системного бота.

И тут я подумал о GraphQL. Это такая альтернатива стандартному REST API. Если REST — это как заказ блюд по строгому меню, то GraphQL — это шведский стол, где ты сам говоришь серверу, какие именно данные тебе нужны, и получаешь все одним запросом. Разработчики любят его для внутренних сервисов, потому что он гибкий. А к внутренним сервисам часто относятся менее трепетно в плане безопасности. Что, если у них есть такой эндпоинт, торчащий наружу?

Я запустил ffuf (инструмент для перебора файлов и директорий на сайте) со словарем популярных путей для GraphQL. И он нашел: https://api.example.com/v1/internal/gql.

Я открыл GraphQL-клиент и отправил на этот адрес запрос интроспекции. Это встроенная в GraphQL функция, которая позволяет попросить API рассказать о себе всё: какие у него есть запросы, какие типы данных он возвращает. По сути, это просьба отдать чертежи всей системы. Оставлять интроспекцию включенной на продакшене — все равно что оставлять ключи от сейфа под ковриком. И она была включена.

Я скачал всю схему и начал искать в ней что-то подозрительное. И нашел мутацию (в GraphQL "мутация" — это запрос, который что-то меняет, в отличие от "запроса", который только читает данные):

mutation _debug_generateUserSession($userId: String!) { ... }

Префикс _debug. Это отладочный код, который разработчики используют для тестов. В данном случае, он генерировал сессию для любого пользователя, зная только его ID. Они забыли его убрать перед выкаткой в продакшн.

Пазл сложился. У меня есть user_id системного бота. У меня есть дырявый GraphQL-эндпоинт с отладочной мутацией.

Я собрал запрос: mutation { _debug_generateUserSession(userId: "system-internal-bot") { sessionToken } }.
И получил в ответ свежий, валидный JWT-токен для этого бота.

Это был конец игры. Я подставил этот токен в заголовок Authorization и получил права системного сервиса, который мог делать в приложении абсолютно все.

Вся атака — это цепочка уязвимостей (vulnerability chain). Ни одна из них поодиночке (кроме IDOR) не была бы критической. Просроченный токен? Бесполезен. Открытый GraphQL? Ну, можно посмотреть схему. Но вместе они сложились в путь, который привел от рядового пользователя до полного администратора системы.

Отчет я писал с особым удовольствием. Это была не просто демонстрация бага. Это был урок о том, как одна маленькая ошибка, допущенная одним разработчиком, может свести на нет усилия всей команды безопасности.
 
Top