SQL注入攻击与防御实例

  • 1.1

以下是一段普普通通的登录演示代码,该脚本需要username和password两个参数,该脚本中sql语句没有任何过滤,注入起来非常容易,后续部分将逐步加强代码的防注入功能。

<?php
include 'config.php';
$username = $_POST['username'];
$password = $_POST['password'];
if(!empty($username) && !empty($password))
{
    $conn = new mysqli($db_server,$db_user,$db_pass,$db_name);
    if(!$conn)
        die('数据库连接失败!<br/>');
    $sql = "select * from users where username='{$username}' and password='{$password}'limit 1";
    $result = $conn->query($sql);
    if($result->num_rows==1){
        echo "<script>alert(\"登录成功!\")</script>";
    }
    else{
        echo "<script>alert(\"用户名或密码错误!\")</script>";
    }
}
else{
    header("Location:/index.php?display=1");
}
?>

针对上面的代码进行sql注入的例子:

username='or''='
password='or''='
如果这样的话SQL语句就变成了select * from users where username=''or''=' and password=''or''='' limit 1,显然条件是个永真式,查询一定成功。
或者
username='or''=' limit 1#
password=任意非空值
SQL语句可以自己写一下。

除了上述的payload,还有很多其他的payload可用。

  • 1.2

如何将上述代码加强一下呢?上述代码在进行查询时同时查询了username和password,查询时用户能操作的参数越多,不确定性就越大。可以换一种思路,查询时拼接的字符串只用到主键username,后面在检查password和数据库中的是否一致。即,可以调整查询的结构,减少用户可控的参数拼接

数据库中密码明文不太好,顺便md5处理一下,加盐效果更好,可以防止数据库被黑了导致敏感信息泄漏。

$password = md5($_POST['password']);
if(!empty($username) && !empty($password))
{
  $conn = new mysqli($db_server,$db_user,$db_pass,$db_name);
  if(!$conn)
    die('数据库连接失败!<br/>');
  $sql = "select * from users where username='{$username}' limit 1";
  $result = $conn->query($sql);
  if($result->num_rows==1){
    $row = mysqli_fetch_assoc($result);
    if($row['password']==$password)
      echo "<script>alert(\"登录成功!\")</script>";
    else
      echo "<script>alert(\"用户名或密码错误!\")</script>";
  }
  else{
    echo "<script>alert(\"用户名或密码错误!\")</script>";
  }
}

这样做的话如果继续用username='or''='显然是不可以了,除非你知道数据库中第一个用户的密码。但是毕竟还是可以破解,因此可以在借助过滤函数来帮忙。在这个例子中,由于username参数两侧是单引号,如果构造sql注入一定需要加入额外的单引号来破坏原语句,所以可以直接借助addslashes()函数将username中的单引号转义。

$username = addslashes($_POST['username']);
$password = md5($_POST['password']);

在这个最简单的例子中,经过这样简单的修改似乎已经没有办法注入了。后面会给一些其他的例子,并给出一些新方法来防御sql注入。

  • 1.3

之前提到了过滤函数,用到的是PHP自带的转义函数,但是这个有时候是不够用的。这种情况下可以自定义过滤函数。

常见的过滤手段就是限制关键字,通过正则实现。

以下是节选的某CTF赛题中的一段代码,CTF中经常使用留有余地的过滤函数,让选手可以进行SQL注入。

if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
    $msg = '';
    $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
    $user_name = $_POST["user_name"];
    $phone = $_POST["phone"];
    if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){ 
        $msg = 'no sql inject!';
    }else{
        $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
        $fetch = $db->query($sql);
    }

    if (isset($fetch) && $fetch->num_rows>0){
        $row = $fetch->fetch_assoc();
        if(!$row) {
            echo 'error';
            print_r($db->error);
            exit;
        }
        $msg = "<p>姓名:".$row['user_name']."</p><p>, 电话:".$row['phone']."</p><p>, 地址:".$row['address']."</p>";
    } else {
        $msg = "未找到订单!";
    }
}else {
    $msg = "信息不全";
}
?>

该段代码中限制了select,insert等很多关键字,对防止SQL注入有一定效果,但是有缺陷。如果考虑的不太全还是会被注入,过滤函数设置的对关键词过于敏感会让很多正常信息的查询也变得不易。

$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';

关于该题目的注入可参考以下文章,这里用到了二次注入:

https://www.cnblogs.com/kevinbruce656/p/11347127.html#4355106

  • 1.4

之前的各种方法都比较麻烦,对程序员不友好,有一种比较简单的方法就是预编译,既能有效的防止SQL注入,又容易编写。

预编译能防止SQL注入是因为SQL语句在执行前经过编译后,数据库将以参数化的形式进行查询,当运行时动态地把参数传给预处理语句时,即使参数里有敏感字符如 'or''='数据库也会将其作为一个字段的属性值来处理而不会作为一个SQL指令。

总结一下,SQL注入的核心就是构造SQL指令,预编译破坏了这个条件,因此能防止SQL注入。

举个例子

<?php
include 'config.php';
$username = $_POST['username'];
$password = md5($_POST['password']);
if(!empty($username) && !empty($password))
{
    $conn = new mysqli($db_server,$db_user,$db_pass,$db_name);
    if(!$conn)
        die('数据库连接失败!<br/>');
    $sql = "select * from users where username=? limit 1";
    $result = $conn->prepare($sql);
    $result->bind_param('s',$username);
    $result->bind_result($users,$pass);
    $result->execute();
    if($result->fetch()){
        if($pass==$password)
            echo "<script>alert(\"登录成功!\")</script>";
        else
            echo "<script>alert(\"用户名或密码错误!\")</script>";
    }
    else{
        echo "<script>alert(\"用户名或密码错误!\")</script>";
    }
    $conn->close();
}
else{
    header("Location:/index.php?display=1");
}
?>

以下是比较核心的几行

$sql = "select * from users where username=? limit 1";
$result = $conn->prepare($sql);
$result->bind_param('s',$username);
$result->bind_result($users,$pass);
$result->execute();

第一行是一个SQL语句,?处需要被填充。

第二行是对SQL语句进行预编译。

第三行是限制填充的类型为字符串,使用username变量来填充SQL语句。

第四行是确定查询结果存储到哪些变量中。

第五行是执行,执行完毕将会获得结果。

使用预编译的方式防止SQL语句简单有效,暂时没有发现防不住的情况,建议使用。

我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2k2m12zcg6nq