
تحليل أساسي حول CVE-2021-35942. حقن SQL في Django.
Django هو إطار عمل لتطوير تطبيقات الويب مفتوح المصدر، مكتوب بلغة Python ومبني وفق نموذج MVC (Model - View - Controller). تم بناؤه في البداية لإدارة مواقع الويب ذات المحتوى الإخباري المملوكة لمجموعة النشر Lawrence، وهو برنامج CMS (نظام إدارة المحتوى).
توجد ثغرة حقن SQL في إصدارات Django 3.1.x -> 3.1.13 والإصدارات 3.2.x -> 3.2.5.
سبب هذه الثغرة هو أن وظيفة تصفية بيانات الإدخال التي يتحكم فيها المستخدم في QuerySet.order_by() غير كافية لمنع هجمات حقن SQL. يمكن استغلال هذه الثغرة للسماح للمهاجم بتنفيذ إجراءات غير مصرح بها تؤدي إلى تسريب بيانات حساسة.
| CVE - ID | CVE-2021-35042 |
|---|---|
| الخطورة | 9.8 - حرجة |
| CWE - ID | CWE-89: تحييد غير صحيح للعناصر الخاصة المستخدمة في أمر SQL ('حقن SQL') |
| تاريخ نشر الثغرة | 1/7/2021 |
| البرامج المتأثرة | 3.1.x < 3.1.13, 3.2.x < 3.2.5 |
| يتطلب مصادقة | غير مطلوب |
في Django، يتم إنشاء الجداول وتحديد الحقول في قاعدة البيانات عن طريق تعريف فئة نموذج في ملف models.py. في هذا المثال، نعرّف جدولًا باسم Wolf وحقلًا باسم name.


يُستخدم إطار عمل ORM المدمج في Django للتعامل مع قاعدة البيانات، ونتيجة الاستعلام هي مجموعة، وهذه المجموعة هي QuerySet.
order_by(fields)
بشكل افتراضي، تُرجع order_by() QuerySet مرتبًا بترتيب محدد في خيار ordering في Meta الخاص بالنموذج. يمكننا تجاوز شرط order_by في كل استعلام باستخدام طريقة order_by().
مثال
wolves = Wolf.objects.order_by('-name', 'id')
ستكون نتيجة الاستعلام أعلاه مرتبة تنازليًا حسب حقل name، ثم تصاعديًا حسب id. تشير الإشارة السالبة قبل اسم الحقل name إلى أن النتيجة مرتبة تنازليًا.
المثال التالي سيرتب النتائج حسب الحقل المُدخل من المستخدم، وإذا لم يتم تمرير أي قيمة، فسيتم الترتيب حسب حقل
id.
النتيجة

في الإصدارين 3.1 و 3.2، يسمح Django بدمج طريقة الاستعلام مع اسم الجدول في استعلام order_by. هذا هو السبب الرئيسي لهذه الثغرة.
تمرير اسم جدول يعطينا نفس نتيجة تمرير اسم حقل عادي
cve202135042_wolf هو اسم الجدول
أولاً، يستدعي التطبيق مباشرة دالة order_by()، ويتم تعريف الكود الذي يعالج دالة order_by() في:
django/db/models/query.py

تقوم دالة order_by() بأمرين
- حذف جميع الطرق الحالية التي يتم استدعاؤها بواسطة order_by() وحذف المعامل الافتراضي الذي يتم تمريره عندما يستقبل order_by قيمة مختلفة.
- تمرير المعامل إلى order_by. تقوم دالة
add_ordering()بهذا الأمر.
def add_ordering(self, *ordering):
"""
Add items from the 'ordering' sequence to the query's "order by"
clause. These items are either field names (not column names) --
possibly with a direction prefix ('-' or '?') -- or OrderBy
expressions.
If 'ordering' is empty, clear all ordering from the query.
"""
errors = []
for item in ordering:
if isinstance(item, str):
if '.' in item:
warnings.warn(
'Passing column raw column aliases to order_by() is '
'deprecated. Wrap %r in a RawSQL expression before '
'passing it to order_by().' % item,
category=RemovedInDjango40Warning,
stacklevel=3,
)
continue
if item == '?':
continue
if item.startswith('-'):
item = item[1:]
if item in self.annotations:
continue
if self.extra and item in self.extra:
continue
# names_to_path() validates the lookup. A descriptive
# FieldError will be raise if it's not.
self.names_to_path(item.split(LOOKUP_SEP), self.model._meta)
elif not hasattr(item, 'resolve_expression'):
errors.append(item)
if getattr(item, 'contains_aggregate', False):
raise FieldError(
'Using an aggregate in order_by() without also including '
'it in annotate() is not allowed: %s' % item
)
if errors:
raise FieldError('Invalid order_by arguments: %s' % errors)
if ordering:
self.order_by += ordering
else:
self.default_ordering = False
المعامل الذي يتم تمريره إلى add_ordering() هو مصفوفة.
مثال عند تمرير المعامل كما يلي:
wolves = Wolf.objects.order_by( 'name' , 'id' )عندها، سيقوم التطبيق بتحويله إلى استعلام في قاعدة البيانات كما يلي:
SELECT "cve202135042_wolf"."id", "cve202135042_wolf"."name" FROM "cve202135042_wolf" ORDER BY "cve202135042_wolf"."name" ASC, "cve202135042_wolf"."id" ASC
عند تمريره، ستقوم دالة add_ordering بفحص كل عنصر في المصفوفة، وإذا كان string، فسيتم فحصه في 5 حالات:
if '.' in item:سيتحقق مما إذا كان استعلامًا باسم عمود وأن هذا العمود له اسم جدول محدد في عبارة SQL أم لا. إذا كان كذلك، فسيصدر تحذيرًا وينفذcontinue.if item == '?':إذا كانت قيمة العنصر هي علامة '?'، فسيتم ترتيب النتائج عشوائيًا، ثمcontinue.if item.startswith('-'):إذا بدأ العنصر بالحرف '-'، فسيتم ترتيب نتائج الاستعلام تنازليًا (DESC).if item in self.annotations:يتحقق مما إذا كان يحتوي على تعليق توضيحي أم لا، وإذا كان كذلك، فسيتمcontinue.if self.extra and item in self.extra:يحدد ما إذا كانت هناك إضافات، وإذا كان كذلك، فسيتمcontinue.
بعد الفحوصات الخمسة، يتم تمرير المعامل إلى دالة self.names_to_path(item.split(LOOKUP_SEP), self.model._meta) لمواصلة التحقق مما إذا كان اسم عمود صالحًا أم لا، وبعد ذلك إذا كان صالحًا، فسيتم إضافته إلى self.ordering الخاص بفئة Query لمزيد من المعالجة.
يقوم ORM الخاص بـ Django بتصفية البيانات المُدخلة في الاستعلام بشكل صارم للغاية، لكن التغيير في الكود المصدري هذه المرة الذي أدى إلى حقن SQL هو أن المؤلف افترض أنه إذا كان اسم العمود هو عمود UUID (المعرف الفريد العام)، فلن يمكن تنفيذ استعلام order_by.
أي إذا كانت البيانات المُدخلة هي xxx-xxx-xxx-xxx (تنسيق UUID)، فلن يمكن تنفيذ الاستعلام.
الكود قبل إجراء التغيير
# django/db/models/sql/constants.py
ORDER_PATTERN = _lazy_re_compile ( r '\?|[-+]?[.\w]+$' )