利用预编译技术防御SQL注入

一、关于SQL注入

众所周知,SQL注入漏洞是一种常见的Web安全漏洞,其形成原因是服务器没有对用户输入的内容进行严格过滤,导致该内容拼接到服务器原本的SQL语句中,被当作SQL语句的一部分执行。

以基于MySQL数据库的开源靶场sqli-labs的第一关(Less-1)为例,我们查看Less-1页面(index.php)的PHP源码:

利用预编译技术防御SQL注入_第1张图片

发现服务器端关键的查询语句是这样两句:

$id=$_GET['id']

SELECT * FROM users WHERE id='$id' LIMIT 0, 1

按照此逻辑,服务器会从URL中获取动态参数id的值,将其赋值给$id这个变量,并将其直接代入到SELECT这条查询语句中去查询资源(即从users表中查询id='$id'的数据项的全部字段),没有对用户输入的id值进行过滤!

正常情况下,如果前端的请求是:

http://.../sqli-labs/Less-1/?id=1

那么,后端的查询语句应该就成了:

SELECT * FROM users WHERE id='1' LIMIT 0, 1

服务器会从users表中查询id='1'(此时数据库把id当成是字符型变量,字符串需要用单引号包裹)的数据项的全部字段,并将其中的一些字段的值回显在页面上,如下图所示:

利用预编译技术防御SQL注入_第2张图片

此时,页面显示了id=1的用户和账号名和密码。

然而,这里的动态参数id对于用户来说是可控的。如果用户精心构造输入,他把前端的请求设置成:

http://.../sqli-labs/Less-1/?id=-1' union select 1, 2, user()--+

那么,后端的查询语句应该就会变成:

SELECT * FROM users WHERE id='-1' union select 1, 2, user()-- ' LIMIT 0,1

由URL中代入进来的加号“+”会被变成空格,而“-- ”(两个减号,后面紧跟一个空格)在SQL语句中表示注释,那么上述查询语句实际上可以简化为:

SELECT * FROM users WHERE id='-1' union select 1, 2, user()

注意看这条查询语句,用户通过精心构造输入(即-1' union select 1, 2, user()--+),使输入的内容代入到原本的SQL语句中并且改变了原本SQL语句的结构——前半句SELECT * FROM users WHERE id='-1',由于数据库中没有id='-1'的数据项,此半句运行结果为FALSE,页面不会显示任何内容;后半句union select 1, 2, user(),表示联合查询当前的用户名(user()是PHP的一个函数,用于显示当前登录数据库的用户名),运行结果为True,此时页面显示当前登录数据库的用户名(此为敏感信息),如图所示:

利用预编译技术防御SQL注入_第3张图片

上述就是通过控制输入构造payload,引发SQL注入攻击的过程。总结一下,那就是攻击者通过精心构造输入,让输入的内容拼接到服务器原本的SQL语句中,改变了原有SQL语句的结构而导致注入攻击。

二、关于SQL语句预编译

(一)什么是预编译

预编译又称为预处理,顾名思义,就是为代码编译做的预备工作。预编译指令指示了在程序正式编译前就由编译器进行的操作,可以放在程序中的任何位置。

对于数据库来说,通常一条SQL语句从传入到执行经历了以下过程:(1)词法和语义解析优化;(2)制定执行计划;(3)执行并返回结果。这种普通语句称为Immediate Statements。但很多情况下,一条SQL语句可能会反复执行,或者每次执行的时候只有个别的参数值不同,比如:

SELECT username, password FROM users WHERE id=1;

SELECT username, password FROM users WHERE id=2;

这两个SQL语句由于id后的值不同,因此在词法和语义解析优化阶段不会匹配,不能得到重复使用。如果两条语法树相似的SQL语句都需要经过“词法语义解析优化、制定执行计划、执行并返回结果”这样一个过程,则很容易造成时间的浪费、效率的下降。

所谓预编译语句就是将这类语句中的值用占位符(“?”)替代,可以视为将SQL语句模板化或者参数化,即将SQL语句先交由数据库预处理,构建语法树,再传入真正的字段值多次执行,省却了重复解析和优化相同语法树的时间,提升了SQL执行的效率。一般这类语句称为Prepared Statements。

以MySQL为例,利用mysqli的预编译功能编写的核心PHP语句为:

//定义需要预编译的SQL语句,从外界传递的参数(输入)用占位符?表示

$sql = "SELECT FROM security.users WHERE id= ? LIMIT 0,1";

//创建预处理对象

$mysqli_stmt = $mysqli->prepare($sql);

//绑定参数

$mysqli_stmt->bind_param('i', $id);

//绑定结果集

$mysqli_stmt->bind_result($id, $username, $password);

//执行

$mysqli_stmt->execute();

预编译语句的优势在于:一次编译、多次运行,省去了解析优化等过程。

(二)为什么预编译能够防御SQL注入

上一节中我们说到,SQL注入漏洞产生的原因就是服务器对用户输入的内容没有严格过滤,攻击者通过精心构造输入,让输入的内容拼接到服务器原本的SQL语句中,改变了原有SQL语句的结构,而这个“新”的SQL语句代入到数据库中执行,产生了非预期的结果。

而在预编译的机制下,用户在向原有SQL语句传入输入值之前,原有SQL语句的语法树就已经构建完成,因此无论用户输入什么样的内容,都无法再更改语法树的结构。至此,任何输入的内容都只会被当做值来看待,不会再出现非预期的查询,这便是预编译能够防御SQL注入的根本原因。

三、实战演示

仍然以sqli-labs的第一关(Less-1)为例,Less-1的index.php原始代码如下:

Less-1 **Error Based- String**

Welcome Dhakkan

//including the Mysql connect parameters.

include("../sql-connections/sql-connect.php");

error_reporting(0);

// take the variables

if(isset($_GET['id']))

{

$id=$_GET['id'];

//logging the connection parameters to a file for analysis.

$fp=fopen('result.txt','a');

fwrite($fp,'ID:'.$id."\n");

fclose($fp);

// connectivity

$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";

$result=mysql_query($sql);

$row = mysql_fetch_array($result);

if($row)

{

echo "";

echo 'Your Login name:'. $row['username'];

echo "
";

echo 'Your Password:' .$row['password'];

echo "";

}

else

{

echo '';

print_r(mysql_error());

echo "";

}

}

else { echo "Please input the ID as parameter with numeric value";}

?>




情形一:靶机不做任何防御,此时在攻击机上使用以下payload即可注入成功(获取当前登录数据库的用户名):

http://[靶机IP]/sqli-labs/Less-1/?id=-1' union select 1, 2, user()--+

利用预编译技术防御SQL注入_第4张图片

情形二:靶机利用预编译技术进行防御,index.php原始代码更改为:

Less-1 **Error Based- String**

Welcome Dhakkan

//including the Mysql connect parameters.

$sql_server = "localhost";

$sql_username = "root";

$sql_password = "root";

$sql_database = "security";

$mysqli = new mysqli($sql_server, $sql_username, $sql_password, $sql_database);

error_reporting(0);

// take the variables

if(isset($_GET['id']))

{

$id=$_GET['id'];

//logging the connection parameters to a file for analysis.

$fp=fopen('result.txt','a');

fwrite($fp,'ID:'.$id."\n");

fclose($fp);

$sql="SELECT * FROM security.users WHERE id= ? LIMIT 0,1";

$mysqli_stmt = $mysqli->prepare($sql); //创建预处理对象

$mysqli_stmt->bind_param('i',$id); //绑定参数

$mysqli_stmt->bind_result($id,$username,$password); //绑定结果集

$mysqli_stmt->execute(); //执行

while($mysqli_stmt->fetch())

{

echo "";

echo 'Your Login name:' . $username;

echo "
";

echo 'Your Password:' . $password;

echo "";

}

}

else { echo "Please input the ID as parameter with numeric value";}

?>




仍然在攻击机上使用以下payload尝试注入:

http://[靶机IP]/sqli-labs/Less-1/?id=-1' union select 1, 2, user()--+

利用预编译技术防御SQL注入_第5张图片

此时会发现,union select、user()等语句未成功执行,注入不成功。究其原因,靶机的预编译SQL语句将用户输入的内容(即-1' union select 1, 2, user()--+)当成了普通的参数(值),而不是可执行的SQL语句(或SQL语句的一部分)。

本期作者袁泉:深信服安全服务认证专家(SCSE-S),产业教育中心资深讲师,暨南大学网络空间学院校外实践指导老师;曾任职于国防科技大学信息通信学院,从事计算机网络、信息安全专业教学和科研工作十余年,持有HCNA(SECURITY)和HCNA (R&S)证书;熟悉 TCP/IP 协议及网络安全防护体系架构,具有丰富的计算机网络管理、运维与安防实践经验。

你可能感兴趣的:(sql,数据库)