网安教育
培养网络安全人才
技术交流、学习咨询
该漏洞是由于对QuerySet.order_by()中用户提供数据的过滤不足,攻击者可利用该漏洞在未授权的情况下,构造恶意数据执行SQL注入攻击,最终造成服务器敏感信息泄露。
先本地创建一个Django环境,使用的版本为Django 3.1.10。具体的示例代码就使用:https://github.com/YouGina/CVE-2021-35042。
其中获取GET参数值的是request.GET.get('order_by', 'name')这么一段,从order_by 中获取值,缺省为name。这个name的意思是数据库的字段。在models.py文件中有定义,也就是其实获取的是需要去查询的数据库字段名。
1class User(models.Model):
2 name = models.CharField(max_length=200)
3
4 def __str__(self):
5 return self.name
order_by这个参数的作用的排序,对一个列或者多个值进行升序或者降序的排列。比如:
1SELECT * FROM Websites ORDER BY alexa DESC;
上面这个SQL的意思就是,按照按照Alexa的顺序降序排列,DESC为降序,ASC为升序。
此问题按照官方的说法是:绕过标记为弃用的路径中的预期列引用验证。
在这里我们先输入一个不存在的字段名name4,查看一下是怎样一个流程。首先进入如下函数,判断order_by 的排序顺序和表达式。
1def add_ordering(self, *ordering):
2 """
3 Add items from the 'ordering' sequence to the query's "order by"
4 clause. These items are either field names (not column names) --
5 possibly with a direction prefix ('-' or '?') -- or OrderBy
6 expressions.
7 If 'ordering' is empty, clear all ordering from the query.
8 """
9 errors = []
10 for item in ordering:
11 if isinstance(item, str):
12 if '.' in item:
13 warnings.warn(
14 'Passing column raw column aliases to order_by() is '
15 'deprecated. Wrap %r in a RawSQL expression before '
16 'passing it to order_by().' % item,
17 category=RemovedInDjango40Warning,
18 stacklevel=3,
19 )
20 continue
21 if item == '?':
22 continue
23 if item.startswith('-'):
24 item = item[1:]
25 if item in self.annotations:
26 continue
27 if self.extra and item in self.extra:
28 continue
29 # names_to_path() validates the lookup. A descriptive
30 # FieldError will be raise if it's not.
31 self.names_to_path(item.split(LOOKUP_SEP), self.model._meta)
32 elif not hasattr(item, 'resolve_expression'):
33 errors.append(item)
34 if getattr(item, 'contains_aggregate', False):
35 raise FieldError(
36 'Using an aggregate in order_by() without also including '
37 'it in annotate() is not allowed: %s' % item
38 )
39 if errors:
40 raise FieldError('Invalid order_by arguments: %s' % errors)
41 if ordering:
42 self.order_by += ordering
43 else:
44 self.default_ordering = False
函数走到names_to_path的时候会根据传入的参数生成一个PathInfo 元组。返回最终的字段和没有找到的字段。其中opts代表模型选项,这里代表的这个表。然后去获取传入的字段值。当最后找不到这个字段的时候,会报一个Cannot resolve keyword '%s' into field的错误,也就是我们最后会看到的错误。
1def names_to_path(self, names, opts, allow_many=True, fail_on_missing=False):
2 path, names_with_path = [], []
3 for pos, name in enumerate(names):
4 cur_names_with_path = (name, [])
5 if name == 'pk':
6 name = opts.pk.name
7
8 field = None
9 filtered_relation = None
10 try:
11 field = opts.get_field(name)
12 except FieldDoesNotExist:
13 if name in self.annotation_select:
14 field = self.annotation_select[name].output_field
15 elif name in self._filtered_relations and pos == 0:
16 filtered_relation = self._filtered_relations[name]
17 field = opts.get_field(filtered_relation.relation_name)
18 if field is not None:
19 # Fields that contain one-to-many relations with a generic
20 # model (like a GenericForeignKey) cannot generate reverse
21 # relations and therefore cannot be used for reverse querying.
22 if field.is_relation and not field.related_model:
23 raise FieldError(
24 "Field %r does not generate an automatic reverse "
25 "relation and therefore cannot be used for reverse "
26 "querying. If it is a GenericForeignKey, consider "
27 "adding a GenericRelation." % name
28 )
29 try:
30 model = field.model._meta.concrete_model
31 except AttributeError:
32 # QuerySet.annotate() may introduce fields that aren't
33 # attached to a model.
34 model = None
35 else:
36 # We didn't find the current field, so move position back
37 # one step.
38 pos -= 1
39 if pos == -1 or fail_on_missing:
40 available = sorted([
41 *get_field_names_from_opts(opts),
42 *self.annotation_select,
43 *self._filtered_relations,
44 ])
45 raise FieldError("Cannot resolve keyword '%s' into field. "
46 "Choices are: %s" % (name, ", ".join(available)))
47 break
get_field函数的意思是返回一个字段名称的字段实例。对应的表内字段名和字段实例的字典类型。其中_forward_fields_map和fields_map的作用是相同的,就是后者还会检查一些内部的其他字段。
1def get_field(self, field_name):
2 """
3 Return a field instance given the name of a forward or reverse field.
4 """
5 try:
6 # In order to avoid premature loading of the relation tree
7 # (expensive) we prefer checking if the field is a forward field.
8 return self._forward_fields_map[field_name]
9 except KeyError:
10 # If the app registry is not ready, reverse fields are
11 # unavailable, therefore we throw a FieldDoesNotExist exception.
12 if not self.apps.models_ready:
13 raise FieldDoesNotExist(
14 "%s has no field named '%s'. The app cache isn't ready yet, "
15 "so if this is an auto-created related field, it won't "
16 "be available yet." % (self.object_name, field_name)
17 )
18 try:
19 # Retrieve field instance by name from cached or just-computed
20 # field map.
21 return self.fields_map[field_name]
22 except KeyError:
23 raise FieldDoesNotExist("%s has no field named '%s'" % (self.object_name, field_name))
最后都不存在的情况下会告知,User has no field named name4。
当然如果是存在的字段,比如name,程序从get_field获取到的field就是cve_orderby.User.name。也就是不管传入的参数是否正常,只要走了names_to_path最后都会返回不存在字段或者存在的字段实例对象,而不是拼接SQL去执行,那么至少在这里就不能造成SQL注入了。整个执行的代码都为:SELECT "cve_orderby_user"."id", "cve_orderby_user"."name" FROM "cve_orderby_user"。
在查了一堆资料发现这个问题其实是绕过names_to_path这个判断,在函数add_ordering中,主要有五个判断:
字段中是否带点,带的话提示传入的是原始列的别名,并警告不建议这么使用。
字段是否为问号。
字段开头是否为短横杠。
判断是否在一个map字典里,暂时也不知道是干啥的。
判断是否有额外的参数信息。
所以,此处我们传一个带点的参数,比如name.name。到add_ordering中的时候,走到这个函数上,由于存在continue的作用,将跳过后续的判断,也就是不在进行names_to_path,无法获取字段的实例对象。
后续进入_fetch_all的时候就已经生成SQL:SELECT "cve_orderby_user"."id", "cve_orderby_user"."name" FROM "cve_orderby_user" ORDER BY ("name".name) ASC。也就是把参数name.name拼接进去。
于是构造一条语句,注意这里使用的是MySQL数据库。构造:SELECT cve_orderby_user.id, cve_orderby_user.name FROM cve_orderby_user ORDER BY (cve_orderby_user.name);select updatexml(1,concat(0x7e,(select @@version)),1);#) ASC
只需要传输参数:cve_orderby_user.name);select updatexml(1,concat(0x7e,(select @@version)),1);#
原文作者:Misaki
原文链接:https://misakikata.github.io/2021/08/CVE-2021-35042-Django-SQL%E6%B3%A8%E5%85%A5/
发表日期:August 6th 2021, 4:16:55 pm
更新日期:August 6th 2021, 4:19:06 pm
版权声明:本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可
版权声明:著作权归作者所有。如有侵权请联系删除
战疫期间,开源聚合网络安全基础班、实战班线上全面开启,学网络安全技术、升职加薪……有兴趣的可以加入开源聚合网安大家庭,一起学习、一起成长,考证书求职加分、升级加薪,有兴趣的可以咨询客服小姐姐哦!
加QQ(1005989737)找小姐姐私聊哦
本文始发于微信公众号(开源聚合网络空间安全研究院):【漏洞分析】CVE-2021-35042 Django SQL注入
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论