Django order_by SQL注入漏洞分析(CVE-2021-35042)
2023-4-28 00:54:21 Author: 白帽子(查看原文) 阅读量:19 收藏

STATEMENT

声明

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测及文章作者不为此承担任何责任。

雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。

漏洞介绍

官方给出的解释如下:

[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

安恒信息

杭州亚运会网络安全服务官方合作伙伴

成都大运会网络信息安全类官方赞助商

武汉军运会、北京一带一路峰会

青岛上合峰会、上海进博会

厦门金砖峰会、G20杭州峰会

支撑单位北京奥运会等近百场国家级

重大活动网络安保支撑单位

END

长按识别二维码关注我们


文章来源: http://mp.weixin.qq.com/s?__biz=MzAwMDQwNTE5MA==&mid=2650246661&idx=1&sn=db155861c0fd445398efd912ca266986&chksm=82ea55acb59ddcba4d0f8f66adb0addcb102bebcde226af14f2a1365e18d696c19fd37fa38f2#rd
如有侵权请联系:admin#unsafe.sh