SQL注入攻击

      所谓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.  应用的异常信息应该给出尽可能少的提示,最好使用自定义的错误信息对原始错误信息进行包装。


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值