一、关于SQL注入
众所周知,SQL注入漏洞是一种常见的Web安全漏洞,其形成原因是服务器没有对用户输入的内容进行严格过滤,导致该内容拼接到服务器原本的SQL语句中,被当作SQL语句的一部分执行。
以基于MySQL数据库的开源靶场sqli-labs的第一关(Less-1)为例,我们查看Less-1页面(index.php)的PHP源码:
发现服务器端关键的查询语句是这样两句:
$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当成是字符型变量,字符串需要用单引号包裹)的数据项的全部字段,并将其中的一些字段的值回显在页面上,如下图所示:
此时,页面显示了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,此时页面显示当前登录数据库的用户名(此为敏感信息),如图所示:
上述就是通过控制输入构造payload,引发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注入的根本原因。
仍然以sqli-labs的第一关(Less-1)为例,Less-1的index.php原始代码如下:
//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()--+
情形二:靶机利用预编译技术进行防御,index.php原始代码更改为:
//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()--+
此时会发现,union select、user()等语句未成功执行,注入不成功。究其原因,靶机的预编译SQL语句将用户输入的内容(即-1' union select 1, 2, user()--+)当成了普通的参数(值),而不是可执行的SQL语句(或SQL语句的一部分)。
本期作者袁泉:深信服安全服务认证专家(SCSE-S),产业教育中心资深讲师,暨南大学网络空间学院校外实践指导老师;曾任职于国防科技大学信息通信学院,从事计算机网络、信息安全专业教学和科研工作十余年,持有HCNA(SECURITY)和HCNA (R&S)证书;熟悉 TCP/IP 协议及网络安全防护体系架构,具有丰富的计算机网络管理、运维与安防实践经验。