
Воспроизводимая лаборатория Docker + PoC: повышение привилегий Keycloak manage-users -> realm-admin через составные роли (вариант CVE-2026-94000). Проверено на 26.8.1.
Воспроизводимая лаборатория Docker и PoC для пути повышения привилегий в admin REST API Keycloak: делегированный администратор, обладающий только manage-users, может повысить себя (или любого пользователя или группу) до полного realm-admin, сопоставив составную роль, имя которой обычное, но дочерние элементы которой включают realm-management/realm-admin.
Подтверждено сквозным тестированием на официальном образе quay.io/keycloak/keycloak:26.8.1.
⚠️ Только авторизованные исследования. Всё выполняется на одноразовом Keycloak на
localhost. Не направляйте PoC на системы, которые вам явно не разрешено тестировать.
Keycloak 26.8.1 ужесточил присоединение к группе (CVE-2026-94000): администратор с manage-users
больше не может добавить пользователя в группу, которая транзитивно предоставляет административные роли realm-management.
GroupUtils.checkAdminGroupRoles обходит роли группы рекурсивно и вызывает
requireMapRole для каждой вложенной административной роли:
private static void requireMapRoleRecursive(RoleModel role, AdminPermissionEvaluator auth, Set<String> visited) {
if (!visited.add(role.getId())) return;
if (AdminRoles.isAdminRole(role)) auth.roles().requireMapRole(role); // checks nested admin roles
if (role.isComposite()) role.getCompositesStream().forEach(child -> requireMapRoleRecursive(child, auth, visited));
}
Тот же рекурсивный обход так и не был добавлен в эндпоинты сопоставления ролей. Когда роль сопоставляется напрямую с пользователем или группой, сервер проверяет только переданную роль:
// RoleMapperResource.addRealmRoleMappings — mounted on BOTH users and groups
for (RoleRepresentation role : roles) {
RoleModel roleModel = realm.getRole(role.getName());
auth.roles().requireMapRole(roleModel); // posted role only — children never inspected
roleMapper.grantRole(roleModel);
}
И сам механизм проверки привилегий опирается на имя роли, а не на её содержимое. В
RolePermissionsV2.canMapRole роль считается привилегированной только тогда, когда её контейнер —
realm-management/master и её имя входит в AdminRoles.ALL_ROLES:
public boolean canMapRole(RoleModel role) {
if (isRealmAdminRole(role)) { ... } // isRealmAdminRole = container + ALL_ROLES.contains(name)
if (root.hasOneAdminRole(AdminRoles.MANAGE_USERS)) // manage-users may map ANY non-admin-named role
return true;
...
}
Итак, композитная роль с именем sneaky-composite, которая оборачивает realm-management/realm-admin, проходит проверку — её имени нет в ALL_ROLES, а её дочерние роли никогда не обходятся. Пробел охватывает три эндпоинта:
RoleMapperResource.addRealmRoleMappings — пользователи и группыClientRoleMappingsResource.addClientRoleMapping — роли клиента на любом клиентеИтоговый эффект: привилегированная полезная нагрузка (композитная роль, оборачивающая realm-admin) должна всего лишь существовать — созданная настоящим администратором, импортом realm или более ранним процессом. Затем manage-users может её назначить, что модель угроз CVE-2026-94000 никогда не предполагала.
| # | Вариант | Почему работает |
|---|---|---|
| A | Самоназначение sneaky-composite → realm-admin | проверка по имени пропускает композитную роль |
| B | Назначение вложенной outer-sneaky (→ sneaky-composite → realm-admin) пользователю | глубина не проверяется |
| C | Назначение роли клиента на неадминистративном клиенте (spa/hidden-ra → realm-admin) | ClientRoleMappingsResource использует ту же проверку |
| D | Назначение композитной роли на группу, в которой уже есть участники | другой API, не join; существующие участники повышаются в правах без какого-либо join |
Требования: Docker + Docker Compose v2, а также curl + jq на хосте.
cp .env.example .env # optional: pin KC_VERSION (default 26.8.1)
make up # start Keycloak + auto-bootstrap the vulnerable realm `app`
make logs # wait for "Lab realm 'app' is ready"
make check # non-destructive detection (maps, then immediately unmaps)
make exploit # Attack A: self-escalate limited-admin -> realm-admin
make all # full chain: check + A + B + C + D
make reset # undo every escalation (keep the lab running)
make clean # tear everything down
Одноразовый контейнер bootstrap создаёт сценарий через admin REST API, затем завершается:
| Объект | Назначение |
|---|---|
limited-admin / limitedpass | Атакующий — имеет только manage-users |
alice / bob / carol / dave | Пользователи-жертвы |
sneaky-composite (роль realm) | Композитная → realm-management/realm-admin |
outer-sneaky (роль realm) | Композитная → sneaky-composite (на один переход больше) |
клиент spa + роль spa/hidden-ra | Роль неадминистративного клиента → realm-admin |
клиент confidential (confsecret) | Цель воздействия — чтение секрета |
группа helpdesk (alice является участником) | Цель атаки через сопоставление групп |
PoC определяет идентификаторы ролей так, как это реально может сделать вызывающий с правами manage-users — через
эндпоинты .../role-mappings/.../available — поскольку этой учётной записи запрещён доступ к
GET /roles/{name} и GET /clients (403), но не к спискам доступных ролей. Это сохраняет
честное воспроизведение с точки зрения атакующего с низкими привилегиями.
Направьте лабораторию на пропатченную сборку; исправленный сервер должен возвращать 403 на каждом шаге сопоставления, при этом по-прежнему разрешая обычное сопоставление ролей неадминистративного пользователя:
KC_VERSION=<patched-tag> docker compose up -d --force-recreate
make check # expect: NOT vulnerable
На quay.io/keycloak/keycloak:26.8.1 (полная расшифровка в docs/demo.txt,
подробности в VALIDATION.md):
| Шаг | Действие | Результат |
|---|---|---|
| Базовая линия | limited-admin POST /roles | 403 (действительно низкие привилегии) |
| check | сопоставить sneaky-composite с самим собой (затем снять сопоставление) | 204 → УЯЗВИМО |
| A | сопоставить sneaky-composite с самим собой → realm-admin → создать роль / выгрузить email пользователей | 204 / 201 / 200 |
| B | вложенный outer-sneaky на bob → bob создаёт роль | 204 / 201 |
| C | spa/hidden-ra на carol → carol создаёт роль | 204 / 201 |
| D | сопоставить обёртку с helpdesk → существующий участник alice создаёт роль | 204 / 201 |
| Контроль D | manage-users добавляет нового участника в helpdesk | 403 (исправление CVE-2026-94000 не нарушено) |
Контроль D доказывает асимметрию: добавление в группу заблокировано, но сопоставление того же композита с группой (или с пользователем/клиентом) не проверяется и возвращает 204.