//Кейс

Кейс: как разработчик B2B-систем нашёл и закрыл критические уязвимости в CMS и CRM

За две недели AI-хакер проверил внешний периметр, вошёл в учётную запись администратора CMS и нашёл в открытом доступе ключ к CRM. Ретест подтвердил, что критические уязвимости закрыты.

Команда Sentra20 сентября 20265 мин чтения
Серверный шкаф с открытой дверцей, монохромная фотография.

Разработчик B2B-систем отвечает не только за свои данные. Успешная атака на его продукт задевает бизнес-процессы клиентов — а значит, и доверие к самому разработчику.

Поверхность атаки меняется с каждым релизом, новой версией API и подключённым сервисом. Поэтому защищённость нужно проверять непрерывно: замечать новые риски, выяснять, можно ли ими воспользоваться на деле, и убеждаться, что исправления работают.

Крупный разработчик B2B-систем решил посмотреть, как с этим справится AI-хакер Sentra, и запустил двухнедельный пилот: внешний периметр и веб-приложения.

Масштаб пилота

Количество активов и находок изменено для анонимизации. Технические сценарии и результаты проверок сохранены.

Периметр и границы проверки согласовали до старта. За две недели AI-хакер проанализировал 868 активов.

Обзор активов

868Всего активов

Проверка шла методом «чёрного ящика» — глазами внешнего атакующего. Всего AI-хакер зафиксировал 423 находки: 22 уязвимости разного уровня и 401 информационную находку.

Уязвимости

423Всего

Все 10 уязвимостей критического, высокого и среднего уровней эксперт Sentra дополнительно перепроверил вручную. Самые значимые сценарии довели до практического подтверждения: выяснили, какой доступ они дают и к чему это может привести.

Подтверждённые цепочки атак

Названия маршрутов API, параметров и технических полей в примерах изменены. Последовательность проверок и границы подтверждённых результатов сохранены.

1. Вход в учётную запись администратора Strapi

AI-хакер Sentra
Цель

Проверить административный доступ через публичный Strapi API.

Подтверждён
3 из 3 подтверждены
  1. Content APIcms.example
  2. Оракул фильтровeditor_link
  3. Reset-кодrecovery_code
  4. Администратор CMSВход подтверждён
  1. АнализВ публичном API есть /example-api/cms/records. Начну с baseline и фильтра по editor_link.contact_address: если скрытое поле участвует в запросе, количество записей должно зависеть от условия, даже когда само поле не возвращается.

    В публичном API есть /example-api/cms/records. Начну с baseline и фильтра по editor_link.contact_address: если скрытое поле участвует в запросе, количество записей должно зависеть от условия, даже когда само поле не возвращается.

  2. Повтор HTTP-запросаContent API отвечает без сессии Завершено
    dast_replayОбезличенный фрагмент
    Вход
    GET /example-api/cms/records
    Без Cookie и Authorization
    Результат
    HTTP 200
    match_count: N (N > 0)
  3. Проверка входных данныхФильтры дают разное число совпадений Завершено
    dast_fuzzОбезличенный фрагмент
    Вход
    filters[editor_link][contact_address][$contains] = @
    Контроль: то же поле, значение <контроль без совпадений>
    Результат
    contains "@"        → HTTP 200 · match_count: N (N > 0)
    contains "<контроль без совпадений>" → HTTP 200 · match_count: 0
  4. Повтор HTTP-запросаКонтроль с несуществующим полем Завершено
    dast_replayОбезличенный фрагмент
    Вход
    Тот же Content API и relation editor_link
    Вместо contact_address — несуществующее поле
    Результат
    HTTP 500
    Неизвестное поле отличается от валидного фильтра без совпадений
  5. Обращение к памятиСлужебные поля и особенности LIKE Контекст найден
    Поисковый запрос
    Strapi Content API: editor_link, recovery_code, startsWith, точная проверка email
    Источник · Заметки исследования · фильтры Strapi

    Вложенный фильтр может участвовать в запросе к БД, хотя поле не возвращается в JSON. В LIKE символ _ — wildcard. Восстановленный email нужно проверить через $eq; принятие фильтра по токену само по себе не означает, что токен уже существует.

  6. АнализПолучился дифференциал совпадения есть → совпадений нет, неизвестное поле даёт 500. Сервер разбирает вложенное поле пользователя. Проверю, принимает ли он recovery_code: значение можно восстанавливать по ответам, даже если оно не выводится в JSON.

    Получился дифференциал совпадения есть → совпадений нет, неизвестное поле даёт 500. Сервер разбирает вложенное поле пользователя. Проверю, принимает ли он recovery_code: значение можно восстанавливать по ответам, даже если оно не выводится в JSON.

  7. Повтор HTTP-запросаФильтр по recovery_code принимается Завершено
    dast_replayОбезличенный фрагмент
    Вход
    filters[editor_link][recovery_code][$startsWith]
    Значение: <проверяемый префикс>
    Результат
    HTTP 200
    Фильтр по служебному полю принят
  8. Повтор HTTP-запросаДо восстановления reset-кода нет Завершено
    dast_replayОбезличенный фрагмент
    Вход
    filters[editor_link][recovery_code][$notNull] = true
    До запроса account/recovery
    Результат
    HTTP 200
    match_count: 0
  9. АнализВ извлечении email есть ловушка: _ в LIKE совпадает с любым символом. Проверю восстановленный адрес через $eq, иначе можно получить похожее, но неверное значение.

    В извлечении email есть ловушка: _ в LIKE совпадает с любым символом. Проверю восстановленный адрес через $eq, иначе можно получить похожее, но неверное значение.

  10. Проверка входных данныхEmail восстановлен по суффиксам Завершено
    dast_fuzzОбезличенный фрагмент
    Вход
    Проверка editor_link.contact_address через $endsWith
    Уточнение адреса после неоднозначного LIKE-поиска
    Результат
    Кандидат email: [скрыт]
    Совпадение по суффиксу получено
  11. Повтор HTTP-запросаПолный email совпадает через $eq Завершено
    dast_replayОбезличенный фрагмент
    Вход
    filters[editor_link][contact_address][$eq] = <восстановленный адрес>
    Результат
    HTTP 200
    match_count: N (N > 0)
    Точное совпадение подтверждено
  12. РешениеСогласовать запрос восстановления

    Запрос отправит письмо, смена пароля изменит учётную запись. Эти действия в кейсе согласованы с заказчиком.

  13. Повтор HTTP-запросаПосле восстановления появился reset-код Завершено
    dast_replayОбезличенный фрагмент
    Вход
    POST /example-api/cms/account/recovery
    {"contact_address":"[скрыто]"}
    Повтор recovery_code[$notNull]
    Результат
    HTTP 204
    До восстановления: match_count 0
    После восстановления: match_count N > 0
  14. АнализКод появился и доступен через тот же оракул. Доведу проверку до согласованной смены пароля и входа. Дальнейшие уязвимости CMS — отдельная область, в этот результат они не входят.

    Код появился и доступен через тот же оракул. Доведу проверку до согласованной смены пароля и входа. Дальнейшие уязвимости CMS — отдельная область, в этот результат они не входят.

  15. Проверка входных данныхКод восстановления получен через оракул Завершено
    dast_fuzzОбезличенный фрагмент
    Вход
    Ограниченная проверка префиксов recovery_code
    Полученное значение не публикуется
    Результат
    recovery_code: [скрыт]
    Код передан для согласованной проверки
  16. НаходкаДоступ к учётной записи администратора

    По результатам кейса выполнены согласованная смена пароля и административный вход. После исправления критических уязвимостей проведён ретест.

Цепочка атаки
Подтверждён

Захват администратора CMS через Content API

Посторонний получает административный доступ к CMS

Бизнес-риск
80/100
Вероятность реализации
95%

Маршрут атаки

Исходный доступ
Внешний посетитель без учётной записи. Смена пароля согласована с заказчиком.
01Фильтрация по скрытым полям администратораContent API Оракул фильтровПодтверждён
Предпосылка
Content API доступен без сессии и принимает вложенные фильтры.
Результат
Количество записей зависит от условия по editor_link; служебное поле recovery_code принимается.
Content API отвечает без сессии
Обезличенный фрагмент материалов исследования
Запрос / действие
GET /example-api/cms/records
Без Cookie и Authorization
Наблюдение
HTTP 200
match_count: N (N > 0)
Открыть в ходе работы
Фильтры дают разное число совпадений
Обезличенный фрагмент материалов исследования
Запрос / действие
filters[editor_link][contact_address][$contains] = @
Контроль: то же поле, значение <контроль без совпадений>
Наблюдение
contains "@"        → HTTP 200 · match_count: N (N > 0)
contains "<контроль без совпадений>" → HTTP 200 · match_count: 0
Открыть в ходе работы
Контроль с несуществующим полем
Обезличенный фрагмент материалов исследования
Запрос / действие
Тот же Content API и relation editor_link
Вместо contact_address — несуществующее поле
Наблюдение
HTTP 500
Неизвестное поле отличается от валидного фильтра без совпадений
Открыть в ходе работы
Фильтр по recovery_code принимается
Обезличенный фрагмент материалов исследования
Запрос / действие
filters[editor_link][recovery_code][$startsWith]
Значение: <проверяемый префикс>
Наблюдение
HTTP 200
Фильтр по служебному полю принят
Открыть в ходе работы
02Получение действующего кода восстановленияОракул фильтров Reset-кодПодтверждён
Предпосылка
Email проверен точным сравнением. Запрос восстановления согласован.
Результат
После запроса восстановления код появился; значение восстановлено через тот же оракул.
Полный email совпадает через $eq
Обезличенный фрагмент материалов исследования
Запрос / действие
filters[editor_link][contact_address][$eq] = <восстановленный адрес>
Наблюдение
HTTP 200
match_count: N (N > 0)
Точное совпадение подтверждено
Открыть в ходе работы
До восстановления reset-кода нет
Обезличенный фрагмент материалов исследования
Запрос / действие
filters[editor_link][recovery_code][$notNull] = true
До запроса account/recovery
Наблюдение
HTTP 200
match_count: 0
Открыть в ходе работы
После восстановления появился reset-код
Обезличенный фрагмент материалов исследования
Запрос / действие
POST /example-api/cms/account/recovery
{"contact_address":"[скрыто]"}
Повтор recovery_code[$notNull]
Наблюдение
HTTP 204
До восстановления: match_count 0
После восстановления: match_count N > 0
Открыть в ходе работы
Код восстановления получен через оракул
Обезличенный фрагмент материалов исследования
Запрос / действие
Ограниченная проверка префиксов recovery_code
Полученное значение не публикуется
Наблюдение
recovery_code: [скрыт]
Код передан для согласованной проверки
Открыть в ходе работы
03Смена пароля и административный входReset-код Администратор CMSПодтверждён
Предпосылка
Получен действующий код; заказчик разрешил смену пароля.
Результат
Административный вход подтверждён в кейсе. После исправления критических уязвимостей проведён ретест.
Доступ к учётной записи администратора
Обезличенный фрагмент материалов исследования

По результатам кейса выполнены согласованная смена пароля и административный вход. После исправления критических уязвимостей проведён ретест.

Открыть в ходе работы

Посторонний получает административный доступ к CMS

Административный вход подтверждён по согласованию. Ретест критических уязвимостей после исправлений прошёл.

Условие реализации

Подтверждён административный вход после согласованного восстановления пароля.

Меры защиты

Закрыть фильтрацию служебных полей

Запретить публичному Content API фильтры по административным отношениям и обновить CMS.

Отозвать раскрытые коды и проверить доступ

Инвалидировать коды восстановления и провести ретест административного доступа.

Цепочка начиналась с уязвимости CVE-2026-27886. Публичный API принимал фильтры по скрытым полям и по-разному отвечал на совпадение и его отсутствие. По этим различиям AI-хакер восстановил адрес администратора, а затем и код восстановления пароля.

С согласия заказчика пароль сменили и вошли в учётную запись. Захват администратора подтвердился на практике.

2. Административный webhook Bitrix24 в открытом доступе

AI-хакер Sentra
Цель

Проверить полномочия webhook из публичной клиентской конфигурации.

Подтверждён
3 из 3 подтверждены
  1. Публичная страницаportal.example
  2. Webhookclient_settings
  3. АдминистраторПрофиль интеграции
  4. CRM APIРазрешённые методы
  1. АнализВ app-bootstrap есть client_settings.crm_hook_url. URL похож на входящий webhook: /example-api/integration/<actor>/<key>/. Начну с actor-info без сессии: нужен действующий ключ и его пользователь.

    В app-bootstrap есть client_settings.crm_hook_url. URL похож на входящий webhook: /example-api/integration/<actor>/<key>/. Начну с actor-info без сессии: нужен действующий ключ и его пользователь.

  2. Просмотр HTTP-трафикаWebhook находится в HTML публичной страницы Завершено
    dast_get_trafficОбезличенный фрагмент
    Вход
    GET /
    script#app-bootstrap → client_settings
    Результат
    crm_hook_url:
    https://crm.example/example-api/integration/<actor>/[key]/
  3. Обращение к памятиКак различить пользователя ключа и его полномочия Контекст найден
    Поисковый запрос
    Входящий webhook Bitrix24: actor-info, permission-list, operation-list; проверка без изменения CRM
    Источник · Материалы проверки · служебные методы интеграции

    actor-info определяет пользователя webhook, permission-list — разрешённую область, operation-list — доступные методы. Эти ответы нужно сопоставить: признак is_admin не заменяет проверку области CRM и не доказывает выполнение каждой операции.

  4. АнализСекрет уже находится в пути /example-api/integration/<actor>/<key>/; браузерная сессия для этого способа доступа не нужна. В повторе уберу Cookie и Authorization, чтобы административный профиль нельзя было объяснить моей собственной сессией.

    Секрет уже находится в пути /example-api/integration/<actor>/<key>/; браузерная сессия для этого способа доступа не нужна. В повторе уберу Cookie и Authorization, чтобы административный профиль нельзя было объяснить моей собственной сессией.

  5. Повтор HTTP-запросаactor-info: is_admin=true Завершено
    dast_replayОбезличенный фрагмент
    Вход
    GET /example-api/integration/<actor>/[key]/actor-info
    Без Cookie и Authorization
    Результат
    HTTP 200
    {"actor_context":{"is_admin":true}}
  6. АнализWebhook живой, профиль вернул is_admin: true. Теперь проверю permission-list: признак администратора ещё не говорит, какие области разрешены этому ключу.

    Webhook живой, профиль вернул is_admin: true. Теперь проверю permission-list: признак администратора ещё не говорит, какие области разрешены этому ключу.

  7. Повтор HTTP-запросаpermission-list: разрешена CRM Завершено
    dast_replayОбезличенный фрагмент
    Вход
    GET /example-api/integration/<actor>/[key]/permission-list
    Результат
    HTTP 200
    {"access_areas":["crm"]}
  8. Повтор HTTP-запросаoperation-list возвращает разрешённые методы Завершено
    dast_replayОбезличенный фрагмент
    Вход
    GET /example-api/integration/<actor>/[key]/operation-list
    Результат
    HTTP 200
    Получен список методов для этого webhook
  9. Выполнение командыВ списке есть операции с CRM и групповые запросы Завершено
    computer_executeОбезличенный фрагмент
    Вход
    Локальный разбор сохранённого operation-list
    Отбор операций с объектами CRM и групповых запросов; без вызова самих операций
    Результат
    Операции с обращениями, сделками, контактами,
    организациями и активностями CRM
    Групповые запросы
  10. АнализСекрет в HTML, actor-info подтверждает администратора, permission-list и operation-list — CRM. Этого достаточно для отчёта. Создавать или удалять клиентские объекты для проверки полномочий не нужно.

    Секрет в HTML, actor-info подтверждает администратора, permission-list и operation-list — CRM. Этого достаточно для отчёта. Создавать или удалять клиентские объекты для проверки полномочий не нужно.

  11. РешениеОстановиться на служебных запросах

    Выполнены только actor-info, permission-list и operation-list. Создание, изменение и удаление CRM-данных не выполнялись.

  12. НаходкаПубличный административный webhook Bitrix24

    Внешний посетитель получает действующий секрет интеграции с областью CRM. Ключ нужно отозвать и убрать из клиентской конфигурации.

Цепочка атаки
Подтверждён

Публичный административный webhook Bitrix24

Посторонний получает ключ интеграции с областью CRM

Бизнес-риск
80/100
Вероятность реализации
95%

Маршрут атаки

Исходный доступ
Внешний посетитель публичной страницы без сессии.
01Получение секрета из HTMLПубличная страница WebhookПодтверждён
Предпосылка
client_settings включён в app-bootstrap публичной страницы.
Результат
В конфигурации найден URL webhook с секретом интеграции.
Webhook находится в HTML публичной страницы
Обезличенный фрагмент материалов исследования
Запрос / действие
GET /
script#app-bootstrap → client_settings
Наблюдение
crm_hook_url:
https://crm.example/example-api/integration/<actor>/[key]/
Открыть в ходе работы
02Проверка действующего административного ключаWebhook АдминистраторПодтверждён
Предпосылка
Webhook вызывается без пользовательской сессии.
Результат
actor-info отвечает 200 и возвращает is_admin=true.
actor-info: is_admin=true
Обезличенный фрагмент материалов исследования
Запрос / действие
GET /example-api/integration/<actor>/[key]/actor-info
Без Cookie и Authorization
Наблюдение
HTTP 200
{"actor_context":{"is_admin":true}}
Открыть в ходе работы
03Доступность методов CRMАдминистратор CRM APIПодтверждён
Предпосылка
permission-list разрешает CRM; operation-list перечисляет операции с её объектами.
Результат
Полномочия подтверждены служебными методами. Создание, изменение и удаление объектов не проверялись.
permission-list: разрешена CRM
Обезличенный фрагмент материалов исследования
Запрос / действие
GET /example-api/integration/<actor>/[key]/permission-list
Наблюдение
HTTP 200
{"access_areas":["crm"]}
Открыть в ходе работы
operation-list возвращает разрешённые методы
Обезличенный фрагмент материалов исследования
Запрос / действие
GET /example-api/integration/<actor>/[key]/operation-list
Наблюдение
HTTP 200
Получен список методов для этого webhook
Открыть в ходе работы
В списке есть операции с CRM и групповые запросы
Обезличенный фрагмент материалов исследования
Запрос / действие
Локальный разбор сохранённого operation-list
Отбор операций с объектами CRM и групповых запросов; без вызова самих операций
Наблюдение
Операции с обращениями, сделками, контактами,
организациями и активностями CRM
Групповые запросы
Открыть в ходе работы

Посторонний получает ключ интеграции с областью CRM

is_admin=true, access_areas=crm. Проверены служебные методы; объекты CRM не изменялись.

Условие реализации

Webhook действующий: actor-info возвращает is_admin=true, permission-list включает CRM. Изменение CRM-объектов не выполнялось.

Меры защиты

Отозвать опубликованный webhook

Заменить секрет интеграции и проверить обращения к нему.

Перенести секрет на сервер

Убрать webhook из HTML и клиентской конфигурации; ограничить полномочия интеграции.

В клиентской конфигурации, доступной любому посетителю, нашёлся действующий webhook Bitrix24. Несколько служебных запросов показали, что ключ рабочий, у него права администратора и доступ к CRM и её методам API.

С таким ключом объекты CRM можно читать и изменять — то есть данные могли как утечь, так и быть подменены. Сами данные CRM при проверке не трогали: хватило служебных запросов.

Чем закончился пилот

Заказчик получил подробный технический отчёт:

  • найденные уязвимости с оценкой критичности;
  • доказательства и шаги воспроизведения для подтверждённых сценариев;
  • разбор цепочек атак и их возможных последствий;
  • рекомендации по устранению, разделённые на первоочередные и дополнительные.

Sentra автоматически приоритизировала уязвимости с учётом подтверждённых сценариев эксплуатации и риска для бизнеса. Заказчик закрыл критические уязвимости, и ретест подтвердил, что исправления работают.

Теперь компания готовится встроить AI-хакер Sentra в непрерывный контроль защищённости.

Пилот

Проверьте работу AI-хакера на своей инфраструктуре — бесплатно

Запросить пилот
Запросить пилот contact@sentra-tech.ru