
再現可能なDockerラボ + PoC: 複合ロールを介したKeycloak manage-users -> realm-admin 権限昇格(CVE-2026-94000 の変種)。26.8.1 で検証済み。
Keycloakの管理REST APIにおける権限昇格経路の再現可能なDockerラボおよびPoC:manage-usersのみを保持する委任管理者が、名前は一般的だが子にrealm-management/realm-adminを含む複合ロールをマッピングすることで、自身(または任意のユーザーやグループ)を完全な**realm-admin**に昇格させることができます。
公式イメージquay.io/keycloak/keycloak:26.8.1に対してエンドツーエンドで確認済みです。
⚠️ 許可された研究のみ。 すべては
localhost上の使い捨てKeycloakに対して実行されます。明示的にテストを許可されていないシステムに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;
...
}
そのため、realm-management/realm-admin をラップする sneaky-composite という名前のコンポジットはゲートを通過します。その名前は ALL_ROLES に含まれておらず、その子要素が走査されることもありません。このギャップは3つのエンドポイントにまたがります:
RoleMapperResource.addRealmRoleMappings — ユーザーとグループClientRoleMappingsResource.addClientRoleMapping — 任意のクライアント上のクライアントロール正味の影響: 特権ペイロード(realm-admin をラップするコンポジット)は、存在するだけで十分です — 実際の管理者、レルムインポート、または以前のプロセスによって作成されたものです。その後 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 | コンポジットをすでにメンバーを持つグループにマッピング | join とは異なる API。既存のメンバーは 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 コンテナは、管理 REST API 経由でシナリオを構築してから終了します:
| オブジェクト | 目的 |
|---|---|
limited-admin / limitedpass | 攻撃者 — manage-users のみを保持 |
alice / bob / carol / dave | 被害ユーザー |
sneaky-composite(レルムロール) | コンポジット → realm-management/realm-admin |
outer-sneaky(レルムロール) | コンポジット → sneaky-composite(追加で1ホップ) |
spa クライアント + spa/hidden-ra ロール | 非管理者クライアントロール → realm-admin |
confidential クライアント(confsecret) | シークレット読み取りの影響対象 |
helpdesk グループ(alice がメンバー) | グループマッピング攻撃の対象 |
PoC は、manage-users 呼び出し元が実際に可能な方法でロール ID を解決します — つまり
.../role-mappings/.../available エンドポイント経由です — なぜならそのアカウントは
GET /roles/{name} と GET /clients を拒否されます(403)が、available リストは拒否されません。これにより、
スクリプトは正直な低権限攻撃者視点の再現を維持します。
ラボをパッチ適用済みのビルドに向けてください。修正されたサーバーは、通常の非管理者ロールマッピングを 引き続き許可しつつ、すべてのマップ手順で 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 → ロール作成 / ユーザーのメールアドレスをダンプ | 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 を返す。
RolePermissionsV2.canMapRole / RolePermissions.checkAdminRoles において、
AdminRoles.isAdminRoleOrComposite(role) が true の場合、呼び出し元がすべての
ネストされた管理者ロールをマップできるのでなければロールを拒否する —
GroupUtils.requireMapRoleRecursive をミラーする。RoleMapperResource.addRealmRoleMappings(ユーザーとグループ)および
ClientRoleMappingsResource.addClientRoleMapping に対して実行する。manage-users が sneaky-composite、ネストされたラッパー、realm-admin を
コンポジットする非管理者クライアントロール、またはメンバーを持つグループへのラッパーを
マップできないことを検証するテストカバレッジを追加する。既存のグループ参加テストだけでは
不十分である。完全な技術的解説:report/report.md。
docker-compose.yml Keycloak + one-shot bootstrap container
bootstrap/setup-realm.sh Builds the vulnerable realm via the admin REST API
poc/kc_composite_maprole_poc.sh Detection + exploitation + cleanup PoC (curl/jq)
report/report.md Full technical write-up
VALIDATION.md / docs/demo.txt Verified run evidence
MIT — LICENSE を参照してください。