PDO下的Sql注入

PHP数据对象(PDO)扩展为PHP访问数据库定义了一个轻量级的一致接口。PDO提供了一个数据访问抽象层,这意味着,不管使用哪种数据库,都可以使用相同的函数(方法)来查询和获取数据。PDO随PHP 5.1发行,在PHP 5.0的PECL扩展中也可以使用,无法运行于之前的PHP版本。

PDO多语句执行

PHP连接MySQL数据库有三种方式(MySQL、Mysqli、PDO),同时官方对三者也做了列表性比较:

Mysqli PDO MySQL
引入的PHP版本 5.0 5.0 3.0之前
PHP5.x是否包含
服务端prepare语句的支持情况
客户端prepare语句的支持情况
存储过程支持情况
多语句执行支持情况 大多数

Mysqli通过multi_query()函数来进行多语句执行。
PDO使用query()就可以进行多语句执行。

PDO默认支持多语句查询,如果php版本小于5.5.21或者创建PDO实例时未设置PDO::MYSQL_ATTR_MULTI_STATEMENTS为false时可能会造成堆叠注入.

如果想禁止多语句执行,可在创建PDO实例时将PDO::MYSQL_ATTR_MULTI_STATEMENTS设置为false

1
new PDO($dsn, $user, $pass, array( PDO::MYSQL_ATTR_MULTI_STATEMENTS => false))

PDO预处理

Mysql预处理

MySQL数据库支持预处理,预处理或者说是可传参的语句用来高效的执行重复的语句。

MySQL官方将prepareexecutedeallocate统称为PREPARE STATEMENT

预制语句的SQL语法基于三个SQL语句:

1
2
3
prepare stmt_name from preparable_stmt;
execute stmt_name [using @var_name [, @var_name] ...];
{deallocate | drop} prepare stmt_name;

PDO预处理

PDO分为模拟预处理和非模拟预处理。

  • 模拟预处理是防止某些数据库不支持预处理而设置的.
    在初始化PDO驱动时,可以设置一项参数,PDO::ATTR_EMULATE_PREPARES,作用是打开模拟预处理(true)或者关闭(false),默认为true。
    PDO内部会模拟参数绑定的过程,SQL语句是在最后execute()的时候才发送给数据库执行。

    一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <?php
    $dbms='mysql';
    $host='192.168.27.61';
    $dbName='test';
    $user='root';
    $pass='root';
    $dsn="$dbms:host=$host;dbname=$dbName";
    try {
    $pdo = new PDO($dsn, $user, $pass, array( PDO::MYSQL_ATTR_MULTI_STATEMENTS => false));
    } catch (PDOException $e) {
    echo $e;
    }
    $username = $_GET['username'];
    $sql = "select * from user where username = ?";
    $stmt = $pdo->prepare($sql);
    $stmt->bindParam(1,$username);
    $stmt->execute();
    while($row=$stmt->fetch(PDO::FETCH_ASSOC))
    {
    var_dump($row);
    echo "<br>";
    }
  • 非模拟预处理则是通过数据库服务器来进行预处理动作,主要分为两步:
    第一步是prepare阶段,发送SQL语句模板到数据库服务器;
    第二步通过execute()函数发送占位符参数给数据库服务器进行执行。

    非模拟预处理代码,在$username = $_GET['username'];代码前增加$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <?php
    $dbms='mysql';
    $host='192.168.27.61';
    $dbName='test';
    $user='root';
    $pass='root';
    $dsn="$dbms:host=$host;dbname=$dbName";
    try {
    $pdo = new PDO($dsn, $user, $pass, array( PDO::MYSQL_ATTR_MULTI_STATEMENTS => false));
    } catch (PDOException $e) {
    echo $e;
    }
    $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); //首先给MySQL服务器发送SQL语句模板,然后通过EXECUTE发送占位符参数给服务器
    $username = $_GET['username'];
    $sql = "select * from user where username = ?";
    $stmt = $pdo->prepare($sql);
    $stmt->bindParam(1,$username);
    $stmt->execute();
    while($row=$stmt->fetch(PDO::FETCH_ASSOC))
    {
    var_dump($row);
    echo "<br>";
    }

安全问题

  • 在模拟预处理下,如果SQL语句模板可控,可以控制sql语句的结构,达到多语句执行的效果。
  • 在非模拟预处理下,即使SQL语句可控多语句也不可执行。
  • 当设置$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);时,可利用MySQL服务端prepare时报错,然后通过设置PDO::ATTR_ERRMODE将MySQL错误信息打印达到报错注入效果。

PDO下的宽字节注入

模拟预编译的请求发送方式和以往的mysqli并没有什么特殊区别,不同的是,在原有的查询语句中对参数并没有用单引号包裹,而在此却用单引号进行了包裹,与此同时,会将传入的单引号加上\转义。
如果在此时设置了GBK编码,就可能产生宽字节注入的风险。

实际上,在模拟预编译的情况下,PDO对于SQL注入的防范(PDO::queto()),无非就是将数字型的注入转变为字符型的注入,又用类似mysql_real_escape_string()的方法将单引号、双引号、反斜杠等字符进行了转义。

例如:

1
2
3
4
5
$query = "select * from table1 where 1=?";
$db->query("set names gbk");
$row = $db->prepare($query);
$row->bindParam(1,$_GET['id']);
$row->execute();

在上面的例子中,我们可以传入id=%df' union select xxx实现注入。

在非模拟预编译的情况下,上述问题将不会存在,它对每一句sql语句都进行了预编译和执行两个操作,在执行select * from table1 where 1=?这句时,如果是GBK编码,那么它将会把?绑定的参数转化成16进制,这样无论输入什么样的东西都无法再进行注入了。

使用Prepare Statement方法彻底防止sql注入

PDO的原理,与Mysql中prepare语句是一样的。上面PDO所执行的SQL语句,用如下的方式可以等效替代:

1
2
3
Set @x=0x31;
Prepare a from "select balabala from table1 where 1=?";
Execute a using @x;

我们可以手动将输入的参数设置为@x,并将其转化为16进制,随后预编译,再执行

也就是说,不用PDO也可以仿照其原理手动设置预编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$db = new mysqli('localhost','root','','pdotest');
if(isset($_GET['id']))
{
$id = "0x".bin2hex($_GET['id']);
}
else
{
$id=1;
}
echo "id:".$id."</br>";
$db->query("set names gbk");
$db->query("set @x={$id}");
$db->query("prepare a from 'select * from table1 where 1=?'");
$row = $db->query("execute a using @x");
$result = $row->fetch_assoc();
if($result)
{
echo "结果为:";
print_r($result);
echo "</br>";
}

摘自

https://www.freebuf.com/articles/web/216336.html

https://xz.aliyun.com/t/3950

文章作者: Dar1in9
文章链接: http://dar1in9s.github.io/2020/02/25/web安全/PDO下的Sql注入/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Dar1in9's Blog