CVE-2022-34265: Potential SQL injection via `Trunc(kind)` and `Extract(lookup_name)` arguments
# 개요
최근 우리 회사 슬랙 방에서 평소 존경하던 갓해커 분이 올려주신 Django SQL Injection CVE가 하나 있어서 분석해보았다. 아무래도 Django를 통해 사내 프로젝트, 학교 프로젝트를 몇 번씩 진행해본 경험이 있다보니 자칭 Django 전문가로서 CVE를 분석해보지 않고 지나칠 수가 없었다.
데모 소스코드를 보니 아래 부분에서 취약점이 발생했다. Extract와 Trunc에서 비슷한 이유로 취약점이 발생하는 듯 보였다.
Trunc 함수에서 받은 인자에 대해 적절히 처리해주지 못해서 발생하는 SQL Injection 이었다. 사실 Pythonic하지 못한 코드에서 발생하는 보안 취약점이라 파급력 자체는 강하다고 볼 수는 없다. 그런데 보안 취약점이 맞기는 하다. 세상의 수많은 프로그래머들 중 아래와 같이 코드를 작성하는 사람 한명이 없을까.
print("[ INFO ] query :", ChallengeModel.objects.all().annotate(time=Trunc("created_at", "2020'")).query)
# [ INFO ] query : SELECT "challenge_challenge"."id", "challenge_challenge"."image_id", "challenge_challenge"."path", "challenge_challenge"."build_command", "challenge_challenge"."category_id", "challenge_challenge"."status", "challenge_challenge"."name", "challenge_challenge"."body", "challenge_challenge"."score", "challenge_challenge"."open_time", "challenge_challenge"."file", "challenge_challenge"."created_at", "challenge_challenge"."updated_at", django_datetime_trunc('2020'', "challenge_challenge"."created_at", 'UTC', 'UTC') AS "time" FROM "challenge_challenge"
이게 약간 무슨 느낌인지 PHP를 예시로 들어서 설명하자면 아래 코드와 같다.
<?php
// ...
$username = $_REQUEST["username"];
$password = $_REQUEST["password"];
$query = "SELECT * FROM `users` WHERE `username` = '{$username}' AND `password` = md5('{$password}')";
// ...
그냥 $_REQUEST를 받을 때부터 md5로 해시처리하면 되는데 굳이 MySQL의 md5 함수 인자로 넣어 해시처리하는 느낌?
내가 만약 위와 같은 기능의 코드를 작성했다면 아래처럼 코드를 작성했을 것이다.
- 사용자로부터 date 값을 str 형태로 받고
- Django의 timezone 오브젝트를 이용해 datetime 객체로 변환한 다음
- Django ORM으로 QuerySet 질의
내일은 Trunc 클래스와 annotate 메소드를 분석해서 Django ORM에서 어떻게 QuerySet <-> Raw SQL Query를 구현했는지 찾아봐야겠다.
실제 소스코드 분석
## 1. 개요
CVE-2022-34265: Potential SQL injection via `Trunc(kind)` and `Extract(lookup_name)` arguments.
- change log : https://github.com/django/django/commit/284b188a4194e8fa5d72a73b09a869d7dd9f0dc5
Extract와 Trunc 중 Trunc에 대한 설명만 하도록 하겠다.
## 2. 설명
이 취약점의 시작은 `QuerySet` 클래스의 `dates()` 메소드로부터 시작한다.
* `dates()` : https://docs.djangoproject.com/en/4.0/ref/models/querysets/#dates
위와 같이 `dates()` 메소드의 인자로 넘길 수 있는 `str`형의 `kind` 값이 있다. 이 값은 `django/db/models/functions/datetime.py`에 정의된 `Trunc` 클래스에 그대로 인자로 넘겨진다. 하지만 `kind` 값을 `dates()` method 내부에서 검사하기 때문에 원하는 값이 `Trunc` 클래스에 넘겨질 수 없다. 하지만 `Trunc` 클래스를 직접 사용하는 경우 이야기가 다르다.
`django/db/models/functions/datetime.py`에 정의된 `Trunc` 클래스는 `TruncBase` 클래스를 상속받은 형태로, 실질적으로 ORM에서 SQL 쿼리로 변환하는 기능은 `TruncBase` 클래스의 `as_sql()` 메소드로 정의되어 있다. 이는 인자로 넘겨진 `output_field`를 확인하고 변수의 유형별로`datetime_trunc_sql()`, `date_trunc_sql()`, `time_trunc_sql()` 메소드를 호출한다. 이 세 가지의 메소드는 각 DBMS별 문법에 따라 각각 정의되어 있으며, SQLite3를 예시로 들어 설명하자면 아래와 같이 정의되어 있다.
두 번째에 전달되는 `lookup_type`로 `Trunc`의 `kind` 값이 전달이 되는데, 실제 SQL Query를 만들기까지 `kind` 값에 대한 아무런 검증이 없다. 때문에 `kind` 값을 통해 SQL Injection이 가능하다.
## 3. 패치
`django/db/models/functions/datetime.py`에 정의된 `TruncBase` 클래스의 `as_sql()` 메소드에 아래와 같은 코드가 추가되었다.
def as_sql(self, compiler, connection):
if not connection.ops.extract_trunc_lookup_pattern.fullmatch(self.kind):
raise ValueError("Invalid kind: %s" % self.kind)
`as_sql()` 메소드를 호출하고 나서 `extract_trunc_lookup_pattern`을 인자로 넘겨진 `kind` 값과 정규식 기능을 통해 비교한다. 정규식으로 검사하는 값은 `_lazy_re_compile(r"[\w\-_()]+")` 이다.
위와 같이 `kind` 값에 대한 직접적인 비교로 공격을 차단하는 방법 외에도, `TruncBase` 클래스에서 SQL로 변환할 때 호출하는 `as_sql()` method 내부 구현 방식이 아래와 같이 바뀌었다.
기존에는 SQL Query 안에 직접적으로 변환하고자 하는`lookup_type`, 즉 `kind`를 넣는 방식으로 구현되어 있었다. 이 취약점이 패치된 지금은 아예 kind (= lookup_type)가 들어가야할 자리를 SQL placeholder로 지정하고, Database Cursor에서 execute 할 때 params에 kind (=lookup_type) 값을 넣는 식으로 수정되었다.
## 4. 느낀점
Django에 얼마 남지 않은 SQL Injection 개꿀 취약점이었는데 내가 몇 달만 먼저 볼 걸! 😓
'write-ups' 카테고리의 다른 글
CVE-2022-28347: Potential SQL injection in Django QuerySet `explain()` Analysis (0) | 2022.07.21 |
---|---|
HackTheBox Heist write-up (0) | 2021.05.10 |
HackTheBox LoveTok write-up (0) | 2021.04.19 |
HackTheBox baby website rick write-up (0) | 2021.04.15 |
HackTheBox baby breaking grad write-up (0) | 2021.04.15 |