Django order_by SQL注入漏洞分析(CVE-2021-35042)
漏洞介紹
官方給出的解釋如下:
[Django]:https://www.djangoproject.com/weblog/2021/jul/01/security-releases/
CVE-2021-35042: Potential SQL injection via unsanitized QuerySet.order_by() input
Unsanitized user input passed to QuerySet.order_by() could bypass intended column reference validation in path marked for deprecation resulting in a potential SQL injection even if a deprecation warning is emitted.
As a mitigation the strict column reference validation was restored for the duration of the deprecation period. This regression appeared in 3.1 as a side effect of fixing # 31426.
The issue is not present in the main branch as the deprecated path has been removed.
該漏洞是由于QuerySet.order_by()查詢時 ,對用戶傳入的參數過濾不嚴格,可以使攻擊者在不需要授權的情況下,構造惡意的參數執行SQL注入攻擊。
漏洞評級:高危
影響版本:Django 3.2、Django 3.1
安全版本:Django >= 3.2.5、Django >= 3.1.13
漏洞分析
2.1 order_by()
order_by是QuerySet下的一種查詢方法,作用是將查詢的結果根據某個字段進行排序,在字段前面加一個符號,結果會倒序輸出。但是如果對列名的查詢過濾不嚴格就會導致SQL注入。
2.2 漏洞原理
Django是MTV架構,視圖views.py的代碼。
# views.pydef vul(request):# 獲取orderquery = request.GET.get('order', default='id')q = Collection.objects.order_by(query)return HttpResponse(q.values())
模型models.py代碼。
# models.pyclass Collection(models.Model):name = models.CharField(max_length=128)
首先獲取用戶傳入的order,沒有傳入參數默認為id,獲取到參數之后 Collection.objects.order_by處理,跟一下order_by。
def order_by(self, *field_names):"""Return a new QuerySet instance with the ordering changed."""assert not self.query.is_sliced, \"Cannot reorder a query once a slice has been taken."obj = self._chain()obj.query.clear_ordering(force_empty=False)obj.query.add_ordering(*field_names)return obj
參數傳入order_by后賦值給obj,經過clear_ordering處理。
def clear_ordering(self, force_empty):"""Remove any ordering settings. If 'force_empty' is True, there will beno ordering in the resulting query (not even the model's default)."""self.order_by = ()self.extra_order_by = ()if force_empty:self.default_ordering = False
clear_ordering的作用是清除所有通過order\_by函數調用的方法。然后再經過add_ordering處理。
def add_ordering(self, *ordering):"""Add items from the 'ordering' sequence to the query's "order by"clause. These items are either field names (not column names) --possibly with a direction prefix ('-' or '?') -- or OrderByexpressions.
If 'ordering' is empty, clear all ordering from the query."""errors = []for item in ordering:if isinstance(item, str):if '.' in item:warnings.warn('Passing column raw column aliases to order_by() is ''deprecated. Wrap %r in a RawSQL expression before ''passing it to order_by().' % item,category=RemovedInDjango40Warning,stacklevel=3,)continueif item == '?':continueif item.startswith('-'):item = item[1:]if item in self.annotations:continueif self.extra and item in self.extra:continue# names_to_path() validates the lookup. A descriptive# FieldError will be raise if it's not.self.names_to_path(item.split(LOOKUP_SEP), self.model._meta)elif not hasattr(item, 'resolve_expression'):errors.append(item)if getattr(item, 'contains_aggregate', False):raise FieldError('Using an aggregate in order_by() without also including ''it in annotate() is not allowed: %s' % item)if errors:raise FieldError('Invalid order_by arguments: %s' % errors)if ordering:self.order_by += orderingelse:self.default_ordering = False
傳入的參數到達add_ordering之后進入for循環,但是如果參數中含有.則會直接跳出循環,因為只有一個參數,而且也是單次循環所以最終不會進入names_to_path方法,而是執行self.order_by += ordering(ordering就是傳入的參數),所以add_ordering的作用就是增加self.order_by參數。
當傳入的參數是默認的id時,SQL語句為。
SELECT "vuln_collection"."id", "vuln_collection"."name" FROM "vuln_collection" ORDER BY (id) ASC。
當參數為id.時:
SELECT "vuln_collection"."id", "vuln_collection"."name" FROM "vuln_collection" ORDER BY (id.) ASC。
當參數為
vuln_collection.id);select updatexml(1,concat(0x7e,(select @@version)),1);#時。
SELECT "vuln_collection"."id", "vuln_collection"."name" FROM "vuln_collection" ORDER BY (vuln_collection.id);select updatexml(1,concat(0x7e,(select @@version)),1);# ASC
注入原理就是使用)進行閉合再利用;進行堆疊注入。
2.3 漏洞成因
在add_ordering中如果正常進入for循環沒有跳出循環,會經過五次if判斷
1.if '.' in item:,判斷參數中是否帶有.,有則會跳出循環。
2.if item == '?':,判斷參數是否是?,如果是則跳出循環。
3.if item.startswith('-'):,判斷參數是否以-開頭,如果是則去除開頭的-。
4.if item in self.annotations:,判斷參數是否含有注釋標識符,是則跳出循環。
5.if self.extra and item in self.extra:,判斷參數是否有額外的參數信息,是則跳出循環。
經過五次if判斷之后表示參數無異常,然后進入names_to_path獲取數據,利用model模型判斷當前的參數是否是有效的列名,如果不是有效的列名則會報錯。
def names_to_path(self, names, opts, allow_many=True, fail_on_missing=False):"""Walk the list of names and turns them into PathInfo tuples. A singlename in 'names' can generate multiple PathInfos (m2m, for example).
'names' is the path of names to travel, 'opts' is the model Options westart the name resolving from, 'allow_many' is as for setup_joins().If fail_on_missing is set to True, then a name that can't be resolvedwill generate a FieldError.
Return a list of PathInfo tuples. In addition return the final field(the last used join field) and target (which is a field guaranteed tocontain the same value as the final field). Finally, return those namesthat weren't found (which are likely transforms and the final lookup)."""path, names_with_path = [], []for pos, name in enumerate(names):cur_names_with_path = (name, [])if name == 'pk':name = opts.pk.name
field = Nonefiltered_relation = Nonetry:field = opts.get_field(name)except FieldDoesNotExist:if name in self.annotation_select:field = self.annotation_select[name].output_fieldelif name in self._filtered_relations and pos == 0:filtered_relation = self._filtered_relations[name]if LOOKUP_SEP in filtered_relation.relation_name:parts = filtered_relation.relation_name.split(LOOKUP_SEP)filtered_relation_path, field, _, _ = self.names_to_path(parts, opts, allow_many, fail_on_missing,)path.extend(filtered_relation_path[:-1])else:field = opts.get_field(filtered_relation.relation_name)if field is not None:# Fields that contain one-to-many relations with a generic# model (like a GenericForeignKey) cannot generate reverse# relations and therefore cannot be used for reverse querying.if field.is_relation and not field.related_model:raise FieldError("Field %r does not generate an automatic reverse ""relation and therefore cannot be used for reverse ""querying. If it is a GenericForeignKey, consider ""adding a GenericRelation." % name)try:model = field.model._meta.concrete_modelexcept AttributeError:# QuerySet.annotate() may introduce fields that aren't# attached to a model.model = Noneelse:# We didn't find the current field, so move position back# one step.pos -= 1if pos == -1 or fail_on_missing:available = sorted([*get_field_names_from_opts(opts),*self.annotation_select,*self._filtered_relations,])raise FieldError("Cannot resolve keyword '%s' into field. ""Choices are: %s" % (name, ", ".join(available)))break# Check if we need any joins for concrete inheritance cases (the# field lives in parent, but we are currently in one of its# children)if model is not opts.model:path_to_parent = opts.get_path_to_parent(model)if path_to_parent:path.extend(path_to_parent)cur_names_with_path[1].extend(path_to_parent)opts = path_to_parent[-1].to_optsif hasattr(field, 'get_path_info'):pathinfos = field.get_path_info(filtered_relation)if not allow_many:for inner_pos, p in enumerate(pathinfos):if p.m2m:cur_names_with_path[1].extend(pathinfos[0:inner_pos + 1])names_with_path.append(cur_names_with_path)raise MultiJoin(pos + 1, names_with_path)last = pathinfos[-1]path.extend(pathinfos)final_field = last.join_fieldopts = last.to_optstargets = last.target_fieldscur_names_with_path[1].extend(pathinfos)names_with_path.append(cur_names_with_path)else:# Local non-relational field.nfinal_field = fieldtargets = (field,)if fail_on_missing and pos + 1 != len(names):raise FieldError("Cannot resolve keyword %r into field. Join on '%s'"" not permitted." % (names[pos + 1], name))breakreturn path, final_field, targets, names[pos + 1:]
現在看來漏洞出現的原因就很好理解了,如果我們傳入的參數中含有.那么就無法進入names_to_path完成對列名的驗證,攻擊者就可以利用這個缺陷構造含有.的參數利用order by進行SQL注入。
漏洞復現
這里使用的環境是vulhub靶場。
https://vulhub.org/# /environments/django/CVE-2021-35042/
傳入參數:id

傳入參數:-id

傳入參數:id.

出現了報錯信息。
傳入參數:
vuln_collection.id);select updatexml(1,concat(0x7e,(select @@version)),1);#

成功查詢出數據庫版本信息,后續按正常操作進行注入即可。
漏洞修復
官方給出的修改方案:
https://github.com/django/django/commit/0bd57a879a0d54920bb9038a732645fb917040e9
django/db/models/sql/constants.py

django/db/models/sql/query.py

在django/db/models/sql/query.py中的add_ordering對.的判斷添加了正則,匹配規則更加嚴格。
參考鏈接
https://xz.aliyun.com/t/9834# toc-0
https://www.bugxss.com/vulnerability-report/3095.html
https://www.cnblogs.com/R3col/p/16094132.html
https://github.com/django/django/commit/0bd57a879a0d54920bb9038a732645fb917040e9
https://code.djangoproject.com/ticket/31426