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.py
def vul(request):
# 获取order
query = request.GET.get('order', default='id')
q = Collection.objects.order_by(query)
return HttpResponse(q.values())
模型models.py代码。
# models.py
class 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 be
no 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 OrderBy
expressions.
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,
)
continue
if item == '?':
continue
if item.startswith('-'):
item = item[1:]
if item in self.annotations:
continue
if 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 += ordering
else:
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 single
name in 'names' can generate multiple PathInfos (m2m, for example).
'names' is the path of names to travel, 'opts' is the model Options we
start 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 resolved
will 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 to
contain the same value as the final field). Finally, return those names
that 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 = None
filtered_relation = None
try:
field = opts.get_field(name)
except FieldDoesNotExist:
if name in self.annotation_select:
field = self.annotation_select[name].output_field
elif 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_model
except AttributeError:
# QuerySet.annotate() may introduce fields that aren't
# attached to a model.
model = None
else:
# We didn't find the current field, so move position back
# one step.
pos -= 1
if 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_opts
if 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_field
opts = last.to_opts
targets = last.target_fields
cur_names_with_path[1].extend(pathinfos)
names_with_path.append(cur_names_with_path)
else:
# Local non-relational field.
nfinal_field = field
targets = (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))
break
return 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
长按识别二维码关注我们