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注入漏洞我们可以使用这种方式:

  1. 执行一个正常查询,比如输入hr,系统正常返回用户名包含关键字hr的查询结果
  2. 构造查询hr' and 1=1#,系统仍然正常返回用户名包含关键字hr的查询结果
  3. 构造查询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注入等问题,有关这些内容将在其它章节详细介绍。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。