
تحليل تقنيتين لتجاوز المصادقة في Apache Shiro (CVE-2020-17523) مع بيئة استغلال قابلة لإعادة الإنتاج وفحص تفصيلي للسبب الجذري.
Apache Shiro هو إطار عمل أمني قوي وسهل الاستخدام بلغة Java، يقوم بتنفيذ التحقق من الهوية والتفويض وإدارة كلمات المرور والجلسات. باستخدام واجهة برمجة التطبيقات (API) سهلة الفهم التي يوفرها Shiro، يمكنك بسرعة وسهولة تأمين أي تطبيق، بدءًا من أصغر تطبيقات الهاتف المحمول وصولاً إلى أكبر تطبيقات الويب والمؤسسات.
عند استخدامه مع Spring، وفي ظل قواعد معينة لمطابقة الأذونات، يمكن للمهاجم تجاوز التحقق من الهوية عبر إنشاء حزم طلبات HTTP خاصة.
النطاق المتأثر: Apache Shiro < 1.7.1
shiro 1.7.0
تم تحديث بيئات الثغرة للطريقتين: https://github.com/jweny/shiro-cve-2020-17523
الطريقة الأولى:
http://127.0.0.1:8080/admin/%20 أو http://127.0.0.1:8080/admin/%20/
يمكن تجاوز مصادقة Shiro باستخدام الأحرف الفارغة مثل المسافة.

الطريقة الثانية:
بعد التواصل مع الخبير p0desta، تم اكتشاف طريقة استغلال أخرى في سيناريو خاص.
http://127.0.0.1:8080/admin/%2e أو http://127.0.0.1:8080/admin/%2e/
لكن . (وكذلك /) في قواعد مطابقة المسارات في Spring تمثل فواصل المسار، ولا تتم مطابقتها كأحرف عادية. لذلك في الظروف الافتراضية، الوصول إلى /admin/. سيعيد 404.
ولكن في سيناريو تفعيل المسار الكامل setAlwaysUseFullPath(true)، تتم المطابقة بشكل طبيعي.

في Shiro، يتم الحصول على عنوان URL ومطابقته في org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain
لنلقِ نظرة سريعة على طريقة getChain هذه:


تتحقق هذه الطريقة أولاً مما إذا كان requestURI ينتهي بـ/، وإذا كان كذلك، فتحذف آخر /.
ثم في حلقة مطابقة المسارات، تتحقق أولاً مما إذا كان نمط المسار pathPattern ينتهي بـ/، وإذا كان كذلك فسيتم حذفه أيضًا. بعد ذلك تستدعي طريقة pathMatches() لإجراء مطابقة المسار.
لذلك في طريقتي الاستغلال، لا يهم ما إذا كان المسار ينتهي بـ/ أم لا، لأنه سيتم حذفه بمجرد المرور عبر طريقة getChain.
دعنا نركز على طريقة pathMatches():
استدعِ Evaluate، واحسب كلاً من pathMatches("/admin/*","/admin/1") وpathMatches("/admin/*","/admin/ ")، الأول يطابق بشكل طبيعي بينما يفشل الثاني في المطابقة.


ابدأ التصحيح؛ ستمر عملية التصحيح بسلسلة طويلة من ضغطات F7. حتى الوصول إلى doMatch("/admin/*","/admin/ "). نلاحظ أن pathDirs المُعاد من tokenizeToStringArray لم يعد يحتوي على مسار المستوى الثاني. لذلك يؤدي ذلك إلى عدم تطابق /admin/* مع /admin .

عند تتبع طريقة tokenizeToStringArray، نجد أن المعامل trimTokens يكون true عند استدعاء طريقة tokenizeToStringArray.

أما طريقة tokenizeToStringArray، فعندما يكون المعامل trimTokens بقيمة true، فإنها تخضع لمعالجة trim()، مما يؤدي إلى إزالة المسافات. وعند العودة إلى getChain مرة أخرى، يتم حذف آخر /. لذلك فإن pathDirs المُعاد من tokenizeToStringArray لا يحتوي على مسار المستوى الثاني.

الخلاصة: في إصدار Shiro المتأثر بالثغرة، نظرًا لأن المعامل trimTokens يكون true افتراضيًا عند استدعاء طريقة tokenizeToStringArray، فإن المسافات تخضع لمعالجة trim()، مما يؤدي إلى إزالتها. وعند العودة إلى getChain مرة أخرى، يتم حذف آخر /، لذلك يفشل تطابق /admin مع /admin/*، مما يؤدي إلى تجاوز التحقق من الهوية. بينما يستقبل Spring مسار الوصول /admin/%20 ويعيد الاستجابة وفق المنطق الطبيعي، مما يؤدي إلى تجاوز الصلاحيات.
عند رؤية /. و/./ في الطريقة الثانية، هل تذكرت طريقة مألوفة؟ نعم، إنها normalize().

وبترجمة مبسطة:
| الشرط | المثال |
|---|---|
| تحويل الخط المائل العكسي إلى خط مائل أمامي | \ -> / |
| تحويل الخطين المائلين الأماميين إلى خط مائل أمامي |
لذلك فإن /admin/. بعد معالجته إلى /admin/./ يصبح /admin/.

وبعد معالجته بواسطة org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain، وبما أن المسار ينتهي بـ/، وإذا كان كذلك يتم حذف آخر /، فيصبح /admin. لا يطابق ``/adminمع/admin/*`، وبالتالي تم تجاوز مصادقة Shiro.

وفي هذا الوقت، يكون الطلب الذي يستقبله Spring هو /admin/.. إذا لم يتم تفعيل مطابقة المسار الكامل، فإن . و/ في Spring تُعتبر فواصل مسار ولا تشارك في مطابقة المسار. لذلك لن يتم العثور على mapping مطابق، ويتم إرجاع 404.

عند تفعيل مطابقة المسار الكامل، سيتم مطابقة عنوان url بالكامل، وبالتالي يعيد Spring 200.
فيما يلي الكود الخاص بتفعيل مطابقة المسار الكامل:
@SpringBootApplication
public class SpringbootShiroApplication extends SpringBootServletInitializer implements BeanPostProcessor {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(SpringbootShiroApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(SpringbootShiroApplication.class, args);
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof RequestMappingHandlerMapping) {
((RequestMappingHandlerMapping) bean).setAlwaysUseFullPath(true);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
}
بناءً على التحليل أعلاه، هناك سببان لتجاوز صلاحيات Shiro:
tokenizeToStringArray لا تتعامل مع المسافات بشكل صحيح./ لا ينبغي أن يكون قبل منطق حلقة مطابقة المسار.لذلك فإن خطة الإصلاح الرسمية هي:
https://github.com/apache/shiro/commit/0842c27fa72d0da5de0c5723a66d402fe20903df
trimTokens في tokenizeToStringArray على false.
/. تم التعديل بحيث تتم مطابقة المسار الأصلي أولاً، وعند فشل المطابقة يتم الانتقال إلى منطق حذف آخر /.
من حيث المبدأ، تقوم trim() بمسح جميع المسافات البيضاء (whitespace) قبل السلسلة النصية وبعدها، والمسافة (space) هي واحدة منها فقط. لكن في الاختبارات تبيّن أنه بالإضافة إلى المسافة، فإن أي whitespace آخر، مثل %08 و%09 و%0a، سيعيد spring+tomcat 400 عند معالجته.
لذلك، وباستثناء المسافة، لم يتم العثور على حمولات (payload) أخرى مفيدة للطريقة الأولى.
https://github.com/apache/shiro/commit/0842c27fa72d0da5de0c5723a66d402fe20903df
| // -> / |
| إذا انتهى بـ /. أو /..، يتم إضافة / في النهاية | /. -> /./ /.. -> /../ |
| تطبيع /./ | /./ -> / |
| القفز في المسار | /aaa/../bbb -> /bbb |