SQL注入
SQL注入是一类比较古老的安全漏洞,它主要发生在服务端程序直接使用字符串拼接SQL查询语句时,没有对用户输入进行校验,此时攻击者构造特定的查询字符串,改变了原SQL语句的语义,达到未经授权的查询数据或是数据库表元信息,甚至直接下载整个数据库的目的。
SQL注入在早期ASP、PHP等语言开发的网站中广泛存在,但由于该漏洞过于常见而且危害性较大,随着开发人员的安全意识提高,现在已经极少能找到了,此外预编译SQL语句和ORM框架的广泛使用也基本杜绝了SQL注入漏洞。然而凡事没有绝对,一些特殊功能或是写法如果开发人员疏忽大意,可能依然存在SQL注入漏洞。
这篇笔记我们介绍一些最简单的SQL注入原理和漏洞利用方式。
SQL注入代码示例
下面PHP页面中是一个用户查询功能,它包含了一个表单和一个表格,我们在表单中输入查询信息,查询结果会返回在表格中。
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>用户查询</title>
</head>
<body>
<form action="/user_list.php" method="post">
<input type="text" name="username">
<input type="submit" value="用户查询">
</form>
<table>
<tr>
<th>用户ID</th>
<th>用户名</th>
<th>最后登录时间</th>
</tr>
<?php
if (isset($_POST["username"])) {
$username = $_POST["username"];
$server_name = "localhost";
$server_username = "root";
$server_password = "root";
$db_name = "netstore";
$conn = new mysqli($server_name, $server_username, $server_password, $db_name);
if ($conn->connect_error) {
die("连接数据库失败" . $conn->connect_error);
}
$result = $conn->query("select user_id,username,last_login_time from t_user where username like '%$username%'");
if ($result->num_rows > 0) {
while ($row_data = $result->fetch_assoc()) {
echo "<tr><td>" . $row_data["user_id"] . "</td><td>" . $row_data["username"] . "</td><td>" . $row_data["last_login_time"] . "</td></tr>";
}
}
$conn->close();
}
?>
</table>
</body>
</html>

代码中,我们使用mysqli连接MySQL数据库并发送查询语句,然而我们拼接查询语句的方式存在漏洞,通过构造特定的查询字符串,我们可以改变原SQL语义,执行非法操作。
SQL注入原理
例如我们构造输入' or 1=1#,此时SQL语句变为select user_id,username,last_login_time from t_user where username like '' or 1=1#',我们构造的输入中的单引号'提前截止了一个where查询参数,而后面的井号#将后续的语句屏蔽为了注释,or 1=1则让该查询语句的where条件直接失效,此时我们跳过了原来的查询语义,而是直接获取表中的所有数据。

SQL注入漏洞判定
判定该PHP页面存在SQL注入漏洞我们可以使用这种方式:
- 执行一个正常查询,比如输入
hr,系统正常返回用户名包含关键字hr的查询结果 - 构造查询
hr' and 1=1#,系统仍然正常返回用户名包含关键字hr的查询结果 - 构造查询
hr' and 1=2#,系统没有返回任何结果
此时可以判断该查询接口存在SQL注入漏洞。
SQL注入漏洞利用
SQL注入漏洞的危害远不止前面查询一个表中所有数据这种程度,通过巧妙的拼接SQL语句,我们可能窃取系统的登录信息、下载整个数据库(俗称脱裤)、甚至拿到数据库服务器的用户名、密码,下面是一些示例。
构造输入abc' union select user(),database(),''#,经过SQL拼接后,我们可以在数据库中执行user()函数和database()函数,这两个函数可以输出当前连接数据库的用户名和当前的数据库名。

图中可以看到,我们的用户名是root,数据库名为netstore。
构造输入abc' union select table_name,'','' from information_schema.tables tbs where tbs.table_schema='netstore'#,该查询语句能够查询MySQL的元数据表information_schema.tables,我们在其中查询了当前netstore数据库的所有表名。

按照此原理我们也可以在元数据表中查询所有的表结构、表数据,达到“脱裤”的目的。
自动化SQL注入
前面我们是手动进行的SQL注入攻击,手动注入是比较繁琐和反直觉的,实际上我们可以直接使用sqlmap工具进行自动化的SQL注入攻击。有关sqlmap工具的使用方法将在信息安全/渗透测试工具/sqlmap-自动SQL注入工具章节进行介绍。
防御SQL注入
防御SQL注入非常简单,我们使用预编译SQL语句preparedStatment即可杜绝大部分SQL注入。这里我们将前面的代码稍加修改即可:
$prepared_stmt = $conn->prepare("select user_id,username,last_login_time from t_user where username like ?");
$prepared_stmt->bind_param("s", $username);
$prepared_stmt->execute();
$result = $prepared_stmt->get_result();
此时前面的SQL注入攻击便都失效了。有关PHP如何使用预编译SQL语句,可以参考PHP开发相关章节。
通过SQL注入举一反三
基于SQL注入的原理,实际上我们还可以发现,直接执行用户输入或只是简单的将用户输入拼接到要执行的命令中,这是个很危险的行为。类似SQL注入,我们很容易想到命令注入、XML注入、XPath注入等问题,有关这些内容将在其它章节详细介绍。