Djangoは、Pythonで書かれたオープンソースのWebアプリケーションフレームワークであり、MVC(Model - View - Controller)モデルに基づいて構築されています。当初は、ローレンス出版グループが所有するニュースコンテンツサイトを管理するために構築されたCMS(コンテンツ管理システム)ソフトウェアでした。
Djangoバージョン3.1.xから3.1.13、およびバージョン3.2.xから3.2.5には、SQLインジェクションの脆弱性が存在します。
この脆弱性の原因は、QuerySet.order_by() におけるユーザーが制御する入力データのフィルタリング機能が、SQLインジェクション攻撃を防ぐのに十分ではなかったためです。この脆弱性が悪用されると、攻撃者が不正な操作を実行し、機密データの漏えいにつながる可能性があります。
| CVE - ID | CVE-2021-35042 |
|---|
| 深刻度 | 9.8 - CRITICAL |
| CWE - ID | CWE-89: SQLコマンドに使用される特殊要素の不適切な無効化('SQLインジェクション') |
| 脆弱性公開日 | 2021年7月1日 |
| 影響を受けるソフトウェア | 3.1.x < 3.1.13, 3.2.x < 3.2.5 |
| 認証要件 | 不要 |
Djangoでは、データベース内のテーブルの作成とフィールドの定義は、models.py ファイル内でモデルクラスを宣言することによって行われます。この例では、Wolf という名前のテーブルと、name という名前のフィールドを宣言します。


Djangoに組み込まれているORMフレームワークはデータベースを操作するために使用され、クエリの結果はコレクションであり、このコレクションはQuerySetです。
order_by(fields)
デフォルトでは、order_by() は、ModelのMeta内のorderingオプションで指定された順序でソートされたQuerySetを返します。各クエリで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()関数は2つのことを行います
- 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します。
5回のチェックの後、パラメータはself.names_to_path(item.split(LOOKUP_SEP), self.model._meta)関数に渡され、それが有効な列名であるかどうかを引き続きチェックします。その後、有効であれば、それらはQueryクラスのself.orderingに追加されて処理が続行されます。
DjangoのORMは、クエリに入力されるデータを非常に厳密にフィルタリングしますが、今回のソースコードの変更により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セットアップ完了後の画面

条件: 悪用するには、何らかの方法でテーブル名を知っている必要があります :))
SQL文をインジェクションする際、SQLi文を実行するにはテーブル名を知っている必要があります。
テーブル名を間違って入力した場合

正しいテーブル名を入力すると、orderbyクエリは正常に実行されます。

この時点でのSQL文は次のようになります。
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