什么是SQL注入
SQL注入漏洞为PHP研发人员所熟知,它是所有漏洞类型中危害最严重的漏洞之一。SQL注入漏洞,主要是通过伪造客户端请求,把SQL命令提交到服务端进行非法请求的操作,最终达到欺骗服务器从而执行恶意的SQL命令。
研发人员在处理应用程序和数据库交互时,未对用户可控参数进行严格的校验防范,例如使用字符串拼接的方式构造SQL语句在数据库中进行执行,很容易埋下安全隐患。
SQL注入可以造成数据库信息泄露,特别是数据库中存放的用户隐私信息的泄露。通过操作数据库对特定网页进行篡改,修改数据库一些字段的值,嵌入恶意链接,进行挂马攻击,传播恶意软件。服务器还容易被远程控制,被安装后门,经由数据库服务器提供的操作系统支持,让攻击者得以修改或控制操作系统以及破坏硬盘数据,瘫痪全系统。
目前常见的SQL注入的攻击方式有报错注入、普通注入、隐式类型注入、盲注、宽字节注入、二次解码注入。下面对每一种注入威胁举例说明,以帮助您在编码过程中有效地避免漏洞的产生。
为了能更直观地了解SQL注入,先在数据库中创建一个名叫hacker的用户表。下面是数据表的结构,示例都是通过这个表结构来说明的。
CREATE TABLE 'hacker'('id' int(10) NOT NULL AUTO_INCREMENT,'name' varchar(255) DEFAULT NULL,'email' varchar(255) DEFAULT NULL,'password' varchar(255) DEFAULT NULL,'status' tinyint(1) DEFAULT NULL,PRIMARY KEY('ID')) ENGINE=InnoDB DEFAULT CHARSET=utf8;
下面的一段PHP代码,主要功能是在数据库中通过用户名查询用户的具体信息。通过这段代码,来介绍SQL注入以及它对系统的危害。
<?php
$username=$_GET['username'];
$conn=mysql_connect("localhost","root","root") or die("数据库连接失败");
mysql_select_db('hacker','$conn');
$sql="select * from hacker where name='{$username}'";
$result=mysql_query($sql);
while($row=mysql_fetch_array($result)) {
echo "username:".$row['name']."<br>";
echo "email:".$row['email']."<br>";
}
mysql_close($conn);
?>
报错注入
报错注入是指恶意攻击者用特殊的方式使数据库发生错误并产生报错信息,从而获得数据库和系统信息,方便攻击者进行下一步攻击。
需要注意,在研发过程中,如果传入查询参数且没有对参数进行严格处理,通常会造成SQL报错注入。
select * from hacker where name='{$username}'
如果对$username传入参数hacker'attack,完整请求http://localhost:8080/mysql.php?name=hacker'attack,查询语句将变成以下形式。
select * from hacker where name = 'hacker'attack'
这可以导致数据库报错,攻击者就可以通过这种方式获取MySQL的各类信息,从而对系统进行下一步的攻击和破坏。
为了防止报错信息被攻击者直接看到,网站上线后需要设置display_errors=Off。
普通注入
下面的示例是普通注入。攻击者在地址栏输入下面带有部分SQL语句的请求。
http://localhost:8080/mysql.php?name=name' OR 'a'='a
从而输入任何参数都可以满足查询条件,使其变成一个万能查询语句。同样,可以使用UNION和多语句进行查询,获取数据库的全部信息。
完整请求URL:
http://localhost:8080/mysql.php?name=name' OR 'a'='a'into outfile '/tmp/backup.sql
数据库当前表中的数据将被全部备份在/tmp/backup.sql文件中。当攻击者再利用其他漏洞找到下载方式,将文件下载或者复制走,最终造成被拖库时,Web站点的数据就会全部暴露。
如果执行下面请求,将发生更可怕的事情。
http://localhost:8080/mysql.php?name=name';DELETE FROM hacker;SELECT * FROM username WHERE 'a'='a
执行上面的请求后,在原有的SQL语句后面拼接了name';DELETE FROM hacker;SELECT * FROM username WHERE 'a'='a,查询语句变成了以下形式。
select * from hacker where name='name';DELETE FROM username ;SELECT *FROM name WHERE 'a'='a'
数据库里的数据被攻击者完全删除。如果没有提前对数据进行备份,这对于系统造成的伤害将是毁灭性的。
隐式类型注入
以数据表结构为例,编写以下查询语句。
select * from hacker where email=0;
该查询语句的作用是通过email查询相应的用户信息,由于将email的值误写为0,在执行结果中可以看到数据库返回了表中的所有内容。
为什么会这样呢?因为在MySQL中执行SQL查询时,如果SQL语句中字段的数据类型和对应表中字段的数据类型不一致,MySQL查询优化器会将数据的类型进行隐式转换。列出了SQL执行过程中MySQL变量类型转换规则,在研发过程中需要注意它的影响。
通过表中的转换关系可以看出,在上面的查询语句中,MySQL将数据类型转换为DOUBLE后进行查询,由于STRING转换后的值为0,同时查询条件中的值也为0,所以匹配到了整张表的内容。
盲注
报错注入和普通注入显而易见,盲注有时容易被忽略。
在页面无返回的情况下,攻击者也可以通过延时等技术实现发现和利用注入漏洞。
select * from hacker where if(MID(version(),1,1) LIKE 5,sleep(5),1) limit 0,1;
判断数据库版本,执行成功,浏览器返回会延时并利用BENCHMARK()函数进行延时注入。
(IF(MID(version(),1,1)LIKE 5, BENCHMARK(100000,SHA1('true')), false))
该请求会使MySQL的查询睡眠5秒,攻击者可以通过添加判断条件到SQL语句中,如果睡眠了5秒,那么说明MySQL版本为5,否则不是。通过盲注可以掌握数据库和服务器的相关信息,为攻击提供便利。
宽字节注入
要触发宽字节注入,有一个前提条件,即数据库和程序编码都是GBK的。下面的示例代码以GBK编码格式保存。
<?php
$conn=mysql_connect('localhost','root','123456') or die("数据库连接失败");
mysql_querry("SET NAMES 'gbk'"); // GBK编码
mysql_select_db('safe',$conn);
$id=isset($_GET['id']) ?addslashes($_GET['id']) : 1;
$sql="SELECT * FROM author WHERE id='{$id}'";
$result=mysql_query($sql,$conn) or die(mysql_error()); // sql出错会报错,方便观察
$row=mysql_fetch_array($result,MYSQL_ASSOC);
print_r($row);
mysql_free_result($result);
?>
在这个SQL语句前面,使用了一个addslashes()函数,将$id的值进行转义处理。只要输入参数中有单引号,就逃逸不出限制,无法进行SQL注入,具体如下。
http://localhost:8080/mysql.php?id=1
http://localhost:8080/mysql.php?id=1'
上面两个请求都通过了addslashes,不会引起SQL注入。要实现注入就要逃过addslashes的限制,addslashes()函数的作用是让“'”变成”\'”,进行了转义。攻击者一般的绕过方式就是想办法处理“\'”前面的“\”。
PHP在使用GBK编码的时候,会认为两个字符是一个汉字。当输入的第一个字符的ASCII码大于128时,看看会发生什么情况,例如输入“%81'”。
MySQL报告出现语法SQL错误,原因是多输入了一个引号,然而前面的反斜杠不见了,一旦出现数据库报错,就说明可以进行SQL注入了。
原因是GBK是多字节编码,PHP认为两个字节代表一个汉字,所以%81和后面的反斜杠%5c变成了一个汉字“乗”,造成反斜杠消失。
二次解码注入
通常情况下,为了防止SQL注入的发生,采取转义特殊字符,例如对用户输入的单引号(')、双引号(")进行转义变成“\'\"”。有一个误区就是通过配置PHP的GPC开关进行自动转义。
当攻击者将参数二次编码时,PHP的自动转义将无法识别用户的恶意输入。
用前面的URL,来构造如下新的请求。
http://localhost:8080/mysql.php?name=name%2527
当PHP接收到请求时会自动进行一次URL解码,变为name%27,然后代码里又使用urldecode()函数或rawurldecode()函数进行解码,%27变成了单引号,URL最终变成name=name'引发SQL注入。