Skip to content
KitploitKITPLOIT
أدواتالمدونة
إرسال
أدواتالمدونة
إرسال

أدوات الاختراق واختبار الاختراق والأمن السيبراني لترسانتك الأمنية!

Kitploit هو دليل لأدوات الاختراق والأمن السيبراني واختبار الاختراق. اكتشف آخر تحديثات المشاريع للعثور على الثغرات وتحليل الأنظمة وأتمتة الاختبارات وتعزيز أمنك.

··الخلاصات·اتصال·الخصوصية·© 2026 Kitploit

دليل الأدوات

الفئات

عرض جميع الفئات
Loading categories
shiro — CVE-2026-49268 — تحليل ومعالجة ثغرة تجاوز المصادقة عبر حقن LDAP | Kitploit
أدوات/GitHubGitHub/sassoftware/shiro
تحليل الشفرة الثابت (SAST)تحليل الثغرات الأمنيةتحليل الكودأمن الويبالمصادقةالتعلم والتعليم
GitHubsassoftware/shiro

shiro

CVE-2026-49268 — تحليل ومعالجة ثغرة تجاوز المصادقة عبر حقن LDAP

عرض المستودع
1منذ 6 أياملم تتم المراجعة بعد

الأكثر شعبية

عرض الكل →

اكتشف الأدوات الأكثر استخدامًا من قبل مجتمعنا.

استكشف جميع الأدوات

تصفح مجموعتنا من الأدوات

عرض جميع الأدوات →
مشاركة

CVE-2026-49268 — تحليل ومعالجة ثغرة تجاوز المصادقة عبر حقن LDAP

الفرع1.13-CVE-2026-49268
المؤلفJinwoo Hwang (https://JinwooHwang.com)

يحتوي هذا الفرع (1.13-CVE-2026-49268) على معالجة أمنية شاملة لثغرة تجاوز المصادقة عبر حقن LDAP في إصدار Apache Shiro 1.13.

وصف NVD الرسمي

يمكن لمهاجم عن بُعد حقن أحرف LDAP الخاصة في بناء Distinguished Name (DN) في فئة DefaultLdapRealm. يتم ربط إدخال اسم المستخدم الذي يوفره المستخدم مباشرةً في قالب LDAP DN دون أي تهريب لأحرف RFC 2253 الخاصة. يتيح ذلك للمهاجم التلاعب ببنية DN المستخدمة في مصادقة ربط LDAP، مما قد يؤدي إلى تجاوز المصادقة أو انتحال هوية مستخدمين آخرين. تؤثر هذه المشكلة على جميع إصدارات Apache Shiro حتى 2.2.0، و3.0.0-alpha-1 عند استخدام DefaultLdapRealm.


1. نظرة عامة على الثغرة


2. الملخص التنفيذي

يمكن لـ Apache Shiro مصادقة المستخدمين مقابل دليل LDAP. وللقيام بذلك، يحوّل اسم المستخدم المُرسَل إلى Distinguished Name — عنوان الدليل الخاص بمدخل ذلك المستخدم — عن طريق إدراج اسم المستخدم في قالب مُهيَّأ، على سبيل المثال uid={0},ou=users,dc=mycompany,dc=com.

في الإصدارات المتأثرة يتم لصق اسم المستخدم في ذلك القالب كنص خام. وهناك مجموعة من أحرف الترقيم — وأهمها الفاصلة — ليست نصاً عادياً بالنسبة لخادم الدليل؛ بل هي الصيغة التي تفصل جزءاً من العنوان عن الجزء التالي. لذلك يتوقف اسم المستخدم الذي يحتوي على تلك الأحرف عن التصرف كقيمة داخل العنوان ويبدأ في التصرف كجزء من العنوان نفسه.

النتيجة العملية هي أن الشخص الذي يسجّل الدخول يمكنه التأثير على أي مدخل دليل يحاول Shiro المصادقة مقابله، بدلاً من مجرد تقديم اسمه الخاص. فبدلاً من البحث عنه في الحاوية التي قصدها النشر، يمكن إعادة توجيه البحث إلى مكان آخر في الدليل. واعتماداً على كيفية تنظيم الدليل وما يسمح به، قد يؤدي ذلك إلى المصادقة بهوية خاطئة، أو إلى نجاح المصادقة عندما لا ينبغي لها ذلك.

ملف المخاطر. ما يرفع المخاطر: لا يُشترط أي وصول مسبق أو بيانات اعتماد — يصل الإدخال عند حدود تسجيل الدخول، التي يمكن لأي شخص يستطيع الوصول إلى التطبيق الوصول إليها؛ والفئة المتأثرة هي الطريقة القياسية والموثّقة لربط Shiro بـ LDAP، لذا فهذا ليس إعداداً غريباً. وما يخفضها: يجب أن يستخدم النشر فعلياً DefaultLdapRealm (أو JndiLdapRealm) مع قالب DN مُهيَّأ؛ أما عمليات النشر التي تصادق بوسائل أخرى، أو التي تمرر DN كاملاً أو بيانات اعتماد غير نصية مثل شهادة، فهي غير متأثرة. كما أن ما إذا كان البحث المُعاد توجيهه يُنتج مصادقة قابلة للاستخدام يعتمد أيضاً على تنظيم الدليل الهدف وقواعد الوصول الخاصة به، والتي تختلف من موقع لآخر.

تجدر الإشارة إلى نتيجة ثانية غير أمنية لأغراض تخطيط الإصدارات: المستخدمون الذين تحتوي أسماء مستخدميهم الشرعية على شرطة مائلة عكسية أو تبدأ بـ # لا يمكنهم حالياً تسجيل الدخول إطلاقاً، لأن العنوان المبني لهم ليس اسماً سليماً ويُرفض تماماً. أما علامات الترقيم الأخرى فلا تفشل تماماً — بل تغيّر بصمت المدخل الذي يشير إليه العنوان، وهي المشكلة الأمنية المذكورة أعلاه. ويحل الإصلاح نفسه كلا المشكلتين.


3. تحليل السبب الجذري

3.1 العيب

core/src/main/java/org/apache/shiro/realm/ldap/DefaultLdapRealm.java، getUserDn(String)، الأسطر 227–250 على الأساس غير المُصلَّح:```java protected String getUserDn(String principal) throws IllegalArgumentException, IllegalStateException { if (!StringUtils.hasText(principal)) { throw new IllegalArgumentException("User principal cannot be null or empty for User DN construction."); } String prefix = getUserDnPrefix(); String suffix = getUserDnSuffix(); if (prefix == null && suffix == null) { log.debug("userDnTemplate property has not been configured, indicating the submitted " + "AuthenticationToken's principal is the same as the User DN. Returning the method argument " + "as is."); return principal; }

root@kitploit:~
int prefixLength = prefix != null ? prefix.length() : 0;
int suffixLength = suffix != null ? suffix.length() : 0;
StringBuilder sb = new StringBuilder(prefixLength + principal.length() + suffixLength);
if (prefixLength > 0) {
    sb.append(prefix);
}
sb.append(principal);            // <-- inserted verbatim
if (suffixLength > 0) {
    sb.append(suffix);
}
return sb.toString();

}

root@kitploit:~
يُقسَّم القالب مرة واحدة، عند وقت التهيئة، حول الرمز `{0}`
(`setUserDnTemplate`، الأسطر 181–200)، إلى `prefix` و`suffix`. عند وقت المصادقة
يُدمَج الـ principal بينهما. **لا يُطبَّق أي ترميز في أي نقطة.** لا يوجد
أي مساعد تهريب في أي مكان ضمن `org/apache/shiro/realm/ldap/`.

### 3.2 الثابت الذي افتُرض لكن لم يُفرَض قط

تتعامل الشيفرة المحيطة مع ناتج `getUserDn` باعتباره Distinguished Name جيد التكوين —
إذ يُمرَّر مباشرة إلى `LdapContextFactory.getLdapContext(...)` ومن هناك إلى JNDI كـ
bind DN. وهذا لا يكون سليماً إلا إذا كان الـ principal المُستبدَل *قيمة سمة واحدة*.

لا يمكن لدمج النصوص فرض ذلك. تحجز RFC 2253 (و RFC 4514) الرموز
`,` `+` `"` `\` `<` `>` `;` `=`، و`#` البادئة، والمسافات البيضاء البادئة/اللاحقة كبنية
نحوية داخل الـ DN. وعندما يظهر أي منها في الـ principal فإنها تُقرأ من قِبَل المحلِّل
كبنية، لا كمحتوى. الثابت — *"يشغل الـ principal قيمة RDN واحدة بالضبط"* —
افتُرض من قِبَل كل مستهلك لاحق ولم يفرضه أيٌّ منهم.

### 3.3 تدفق التنفيذ، من نقطة الدخول إلى العيب```
  submitted credentials (username, password)
        │
        ▼
  DefaultLdapRealm.doGetAuthenticationInfo(AuthenticationToken)        [line 292]
        │
        ▼
  DefaultLdapRealm.getLdapPrincipal(AuthenticationToken)               [line 338]
        │   principal instanceof String ?
        │       ├── no  ──► return principal unchanged   ── NOT AFFECTED (e.g. X.509)
        │       └── yes ──┐
        ▼                 │
  DefaultLdapRealm.getUserDn(String)                                   [line 227]
        │   prefix == null && suffix == null ?
        │       ├── yes ──► return principal unchanged   ── NOT AFFECTED ("principal IS the DN")
        │       └── no  ──┐
        ▼                 │
  prefix + principal + suffix          ◄── DEFECT: unencoded concatenation  [line 246]
        │
        ▼
  LdapContextFactory.getLdapContext(userDn, credentials)
        │
        ▼
  JNDI bind against the directory using the constructed DN

3.4 التأثير المُثبَت

القالب uid={0},ou=users,dc=mycompany,dc=com، والهوية الأساسية jsmith,ou=admins:

الـ DN المُنشأالـ RDNs المُحلَّلة
المقصودuid=jsmith\,ou\=admins,ou=users,dc=mycompany,dc=com4
الأساس (غير المُصلَّح)uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com5

يكتسب الاسم مكوّنًا إضافيًا. لم يعد ou=users هو الحاوية المُستهدَفة — بل تم إقحام ou=admins في المنتصف. تم التحقق من ذلك عبر التحليل باستخدام javax.naming.ldap.LdapName، وهو مستقل عن أي تطبيق للتهريب.

السلوك المقيس للأساس عبر المجموعة المحجوزة (JDK 8، تحليل LdapName):

حرفان يغيّران بنية الاسم، وليس محتواه فقط: الفاصلة، والفاصلة المنقوطة — التي يقبلها RFC 1779 كفاصل RDN بديل. أما + فيُدخل قيمة سمة ثانية في نفس الـ RDN. فقط \ و# في البداية يجعلان الاسم غير قابل للتحليل.

3.5 الكود المجاور الصحيح، ولماذا — نطاق التأثير

  • userDnTemplate غير مُعيَّن. يُعيد getUserDn الهوية الأساسية دون تغيير. هذا هو الوضع الموثَّق "الهوية الأساسية المُقدَّمة هي الـ DN"؛ يوفّر المستدعي DN كاملًا ويتحمّل مسؤولية صحته. غير متأثر.
  • الهويات الأساسية غير String. لا يوجّه getLdapPrincipal سوى الهويات الأساسية من نوع String إلى getUserDn؛ أي شيء آخر (شهادات X.509، رموز مخصصة) يمر دون تغيير. غير متأثر.
  • التحقق من setUserDnTemplate (الأسطر 181–200) يرفض بشكل صحيح قالبًا فارغًا أو خاليًا أو لا يحتوي على {0}. إنه يتحقق من القالب المُقدَّم من المشغّل، الذي لم يكن أبدًا المدخل غير الموثوق — لذا فهو كود صحيح لا يعالج هذا العيب فحسب.
  • JndiLdapRealm يمتد DefaultLdapRealm ولا يتجاوز getUserDn، لذا يرث العيب. تم التأكيد بالاختبار (§6.1). ضمن النطاق، ويُصلَّح بنفس التغيير.

3.6 المسار ذو الصلة — مُقيَّم، غير متأثر

يحمل AbstractLdapRealm.searchFilter (السطر 89) استبدالًا ثانيًا لـ {0}، الافتراضي (&(objectClass=*)(userPrincipalName={0}))، يُستخدم في مسار التفويض. وهو غير متأثر.

كشف مسح شامل لكل استدعاء search(، وكل مستدعي getLdapContext(، وكل سلسلة نصية على شكل LDAP عبر المصادر الرئيسية لـ core وsupport وweb عن استخدام واحد فقط لـ searchFilter — وهو ActiveDirectoryRealm.getRoleNamesForUser، السطر 172:```java Object[] searchArguments = new Object[]{userPrincipalName}; NamingEnumeration answer = ldapContext.search(searchBase, searchFilter, searchArguments, searchCtls);

root@kitploit:~
هذا هو التحميل الزائد **المُعامَل** `DirContext.search(String, String, Object[], SearchControls)`.
يُمرَّر اسم المستخدم كـ *وسيطة* عامل تصفية، ولا يُدمج أبدًا في سلسلة
عامل التصفية. وفقًا لوثائق JDK 8 `javax.naming.directory.DirContext`، حرفيًا:

> "عند استبدال وسيطة عامل تصفية ذات قيمة نصية بمتغير، يُفسَّر عامل التصفية
> كما لو أن السلسلة أُعطيت مكان المتغير، مع أي أحرف
> ذات دلالة خاصة داخل عوامل التصفية (مثل `'*'`) بعد تهريبها وفقًا
> لقواعد RFC 2254."

يقوم مزوّد JNDI بعملية التهريب. والسجل التاريخي لذلك موجود في إعلان
الحقل نفسه.

**المصدر: `core/src/main/java/org/apache/shiro/realm/ldap/AbstractLdapRealm.java`، الأسطر 88–89**```java
    //SHIRO-115 - prevent potential code injection:
    protected String searchFilter = "(&(objectClass=*)(userPrincipalName={0}))";

المصدر: core/src/main/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealm.java، الأسطر 158–172```java protected Set getRoleNamesForUser(String username, LdapContext ldapContext) throws NamingException { Set roleNames; roleNames = new LinkedHashSet();

root@kitploit:~
    SearchControls searchCtls = new SearchControls();
    searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);

    String userPrincipalName = username;
    if (principalSuffix != null && !userPrincipalName.toLowerCase(Locale.ROOT).endsWith(principalSuffix.toLowerCase(Locale.ROOT))) {
        userPrincipalName += principalSuffix;
    }

    Object[] searchArguments = new Object[]{userPrincipalName};

    NamingEnumeration answer = ldapContext.search(searchBase, searchFilter, searchArguments, searchCtls);
root@kitploit:~
يصل اسم المستخدم إلى الدليل باسم `searchArguments[0]`، وليس كنص مُدرج في
`searchFilter`. يتم حل الرمز `{0}` في الفلتر بواسطة مزوّد JNDI، وليس بواسطة
Shiro.

يمرّر `ActiveDirectoryRealm` في السطر 108 اسم المستخدم الخام إلى
`getLdapContext(username, password)`. تصبح تلك القيمة مدخلاً واحداً لبيئة JNDI
`SECURITY_PRINCIPAL`؛ فهي لا تُستبدل في قالب ولا يُبنى منها DN، وبالتالي فهي خارج
آلية هذا العيب.

**تم التحقق منها تجريبياً مقابل خادم دليل حيّ.** لم يُؤخذ تهريب JNDI
على الثقة: بل لوحظ على الشبكة. تم تجهيز خادم LDAP داخل العملية
(`InMemoryDirectoryServer` من UnboundID) بمعترض
`InMemoryOperationInterceptor` يسجّل فلتر كل طلب بحث يستقبله،
وتم إصدار `searchFilter` الافتراضي الخاص بـ Shiro عبر نفس استدعاء JNDI المُعامَل المستخدم
بواسطة `ActiveDirectoryRealm`، مع وسيط مُصمَّم:```
filter template : (&(objectClass=*)(userPrincipalName={0}))
argument passed : *)(uid=jsmith
server received : (&(objectClass=*)(userPrincipalName=\2a\29\28uid=jsmith))

وصلت * و ) و ( إلى الخادم بصيغة \2a و \29 و \28 — مُهرَّبة وفق RFC 2254. يحتفظ الفلتر بجملتين بالضبط؛ ولم يتمكن الوسيط من إضافة جملة ثالثة.

التحكم، نفس القالب مع إدراج الوسيط كنص خام بدلاً من تمريره كوسيط فلتر:``` CONTROL, raw splice: (&(objectClass=)(userPrincipalName=)(uid=jsmith)) server received : (&(objectClass=)(userPrincipalName=)(uid=jsmith))

root@kitploit:~
يصل عنصر التحكم إلى الخادم وقد تغيّرت بنيته — أُضيف شرط ثالث واختُصر اختبار `userPrincipalName` إلى محرف بديل. أما الصيغة ذات المعاملات فلا تصل. وهذا يؤكد، بالملاحظة لا بالعقد، أن مسار `searchFilter` غير متأثر.

---

## 4. خطوات إعادة الإنتاج التفصيلية

حتمية، انطلاقًا من نسخة نظيفة. دليل العمل هو جذر المستودع في كل ما يلي.

### 4.1 المتطلبات المسبقة

| المتطلب | القيمة المستخدمة |
|---|---|
| JDK | **8** — يضبط الفرع `jdk.version` على 1.8. أُنتجت النتائج أدناه باستخدام Zulu 1.8.0_432 (arm64). وجّه `JAVA_HOME` إلى تثبيت JDK 8 قبل تشغيل أي أمر في هذا القسم: |
| البناء | Apache Maven، يتطلب الوصول إلى الشبكة (POM الأب `org.apache:apache:38`) |
| الأساس | `origin/1.13.x` |
| الإعداد | `userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com` |
| خادم الدليل | **غير مطلوب للفقرات §4.3-§4.4** — العيب في بناء DN، قبل أي استدعاء شبكي، لذا تحاكي تلك الخطوات `LdapContextFactory`. إضافة إلى ذلك، تعيد §4.5 إنتاج المسار كاملًا مقابل خادم LDAP **حقيقي** عبر HTTP. |

ضبط `JAVA_HOME` على تثبيت JDK 8:```bash
# macOS
export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)

# Linux (path varies by distribution and vendor)
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64

# Windows (cmd)
set JAVA_HOME=C:\Program Files\Zulu\zulu-8

تأكد باستخدام mvn -v، الذي يوضح إصدار JDK الذي سيستخدمه Maven. تفترض الأوامر أدناه أن هذا قد تم ضبطه بالفعل.

4.2 الحصول على النسخة الأساسية غير المُصلَّحة```bash

git clone https://github.com/apache/shiro.git cd shiro git checkout -b repro origin/1.13.x

root@kitploit:~
### 4.3 مراقبة العيب مباشرةً

هذا هو المُشغِّل الأدنى، بمعزل عن بناء Shiro. اكتب `Repro.java` في دليل مؤقت:```java
import javax.naming.ldap.LdapName;

public class Repro {
    // reproduces DefaultLdapRealm.getUserDn line-for-line
    static String getUserDn(String prefix, String principal, String suffix) {
        StringBuilder sb = new StringBuilder(prefix.length() + principal.length() + suffix.length());
        sb.append(prefix);
        sb.append(principal);
        sb.append(suffix);
        return sb.toString();
    }

    public static void main(String[] args) throws Exception {
        String prefix = "uid=";
        String suffix = ",ou=users,dc=mycompany,dc=com";

        String benign = getUserDn(prefix, "jsmith", suffix);
        System.out.println("benign : " + benign + "  -> RDNs=" + new LdapName(benign).size());

        String crafted = getUserDn(prefix, "jsmith,ou=admins", suffix);
        System.out.println("crafted: " + crafted + "  -> RDNs=" + new LdapName(crafted).size());
    }
}

شغّله:```bash javac Repro.java && java Repro

root@kitploit:~
المخرجات المرصودة:```
benign : uid=jsmith,ou=users,dc=mycompany,dc=com  -> RDNs=4
crafted: uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com  -> RDNs=5

يتغير عدد المكونات من 4 إلى 5. أصبحت القيمة المُرسلة بنية الاسم.

4.4 إعادة الإنتاج عبر واجهة Shiro الخاصة

من جذر المستودع على الأساس غير المُصلَّح، طبّق الاختبارات في §6.3 وشغّل:```bash mvn -B clean verify

root@kitploit:~
يتوقف البناء عند `Apache Shiro :: Core` مع فشل اختبارات الإثبات — راجع §6.1 للحصول على
المخرجات الكاملة المسجلة:```
DefaultLdapRealmTest   Tests run: 16, Failures: 4, Errors: 0, Skipped: 0
JndiLdapRealmTest      Tests run: 16, Failures: 4, Errors: 0, Skipped: 0

4.5 إعادة إنتاج شاملة من طرف إلى طرف عبر HTTP مقابل خادم دليل مباشر

تمت إعادة الإنتاج عبر كامل المكدس — طلب HTTP حقيقي، وحاوية servlet حقيقية، وFormAuthenticationFilter الخاص بـ Shiro، وخادم LDAP حقيقي. لا شيء وهمي في أي طبقة.

تجهيزة الدليل — حاوية مميزة متداخلة، وهي بنية شائعة في العالم الحقيقي:``` dc=mycompany,dc=com └── ou=users ├── uid=jsmith userPassword: userpass (ordinary account) └── ou=admins └── uid=jsmith userPassword: adminpass (privileged account)

root@kitploit:~
#### 4.5.1 البناء المتأثر

**الطلب**```http
POST /login HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded

username=jsmith%2Cou%3Dadmins&password=adminpass

الاستجابة```http HTTP/1.1 302 Found Location: http://127.0.0.1/;jsessionid=node0qu3v2n612vy71alsuyir2bgzz1.node0

root@kitploit:~
**Bind DN الذي استقبله الدليل**```
uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com

إعادة التوجيه 302 هي إعادة توجيه النجاح الخاصة بـ FormAuthenticationFilter: الطلب تمت مصادقته. يحمل DN خمسة مكونات حيث يحدد القالب أربعة، والمدخل الذي تم الوصول إليه هو المدخل ذو الصلاحيات تحت ou=admins.

التحكم — نفس اسم المستخدم مع كلمة مرور الحساب العادي```http POST /login HTTP/1.1 Content-Type: application/x-www-form-urlencoded

username=jsmith%2Cou%3Dadmins&password=userpass

root@kitploit:~
## التثبيت

### المتطلبات الأساسية

- Python 3.8 أو أحدث
- pip (مدير حزم Python)
- Git (اختياري، للتثبيت من المصدر)

### التثبيت عبر pip

```bash
pip install pycryptodome

التثبيت من المصدر

root@kitploit:~
git clone https://github.com/example/crypto-tool.git
cd crypto-tool
pip install -r requirements.txt

التحقق من التثبيت

root@kitploit:~
python -c "import Crypto; print(Crypto.__version__)"

الاستخدام

الاستخدام الأساسي

root@kitploit:~
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes

key = get_random_bytes(32)
cipher = AES.new(key, AES.MODE_GCM)
ciphertext, tag = cipher.encrypt_and_digest(b"secret message")

خيارات سطر الأوامر

الخيارالوصف
-k, --keyمفتاح التشفير بصيغة hex
-i, --inputملف الإدخال
-o, --output

أمثلة

تشفير ملف باستخدام AES-256:

root@kitploit:~
python crypto-tool.py -k 0123456789abcdef0123456789abcdef -i plaintext.txt -o encrypted.bin

فك تشفير ملف:

root@kitploit:~
python crypto-tool.py -d -k 0123456789abcdef0123456789abcdef -i encrypted.bin -o decrypted.txt

واجهة برمجة التطبيقات

الفئات الرئيسية

  • Cipher: الفئة الأساسية لجميع عمليات التشفير
  • Hash: فئة التجزئة لحساب قيم التجزئة
  • Key: فئة إدارة المفاتيح

مثال على واجهة برمجة التطبيقات

root@kitploit:~
from crypto_tool import Cipher, Hash

cipher = Cipher(algorithm="AES-256-GCM")
encrypted = cipher.encrypt(data, key)
hash_value = Hash.sha256(data)

الإعدادات

متغيرات البيئة

المتغيرالوصفالقيمة الافتراضية

ملف الإعدادات

root@kitploit:~
crypto:
  algorithm: AES-256-GCM
  key_size: 32
  log_level: INFO
``````http
HTTP/1.1 200 OK

مرفوض. هذا ما يثبت انتحال الهوية بدلاً من المصادفة: اسم المستخدم المُصمَّم يُصادَق عليه بكلمة مرور الحساب ذي الصلاحيات ويفشل مع كلمة مرور المستخدم العادي، لذا فإن بيانات الاعتماد التي يتم التحقق منها تنتمي إلى إدخال دليل مختلف عن العناوين التي يستهدفها القالب.

4.5.2 البناء المُعالَج — طلبات متطابقة

Bind DN الذي يستقبله الدليل لاسم المستخدم المُصمَّم:``` affected : uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com (5 components) remediated : uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com (4 components)

root@kitploit:~
تسجيلات الدخول العادية دون تغيير — الصف 1 يُصادق بشكل مطابق على كلا البنيتين، مع استلام الدليل `uid=jsmith,ou=users,dc=mycompany,dc=com`.

استخدم كلا التشغيلين نفس أداة الاختبار ونفس بيانات الاختبار؛ فقط `shiro-core` على مسار الفئات (classpath) اختلف. تم التأكد من الفئة التي حُمِّلت فعلياً باستخدام `-verbose:class` لكل تشغيل.

---

## 5. تفاصيل المعالجة وكود المعالجة

### 5.1 الإصلاح الأساسي

قم بترميز الـ principal كقيمة سمة DN واحدة قبل الاستبدال، باستخدام مُرمِّز RFC 2253 الخاص بـ JDK، وهو `javax.naming.ldap.Rdn.escapeValue`. هذه هي نفس الآلية التي يستخدمها JDK لبناء الأسماء، لذا فإن مخرجاته متوافقة بحكم البناء مع المحلل الذي يستهلكها.

**الملف:** `core/src/main/java/org/apache/shiro/realm/ldap/DefaultLdapRealm.java````diff
@@ -35,6 +35,7 @@ import org.slf4j.LoggerFactory;
 import javax.naming.AuthenticationNotSupportedException;
 import javax.naming.NamingException;
 import javax.naming.ldap.LdapContext;
+import javax.naming.ldap.Rdn;
 
 /**
  * An LDAP {@link org.apache.shiro.realm.Realm Realm} implementation utilizing Sun's/Oracle's
@@ -239,11 +240,14 @@ public class DefaultLdapRealm extends AuthorizingRealm {
 
         int prefixLength = prefix != null ? prefix.length() : 0;
         int suffixLength = suffix != null ? suffix.length() : 0;
-        StringBuilder sb = new StringBuilder(prefixLength + principal.length() + suffixLength);
+        //the principal is a single attribute value within the resulting name, so it is encoded
+        //to keep any characters that are significant in a Distinguished Name within that value:
+        String value = Rdn.escapeValue(principal);
+        StringBuilder sb = new StringBuilder(prefixLength + value.length() + suffixLength);
         if (prefixLength > 0) {
             sb.append(prefix);
         }
-        sb.append(principal);
+        sb.append(value);
         if (suffixLength > 0) {
             sb.append(suffix);
         }

ما الذي يفرضه الـ hunk. يقوم Rdn.escapeValue بتهريب الشرطة العكسية لمجموعة RFC 2253 المحجوزة (\ , = + < > # ; ") بالإضافة إلى المسافات البيضاء البادئة واللاحقة. بعد ذلك، لا يمكن للنص المُستبدل أن يشغل إلا قيمة RDN واحدة: يقرأ المحلل الأحرف المُهرَّبة كمحتوى، لذا يكون عدد المكونات في النتيجة ثابتًا حسب القالب ولا يمكن أن يتأثر بالـ principal. يتم تحديث تلميح سعة StringBuilder إلى الطول المُرمَّز — وهو تفصيل حجمي، وليس سلوكيًا.

الموضع. يقع الترميز بعد الإرجاع المبكر لحالة القالب غير المُهيأ. وهذا مقصود: في ذلك النمط يوفّر المستدعي DN كاملًا وترميزه سيفسده. يحتفظ كلا الفرعين بعقودهما الحالية.

5.2 تغيير السلوك للمستدعين الشرعيين

هذا الإصلاح يغيّر سلسلة DN الناتجة لأي principal يحتوي على حرف محجوز. ثلاث نتائج جديرة بالذكر بوضوح:

  1. عمليات تسجيل الدخول المعطوبة سابقًا تعمل الآن. أسماء المستخدمين التي تحتوي على شرطة عكسية أو تبدأ بـ # أنتجت DN لم يكن اسمًا جيد التكوين، لذا لم يكن بالإمكان محاولة أي bind على الإطلاق. الآن تُرمَّز إلى DN صالح وستحاول bind. قد ترى المواقع بدء مصادقة حسابات لم تكن قادرة على ذلك سابقًا — وهو إصلاح، لكنه تغيير مرئي. أسماء المستخدمين التي تحتوي على , أو ; أو + أو = لم تكن مرفوضة سابقًا؛ فقد كانت تُحل إلى الإدخال الخاطئ، والآن تُحل إلى الإدخال المقصود.
  2. تختلف DNs الـ bind المُرسَلة إلى الدليل لأسماء المستخدمين المتأثرة. سترى سجلات جانب الدليل، ومسارات التدقيق، وأي استخراج للسجلات يطابق سلاسل DN الحرفية أشكالًا مُهرَّبة (uid=jsmith\,ou\=admins,...). لا يُطلب أي تغيير في التكوين.
  3. عمليات النشر التي اعتمدت على السلوك القديم لبناء DNs متعددة المكونات من حقل اسم المستخدم ستنكسر. هذا ليس تكوينًا مدعومًا — فالقالب موجود لتعريف البنية — لكنه الخطر الوحيد في الترحيل، وهو نفس السلوك الذي يوجد الإصلاح لإزالته.

getUserDnTemplate() مُنفَّذ كـ getUserDn("{0}"). الحرفان { و } غير محجوزين في RFC 2253، لذا فإن قيمة إرجاع الـ accessor دون تغيير. مؤكَّد بواسطة testUserDnTemplate الموجود مسبقًا، والذي ينجح دون تعديل.


6. التحقق والاختبار

6.1 قبل — خط الأساس، غير المُصلَح

الفرع 1.13-CVE-2026-49268، JDK Zulu 1.8.0_432. الأمر كما في §4.4.``` DefaultLdapRealmTest Tests run: 16, Failures: 4, Errors: 0, Skipped: 0 JndiLdapRealmTest Tests run: 16, Failures: 4, Errors: 0, Skipped: 0

root@kitploit:~
مخرجات الفشل المُلتقطة — هذا هو الدليل، وهو غير قابل للاسترداد بمجرد تطبيق الإصلاح:```
testGetUserDnPreservesTemplateStructure:238
  User DN gained or lost components relative to the template:
  uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com
  expected:<4> but was:<5>

testGetUserDnPreservesTemplateStructureForReservedCharacters:262
  Component count changed for principal [jsmith,ou=admins]:
  uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com
  expected:<4> but was:<5>

testGetUserDnEncodesSubstitutedValue:280
  expected:<uid=jsmith[\,ou\]=admins,ou=users,dc=...>
   but was:<uid=jsmith[,ou]=admins,ou=users,dc=...>

testUserDnTemplateSubstitutionPreservesStructure:300
  Unexpected method call
    LdapContextFactory.getLdapContext("uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com", ...)
  expected:
    LdapContextFactory.getLdapContext("uid=jsmith\,ou\=admins,ou=users,dc=mycompany,dc=com", ...)
    expected: 1, actual: 0

testGetUserDnLeavesOrdinaryPrincipalUnchanged نجح على خط الأساس، كما هو مقصود — فهو حارس ضد التصحيح المفرط، وليس اختبارًا إثباتيًا.

6.2 بعد — مع تطبيق الإصلاح

نفس الأمر، نفس JDK:``` DefaultLdapRealmTest Tests run: 16, Failures: 0, Errors: 0, Skipped: 0 JndiLdapRealmTest Tests run: 16, Failures: 0, Errors: 0, Skipped: 0

root@kitploit:~
جميع اختبارات الإثبات الأربعة تمر الآن على كلا الصنفين. إعادة تشغيل §4.3 مع الإصلاح المطبق
تُنتج `RDNs=4` للهوية المُصمَّمة.

### 6.3 الاختبارات المُضافة

**الملف:** `core/src/test/java/org/apache/shiro/realm/ldap/DefaultLdapRealmTest.java`
(+124 سطرًا، 0 حذف). لأن `JndiLdapRealmTest extends DefaultLdapRealmTest`، فإن كل
اختبار أدناه يُنفَّذ ضد **كلا** الصنفين — 10 عمليات تنفيذ من 5 دوال.

| الاختبار | ما يتحقق منه | خط الأساس |
|---|---|---|
| `testGetUserDnLeavesOrdinaryPrincipalUnchanged` | هوية عادية تُنتج الـ DN المتوقع بالضبط، 4 RDNs، القيمة سليمة. يحمي من الإفراط في الترميز. | **ناجح** (حارس) |
| `testGetUserDnPreservesTemplateStructure` | بالنسبة لـ `jsmith,ou=admins` لا يزال الـ DN يحتوي على 4 مكونات والهوية تبقى كقيمة سمة واحدة. | فاشل |
| `testGetUserDnPreservesTemplateStructureForReservedCharacters` | الأمر ذاته، عبر جميع الهويات العشر ذات الأحرف المحجوزة؛ كما يفشل الاختبار إذا لم يكن الـ DN اسمًا جيد التكوين. | فاشل |
| `testGetUserDnEncodesSubstitutedValue` | القيمة المُستبدَلة مُرمَّزة بحيث تعود ذهابًا وإيابًا إلى الهوية المُقدَّمة. | فاشل |
| `testUserDnTemplateSubstitutionPreservesStructure` | من البداية إلى النهاية عبر `getAuthenticationInfo`: الـ DN المُسلَّم إلى `LdapContextFactory` يحتفظ ببنية القالب. | فاشل |

**ملاحظة تصميمية.** تُحلِّل التأكيدات البنيوية النتيجة باستخدام `javax.naming.ldap.LdapName`
وتقارن عدد المكونات وقيمة الورقة العائدة ذهابًا وإيابًا، بدلًا من تأكيد سلسلة مُرمَّزة متوقعة.
وبالتالي تتحقق الاختبارات من الخاصية المطلوبة الفعلية ولا تفترض مسبقًا أن `Rdn.escapeValue` هو
التنفيذ — أي مُرمِّز صحيح بديل سيظل ينجح.

الأمر:```bash
mvn -B clean verify

6.4 الانحدار — البناء الكامل

يمر بناء المفاعل الكامل بدون أعلام وبدون تخطيات:``` mvn -B clean verify

root@kitploit:~
**BUILD SUCCESS — 907 اختبارًا، 0 إخفاقات، 0 أخطاء، 3 متخطاة، عبر المفاعل الكامل.**
كل بوابة نشطة في هذه التشغيلة: اختبارات الوحدة (surefire)، اختبارات التكامل (failsafe)،
تدقيق ترخيص Apache RAT، maven-enforcer، و japicmp.

| البوابة | النتيجة |
|---|---|
| اختبارات الوحدة، المفاعل الكامل | **907 تشغيل، 0 إخفاقات، 0 أخطاء، 3 متخطاة** |
| وحدة `core` وحدها | **321 تشغيل، 0 إخفاقات، 0 أخطاء، 0 متخطاة** |
| اختبارات LDAP (`DefaultLdapRealmTest` + `JndiLdapRealmTest`) | **32 تشغيل، 0 إخفاقات** |
| اختبارات التكامل (failsafe) | شُغّلت عبر المفاعل، لا إخفاقات |
| تدقيق ترخيص Apache RAT | غير معتمد: 0، غير معروف: 0 |
| maven-enforcer | لا انتهاكات |
| japicmp | لم يُبلَّغ عن أي عدم توافق |

التخطيات الثلاث هي `@Ignore`s موجودة مسبقًا في وحدات غير مرتبطة بهذا التغيير؛ `core` — الوحدة الوحيدة التي مُسّت — لا تتخطى شيئًا.

## المراجع

- سجل CVE (MITRE API): `https://cveawg.mitre.org/api/cve/CVE-2026-49268`
- الإعلان الأصلي: `https://lists.apache.org/thread/svszql3od8td7hn6conyj2oq70v53b5s`
- تقارير أمان Apache Shiro: `https://shiro.apache.org/security-reports.html`
- RFC 2253 / RFC 4514 — التمثيل النصي لـ LDAP للأسماء المميزة (Distinguished Names)
- RFC 4515 — التمثيل النصي لمرشح بحث LDAP

## التواصل

مؤلف أبحاث ومعالجة الثغرات الأمنية: Jinwoo Hwang ([https://JinwooHwang.com](https://jinwoohwang.com/))
تنزيل الأداة
الحقلالقيمة
CVECVE-2026-49268 — Apache Shiro: حقن LDAP DN في DefaultLdapRealm
CWE IDCWE-90 — التحييد غير السليم للعناصر الخاصة المستخدمة في استعلام LDAP ('حقن LDAP')
CVSS v4.08.8 HIGH — CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N/S:P/AU:Y/R:A/RE:L/U:Red
CVSS v3.19.1 CRITICAL — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
الإصدارات المتأثرةorg.apache.shiro:shiro-core 0 حتى 2.2.0 ضمناً؛ 3.0.0-alpha-0 حتى 3.0.0-alpha-1 ضمناً
تم الإصلاح في الإصدارات الأصلية2.2.1 و3.0.0-alpha-2
المرجعhttps://lists.apache.org/thread/svszql3od8td7hn6conyj2oq70v53b5s
الهوية الأساسيةنتيجة الأساسالتأثير
jsmith,ou=adminsيُحلَّل، 5 RDNsتغيّر البنية — تم إقحام حاوية
jsmith;ou=adminsيُحلَّل، 5 RDNsتغيّر البنية — ; فاصل RDN وفق RFC 1779
jsmith+uid=adminيُحلَّل، 4 RDNsيصبح RDN متعدد القيم؛ يكتسب uid قيمة ثانية
jsmith=adminيُحلَّل، 4 RDNsتلف القيمة
quo"te، angle<br>ackets، leadingSpace، trailingSpace تُحلَّل، 4 RDNsتلف القيمة
back\slashIllegalArgumentExceptionمشوّه — لا يمكن محاولة الربط
#leadingNumberSignIllegalArgumentExceptionمشوّه — لا يمكن محاولة الربط
الطبقةالمكوّن
عميل HTTPHttpURLConnection، إرسال نموذج خام عبر POST
حاوية ServletEmbedded Jetty 9.4.58.v20250814
مرشّح الأمانShiroFilter + EnvironmentLoaderListener، authc (FormAuthenticationFilter)
RealmDefaultLdapRealm، userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com
الدليلUnboundID InMemoryDirectoryServer، مُجهّز لتسجيل كل bind DN
ملف الإخراج
-v, --verboseتفعيل الإخراج المفصل
CRYPTO_KEYمفتاح التشفير الافتراضيلا يوجد
CRYPTO_LOG_LEVELمستوى التسجيلINFO
CRYPTO_OUTPUT_DIRدليل الإخراج.
الطلبالمتأثرالمُعالَج
username=jsmith&password=userpass302 مصادَق عليه302 مصادَق عليه
username=jsmith%2Cou%3Dadmins&password=adminpass302 مصادَق عليه200 مرفوض
username=jsmith%2Cou%3Dadmins&password=userpass200 مرفوض200 مرفوض