
पुनरुत्पादनीय Docker लैब + PoC: Keycloak manage-users -> कंपोज़िट भूमिकाओं के माध्यम से realm-admin एस्केलेशन (CVE-2026-94000 वेरिएंट)। 26.8.1 पर सत्यापित।
Keycloak के admin REST API में privilege-escalation पथ के लिए एक पुनरुत्पादनीय Docker लैब और PoC: एक delegated admin जिसके पास केवल manage-users है, वह एक composite role को मैप करके स्वयं को (या किसी भी उपयोगकर्ता या समूह को) पूर्ण realm-admin तक escalate कर सकता है, जिसका नाम सामान्य है लेकिन जिसके children में realm-management/realm-admin शामिल है।
आधिकारिक इमेज quay.io/keycloak/keycloak:26.8.1 के विरुद्ध end-to-end पुष्टि की गई।
⚠️ केवल अधिकृत अनुसंधान। सब कुछ
localhostपर एक अस्थायी Keycloak के विरुद्ध चलता है। PoC को उन सिस्टमों पर न लक्षित करें जिनका परीक्षण करने के लिए आपको स्पष्ट रूप से अधिकृत नहीं किया गया है।
Keycloak 26.8.1 ने group join को कठोर बनाया (CVE-2026-94000): एक manage-users admin अब किसी उपयोगकर्ता को ऐसे समूह में नहीं जोड़ सकता जो पारगमन रूप से realm-management admin roles प्रदान करता हो। GroupUtils.checkAdminGroupRoles समूह की roles को पुनरावर्ती रूप से टहलता है और प्रत्येक nested admin role पर 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);
}
और privilege gate स्वयं role के नाम पर निर्भर करता है, उसकी सामग्री पर नहीं। RolePermissionsV2.canMapRole में, कोई role केवल तभी privileged माना जाता है जब उसका container 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 में नहीं है, और इसके चिल्ड्रन को कभी वॉक नहीं किया जाता। यह अंतराल
तीन एंडपॉइंट्स में फैला है:
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 | कंपोज़िट को उस समूह पर मैप करें जिसमें पहले से सदस्य हैं | जॉइन से भिन्न API; मौजूदा सदस्य बिना किसी जॉइन के एस्केलेट हो जाते हैं |
आवश्यकताएँ: 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 भूमिका) | कंपोज़िट → realm-management/realm-admin |
outer-sneaky (realm भूमिका) | कंपोज़िट → sneaky-composite (एक अतिरिक्त हॉप) |
spa क्लाइंट + spa/hidden-ra भूमिका | गैर-एडमिन क्लाइंट भूमिका → realm-admin |
confidential क्लाइंट (confsecret) | सीक्रेट-रीड प्रभाव लक्ष्य |
helpdesk समूह (alice सदस्य है) | समूह-मैपिंग हमला लक्ष्य |
PoC भूमिका IDs को उस तरह से हल करता है जैसे एक 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 (वास्तव में कम-विशेषाधिकार) |
| जाँच | 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) सत्य हो, जब तक कि कॉलर प्रत्येक नेस्टेड एडमिन भूमिका को मैप न कर सके — GroupUtils.requireMapRoleRecursive का अनुसरण करें।RoleMapperResource.addRealmRoleMappings (उपयोगकर्ता और समूह) और
ClientRoleMappingsResource.addClientRoleMapping के लिए चलाएँ।manage-users sneaky-composite, एक नेस्टेड रैपर,
एक गैर-एडमिन क्लाइंट भूमिका जो realm-admin को कंपोज़िट करती है, या सदस्यों वाले समूह पर रैपर को मैप नहीं कर सकता। केवल मौजूदा समूह-जॉइन परीक्षण पर्याप्त नहीं है।पूर्ण तकनीकी रिपोर्ट: report/report.md।