write-ups

CVE-2022-34265: Potential SQL injection via `Trunc(kind)` and `Extract(lookup_name)` arguments

2022. 7. 15. 01:39

밤새서 공부하고 난 뒤 본 남산의 일출

 

# 개요

 최근 우리 회사 슬랙 방에서 평소 존경하던 갓해커 분이 올려주신 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 함수 인자로 넣어 해시처리하는 느낌?

 

 

 내가 만약 위와 같은 기능의 코드를 작성했다면 아래처럼 코드를 작성했을 것이다.

  1. 사용자로부터 date 값을 str 형태로 받고
  2. Django의 timezone 오브젝트를 이용해 datetime 객체로 변환한 다음
  3. 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 

 

[4.1.x] Fixed CVE-2022-34265 -- Protected Trunc(kind)/Extract(lookup_… · django/django@284b188

…name) against SQL injection. Thanks Takuto Yoshikai (Aeye Security Lab) for the report.

github.com

Extract와 Trunc 중 Trunc에 대한 설명만 하도록 하겠다.

 

## 2. 설명

이 취약점의 시작은 `QuerySet` 클래스의 `dates()` 메소드로부터 시작한다. 
* `dates()` : https://docs.djangoproject.com/en/4.0/ref/models/querysets/#dates

 

QuerySet API reference | Django documentation | Django

Django The web framework for perfectionists with deadlines. Overview Download Documentation News Community Code Issues About ♥ Donate

docs.djangoproject.com

 

django/db/models/query.py, QuerySet

 

위와 같이 `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를 예시로 들어 설명하자면 아래와 같이 정의되어 있다.

 

django/db/backends/sqlite3/operations.py, DatabaseOperations

두 번째에 전달되는 `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 내부 구현 방식이 아래와 같이 바뀌었다. 

 

django/db/models/functions/datetime.py, TruncBase

기존에는 SQL Query 안에 직접적으로 변환하고자 하는`lookup_type`, 즉 `kind`를 넣는 방식으로 구현되어 있었다. 이 취약점이 패치된 지금은 아예 kind (= lookup_type)가 들어가야할 자리를 SQL placeholder로 지정하고, Database Cursor에서 execute 할 때 params에 kind (=lookup_type) 값을 넣는 식으로 수정되었다.

 

 

## 4. 느낀점

Django에 얼마 남지 않은 SQL Injection 개꿀 취약점이었는데 내가 몇 달만 먼저 볼 걸! 😓