所谓SQL注入,就是通过把SQL命令插入到Web表单或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意SQL命令的行为。具体来说,它是利用现有程序,将恶意SQL命令注入到后台数据库引擎执行的能力。它可以通过在Web表单中输入恶意的SQL语句得到一个存在安全漏洞的网站上的数据库,而不是按照设计者的意图去执行SQL语句。
一、注入原理
SQL注入攻击通过构建特殊的输入作为参数传入Web应用程序,而这些输入大都是SQL语法的一些组合,通过执行SQL语句进而执行攻击者的操作,其主要原因是程序没有细致地过滤用户输入地数据,致使非法数据侵入系统。
根据相关技术和原理,SQL注入可以分为平台层注入和代码层注入。前者由不安全的数据库配置或数据库平台的漏洞所致,后者主要是由于程序员对输入未进行细致的过滤,从而执行了非法的数据查询。基于此,SQL注入的产生原因通常表现在以下几方面:
(1)不当的类型处理;
(2)不安全的数据库配置;
(3)不合理的查询集处理;
(4)不当的错误处理;
(5)转义字符处理不合适;
(6)多个提交处理不当。
当应用程序使用输入内容来构造动态SQL语句访问数据库时,会发生SQL注入攻击。如果代码使用存储过程,而这些存储过程作为包含未筛选的用户输入的字符串来传递,也会发生SQL注入。
二、注入方法
以PHP + MySQL为例,构建一个Web网站中最基本的用户系统来演示SQL注入过程。
1. 创建一个名为demo的数据库:
CREATE DATABASE `demo` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
2. 创建一个名为user的数据表,并插入1条演示数据:
CREATE TABLE `demo`.`user` (
`uid` INT( 11 ) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '用户uid',
`username` VARCHAR( 20 ) NOT NULL COMMENT '用户名',
`password` VARCHAR( 32 ) NOT NULL COMMENT '用户密码'
) ENGINE = INNODB;
INSERT INTO `demo`.`user` (`uid`, `username`, `password`) VALUES ('1', 'plhwin', MD5('123456'));
实例一
通过传入username参数,在页面打印出会员的详细信息,编写userinfo.php程序代码:
<?php
header('Content-type:text/html; charset=UTF-8');
$username = isset($_GET['username']) ? $_GET['username'] : '';
$userinfo = array();
if($username){
// 使用mysqli驱动连接demo数据库
$mysqli = new mysqli("localhost", "root", "root", 'demo');
$sql = "SELECT uid,username FROM user WHERE username='{$username}'";
// mysqli multi_query 支持执行多条MySQL语句
$query = $mysqli->multi_query($sql);
if($query){
do {
$result = $mysqli->store_result();
while($row = $result->fetch_assoc()){
$userinfo[] = $row;
}
if(!$mysqli->more_results()){
break;
}
} while ($mysqli->next_result());
}
}
echo '<pre>',print_r($userinfo, 1),'</pre>';
上面这个程序实现的功能是根据浏览器传入的用户名参数,在页面上打印出这个用户的详细信息。该程序使用mysqli驱动,以便能使用到multi_query方法来支持同时执行多条SQL语句,这样能更好地说明SQL注入攻击的危害性。
假设我们可以通过 http://localhost/test/userinfo.php?username=plhwin 这个URL来访问到具体某个会员的详情,正常情况下,如果浏览器里传入的username是合法的,那么SQL语句会执行:
SELECT uid, username FROM user WHERE username='plhwin'
但是,如果用户在浏览器里把传入的username参数变为 plhwin';SHOW TABLES-- hack,也就是当URL变为 http://localhost/test/userinfo.php?username=plhwin';SHOW TABLES-- hack的时候,此时我们程序实际执行的SQL语句变成了
SELECT uid, username FROM user WHERE username='plhwin'; SHOW TABLES-- hack'
在MySQL中,最后两个连续的减号表示忽略减号后面的语句,经过上面的SQL注入后,原本想要执行查询会员详情的SQL语句,此时还额外执行了SHOW TABLES语句,这显然不是开发者的本意,此时可以看到浏览器页面中的输出:
Array
(
[0] => Array
(
[uid] => 1
[username] => plhwin
)
[1] => Array
(
[Tables_in_demo] => user
)
)
此时能清楚地看出,除了会员的信息,数据库表的名字user也被打印在页面上,如果黑客此时将参数换成plhwin'; DROP TABLE user-- hack,就将产生灾难性的后果,当你在浏览器中执行 http://localhost/test/userinfo.php?username=plhwin';DROP TABLE user-- hack 这个URL后,你会发现整个 user 数据表都消失不见了。
通过上面的例子,我们已经认识到SQL注入攻击的危害性,但是仍然会有人心存疑问,MySQL默认驱动的mysql_query方法现在已经不支持多条语句同时执行了,大部分开发者怎么可能像上面的演示程序那样又麻烦又不安全。
是的,在PHP程序中,MySQL是不允许在一个mysql_query中使用分号执行多SQL语句的,这使得很多开发者都认为MySQL本身就不允许多语句执行了,但实际上MySQL早在4.1版本就允许多语句执行,通过PHP的源代码,我们发现其实只是PHP语言自身限制了这种用法。
实例二
如果系统不允许同时执行多条SQL语句,那么SQL注入攻击是不是就不再这么可怕呢?答案是否定的,我们仍然以上面的user数据表,用Web网站中常用的会员登录系统来做另外一个场景实例,编写程序login.php,代码如下:
<?php
if($_POST){
$link = mysql_connect("localhost", "root", "root");
mysql_select_db('demo', $link);
$username = empty($_POST['username']) ? '' : $_POST['username'];
$password = empty($_POST['password']) ? '' : $_POST['password'];
$md5password = md5($password);
$sql = "SELECT uid,username FROM user WHERE username='{$username}' AND password='{$md5password}'";
$query = mysql_query($sql, $link);
$userinfo = mysql_fetch_array($query, MYSQL_ASSOC);
if(!empty($userinfo)){
// 登录成功,打印出会员信息
echo '<pre>',print_r($userinfo, 1),'</pre>';
} else {
echo "用户名不存在或密码错误!";
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Web登录系统SQL注入实例</title>
</head>
<body>
<form name="LOGIN_FORM" method="post" action="">
登录帐号: <input type="text" name="username" value="" size=30 /><br /><br />
登录密码: <input type="text" name="password" value="" size=30 /><br /><br />
<input type="submit" value="登录" />
</form>
</body>
</html>
此时如果输入正确的用户名 plhwin 和密码 123456,执行的SQL语句为:
SELECT uid,username FROM user WHERE username='plhwin' AND password='e10adc3949ba59abbe56e057f20f883e'
上面语句没有任何问题,可以看到页面打印出了登录成功后的会员信息,但如果攻击者输入的用户名为 plhwin' AND 1=1-- hack,密码随意输入,比如 aaaaaa,那么拼接之后的SQL查询语句就变成了如下内容:
SELECT uid,username FROM user WHERE username='plhwin' AND 1=1-- hack' AND password='0b4e7a0e5fe84ad35fb5f95b9ceeac79'
执行上面的SQL语句,因为 1=1 是永远成立的条件,这意味着黑客只需要知道别人的会员名,无需知道密码就能顺利登录系统。
三、如何确定是否存在SQL注入漏洞
通过以上实例,我们仍然会有疑问:黑客并不知道我们程序代码的逻辑和SQL语句的写法,他是如何确定一个网站是否存在SQL注入漏洞呢?一般说来有以下2种途径:
1. 错误提示
如果网站开启了错误提示,攻击者就可以通过反复调整发送的参数,查看页面打印的错误信息,推测出网站使用的数据库和开发语言等重要信息。
2. 盲注
除非运维人员疏忽,否则大部分的网站都应该关闭了错误提示信息,此时攻击者一般会采用盲注的技巧来进行反复的尝试判断。仍然以上面的数据表user为例,我们之前的查看会员详情页面的url地址为 userinfo.php?username=plhwin,此时黑客分别访问 userinfo.php?username=plhwin' AND 1=1-- hack 和 userinfo.php?username=plhwin' AND 1=2-- hack,如果前者访问能返回正常的信息而后者不能,就基本可以判断此网站存在SQL注入漏洞,因为后者的 1=2 这个表达式永远不成立,所以即使username传入了正确的参数也无法通过,由此可以推断这个页面存在SQL注入漏洞,并且可以通过username参数进行注入。
四、防御措施
SQL注入并不是一个在SQL内不可解决的问题,总体来说,没有(运行时)编译,就没有注入。SQL注入产生的原因就是未经检查或未经充分检查的用户输入数据,意外变成了代码被执行。也就是用户输入的数据,在拼接SQL语句的过程中超越了数据本身,成为了SQL语句查询逻辑的一部分。
想要从根本上防止SQL注入,就要避免数据变成代码被执行,时刻分清数据和代码的界限。具体到SQL注入来说,被执行的恶意代码是通过数据库的SQL引擎编译得到的,所以只要避免用户输入的数据被数据库系统编译就可以了。
现在的数据库系统都提供SQL语句的预编译和查询参数绑定功能。在SQL语句中放置占位符"?",然后将带有占位符的SQL语句传给数据库编译,执行的时候才将用户输入的数据作为执行的参数传给用户。这样的操作不仅使得SQL语句在书写的时候不再需要拼接,看起来也更直接,而且用户输入的数据也没有机会被送到数据库的SQL解释器被编译执行,也不会越权变成代码。
以Java操作数据库为例,举几个规避SQL注入风险的实例:
1. 使用JDBC时,避免对SQL语句进行拼接
使用statement的executeQuery、execute、executeUpdate等函数时,避免直接将来自外部的不可信参数拼接到SQL语句中。
// 错误方法
// itemName是外部读入的参数拼接到SQL语句
String sqlString = "SELECT * FROM t_item WHERE owner='" + userName + "' AND itemName='" + request.getParameter("itemName") + "'";
stmt = connection.createStatement();
rs = stmt.executeQuery(sqlString);
正确方法:(1)使用预编译方式;(2)对拼接到SQL语句中的外部参数进行白名单校验
// 使用白名单校验方式校验itemName
String itemName=getCleanedItemName(request.getParameter("itemName"));
String sqlString = "SELECT * FROM t_item WHERE owner='" + userName + "' AND itemName='" + itemName + "'";
stmt = connection.createStatement();
rs = stmt.executeQuery(sqlString);
使用connection的PreparedStatement时,避免拼接来自外部的不可信参数。
正确方法:(1)将拼接方式改为占位符方式;(2)对拼接到SQL语句中的外部参数进行白名单校验
// 传递参数使用占位符
String itemName = request.getParameter("itemName");
String sqlString = "SELECT * FROM t_item WHERE owner=? AND itemName=?";
PreparedStatement stmt = connection.prepareStatement(sqlString);
stmt.setString(1, userName);
stmt.setString(2, itemName);
rs = stmt.executeQuery();
2. 在存储过程中避免使用动态方式构建SQL语句
// 错误方法
REATE PROCEDURE sp_queryItem
@userName varchar(50),
@itemName varchar(50)
AS
BEGIN
DECLARE @sql nvarchar(500);
SET @sql = 'SELECT * FROM t_item
WHERE owner = ''' + @userName + '''
AND itemName = ''' + @itemName + '''';
EXEC(@sql);
END
GO
正确方法:采用参数化查询的方式
CREATE PROCEDURE sp_queryItem
@userName varchar(50),
@itemName varchar(50)
AS
BEGIN
SELECT * FROM t_item
WHERE userName = @userName
AND itemName = @itemName;
END
GO
总体来说,防范SQL注入应该遵循以下几点:
1. 永远不要信任用户的输入,对用户的输入进行校验,可以通过正则表达式,或限制长度,或对单引号和双"-"进行转换等。
2. 永远不要动态拼接SQL语句,可以使用参数化的SQL或者直接使用存储过程进行数据查询和存储。
3. 永远不要使用管理员权限的数据库连接, 为每个应用使用单独的权限连接数据库。
4. 不要直接存放机密信息,要将敏感信息加密后再进行存储。
5. 应用的异常信息应该给出尽可能少的提示,最好使用自定义的错误信息对原始错误信息进行包装。