SQL注入:使用预编译防御SQL注入时产生的问题

目录

前言

模拟预编译

真正的预编译

预编译中存在的SQL注入

宽字节

没有进行参数绑定

无法预编译的位置


前言

相信学习过SQL注入的小伙伴都知道防御SQL注入最好的方法,就是使用预编译也就是PDO是可以非常好的防御SQL注入的,但是如果错误的设置了PDO,即使是预编译也存在注入的可能,那么下面我会参考大佬的文章来学习+复现一下如何正确的使用预编译防御SQL注入

首先是第一个问题,为什么预编译或者说参数化查询可以防止sql注入呢?

使用参数化查询数据库服务器不会把参数的内容当作 sql 指令的一部分来执行,是在数据库完成 sql 指令的编译后才套用参数运行。

简单的说: 参数化能防注入的原因在于,语句是语句,参数是参数,参数的值并不是语句的一部分,数据库只按语句的语义跑 。

SQL注入产生的原因是因为服务器错误把用户的输入当作了执行的语句

假设有一个sql语句是这样的:

select username from test where id = $_POST[id]

如果用户正常输入1,语句则为:

select username from test where id = 1

那么显然查询出来的就只会是test表中id为1的那个username,然而如果用户不按照开发者期待的南阳,输入的是1 union select version(),那么语句就变为了:

select username from test where id = 1 union select version()

最后查询出来的就会使id=1的那个username以及数据库的版本,这是因为本来理论上查询的应该是id为”1 union select version()”的这个用户,而数据库执行语句的时候把它分开了,视作了查询select username from test where id = 1以及select version()。

预编译的原理:如果源码这里提前对$_POST[id]进行了处理,那么数据库相当于会提前对整个语句进行编译,把它编译成select username from test where id = 用户输入

因此整个语句的功能已经提前定死了,就是查询id = 用户输入的username,不再会像之前一样错误理解成查询id=1的用户然后再查询版本,这样看来预编译的作用,就是消除了sql语句的歧义。

那么回看最初我们提出的疑问,预编译真的能完美防御sql注入吗?有没有什么奇技淫巧能绕过预编译进行注入呢?

有一篇大佬的文章分析过:预编译真的能完美防御SQL注入吗?

这里面提到一个很有趣的点——预编译是将sql语句参数化,刚刚的例子中 where语句中的内容是被参数化的。这就是说,预编译仅仅只能防御住可参数化位置的sql注入。那么,对于不可参数化的位置,预编译将没有任何办法。

那么哪些是不可参数化的位置呢,原作者说:

img

为了研究原理,大佬又找到了一篇文章,这个应该是最早提出order by后没法参数化所以可以被sql注入的 SQL预编译中order by后为什么不能参数化原因,文章里是这么解释的

大概就是说,order by后面的字段是不能加引号的,而预编译后会自动加上引号,因为这个矛盾所以order by的后面不能进行预编译。

不过当时他解释原因是因为自动加引号的setString()方法,而这个方法似乎只是java下存在的,而这篇文章是从原理出发研究研究php下的注入可能(其实这种思路不同语言是共通的)

模拟预编译

后端页面代码:

<?php
$username = $_POST['username']; // 接收username
# 建立数据库连接
header("Content-Type:text/html;charset=utf-8");
$dbs = "mysql:host=127.0.0.1;dbname=sort";
$dbname = "root";
$passwd = "root";
// 创建连接,选择数据库,检测连接
try{
    $conn = new PDO($dbs, $dbname, $passwd);
    echo "连接成功<br/>";
}
catch (PDOException $e){
    die ("Error!: " . $e->getMessage() . "<br/>");
}
# 设置预编译语句,绑定参数,这里使用命名占位符
$stmt = $conn->prepare("select fraction from fraction where name = :username");
$stmt->bindParam(":username",$username);
$stmt->execute();
if($fraction = $stmt->fetch(PDO::FETCH_ASSOC)){
    echo '查询成功';
    echo '<br/>';
    echo '学生:'.$username;
    echo '<br/>';
    # echo '分数:'.$fraction;
    print_r("分数".$fraction[fraction]);
}
else{
}
$conn=null; # 关闭链接
?>

执行查询name="mechoy",查看数据库日志:

27 Connect  root@localhost on sort using TCP/IP         # 建立连接
27 Query    select fraction from fraction where name = 'mechoy' # 执行查询
27 Quit                                 # 结束

从日志来看,没有prepare和execute,只是执行了一个查询的SQL语句,并没有进行预编译。

显然,PDO默认情况下使用的是模拟预编译。

模拟预编译是防止某些数据库不支持预编译而设置的(如sqllite与低版本MySQL)。

如果模拟预处理开启,那么客户端程序内部会模拟MySQL数据库中的参数绑定这一过程。

也就是说,程序会在内部模拟prepare的过程,当执行execute时,再将拼接后的完整SQL语句发送给MySQL数据库执行。

而想要真正使用预编译,首先需要数据库支持预编译,再在代码中加入

$conn -> setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
# bool PDO::setAttribute ( int $attribute , mixed  $value ) 设置数据库句柄属性。
# PDO::ATTR_EMULATE_PREPARES 启用或禁用预处理语句的模拟。 有些驱动不支持或有限度地支持本地预处理。使用此设置强制PDO总是模拟预处理语句(如果为 TRUE  ),或试着使用本地预处理语句(如果为 FALSE )。如果驱动不能成功预处理当前查询,它将总是回到模拟预处理语句上。需要 bool  类型。 
#这里在PHP5.2.17时无效,暂未找到原因
#更改版本为PHP5.6.9时生效

再执行查询name="mechoy",查看数据库日志:

4 Connect   root@localhost on sort using TCP/IP
4 Prepare   select fraction from fraction where name = ?
4 Execute   select fraction from fraction where name = 'mechoy'
4 Close     stmt    
4 Quit
# 可以看到当PDO::ATTR_EMULATE_PREPARES设置为false时,取消了模拟预处理,采用本地预处理

也可以使用下列这种方式:
后端代码:

<?php
$username = $_POST['username'];

$db = new PDO("mysql:host=localhost;dbname=test", "root", "root");

$stmt = $db->prepare("SELECT password FROM test where username= :username");

$stmt->bindParam(':username', $username);

$stmt->execute();

$result = $stmt->fetchAll(PDO::FETCH_ASSOC);

var_dump($result);

$db = null;

?>

 然后我们使用Firefox浏览器POST方式提交一个数据:
  

不出意外的查出了值,我们去日志看看预编译对我们传入的值做了什么处理:

2023-10-22T12:59:55.149736Z     5 Connect   root@localhost on test using TCP/IP
2023-10-22T12:59:55.149993Z     5 Query SELECT password FROM test where username= 'root'
2023-10-22T12:59:55.150987Z     5 Quit

只有connect query 然后就quit,你可能会奇怪,我们不是绑定了参数然后预编译了吗,怎么感觉和正常的sql语句逻辑差不多呢,我们再post一个’root’试试:

这次竟然啥也没查出来,到底是怎么回事!我们去日志看看:

2023-10-22T13:12:13.619712Z     9 Connect   root@localhost on test using TCP/IP
2023-10-22T13:12:13.619960Z     9 Query SELECT password FROM test where username= '\'admin\''
2023-10-22T13:12:13.620931Z     9 Quit  

这次你肯定恍然大悟了,为什么默认的预编译模式模拟预编译被称作虚假的预编译,因为他在sql执行的过程中其实根本没有参数绑定、预编译的过程,本质上只是对符号做了过滤

比如假如我们输入注入语句root’ union select database()#,日志里的数据为:

2023-10-22T15:34:50.356115Z    11 Connect   root@localhost on test using TCP/IP
2023-10-22T15:34:50.356353Z    11 Query SELECT password FROM test where username= 'admin\' union select database()#'
2023-10-22T15:34:50.357303Z    11 Quit  

那为什么开发者要做一个虚假的预编译呢,那是因为一个参数——PDO::ATTR_EMULATE_PREPARES,这个选项用来配置PDO是否使用模拟预编译,默认是true,因此默认情况下PDO采用的是模拟预编译模式,设置成false以后,才会使用真正的预编译。

开启这个选项主要是用来兼容部分不支持预编译的数据库(如sqllite与低版本MySQL),对于模拟预编译,会由客户端程序内部参数绑定这一过程(而不是数据库),内部prepare之后再将拼接的sql语句发给数据库执行。

真正的预编译

使用下列代码就是使用的真正的预编译了

<?php
$username = $_POST['username'];

$db = new PDO("mysql:host=localhost;dbname=test", "root", "root");
$db -> setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

$stmt = $db->prepare("SELECT password FROM user where username= :username");

$stmt->bindParam(':username', $username);

$stmt->execute();

$result = $stmt->fetchAll(PDO::FETCH_ASSOC);

var_dump($result);

$db = null;

?>

我们再次使用上面的那种方式来使用POST查询一下:

 可以看到这里的执行结果是和上面的模拟预编译的结果是一样的,那么再来看看日志:

231018 23:51:17	   61 Connect	root@localhost on test
		   61 Prepare	SELECT password FROM test where username= ?
		   61 Execute	SELECT password FROM test where username= 'admin'

这时数据库中执行的顺序变成了:先连接,然后准备语句,用问号?占位,接着用输入替换问号?执行语句,专业点的说法叫做:

  1. 建立连接;

  2. 构建语法树;

  3. 执行

这也是为什么我们之前说的,预编译的作用是让整个语句的功能已经提前定死,消除了sql语句的歧义。

当我们输入username= ‘admin’同样会没有任何输出

那么再来看看日志:

我们看一下数据库的日志:

2023-10-22T15:49:30.089718Z    24 Connect   root@localhost on test using TCP/IP
2023-10-22T15:49:30.089986Z    24 Prepare   SELECT password FROM test where username= ?
2023-10-22T15:49:30.090041Z    24 Execute   SELECT password FROM test where username= '\'admin\''

这时我们再输入注入语句root' union select database()#

2023-10-22T15:43:23.500819Z    17 Connect   root@localhost on test using TCP/IP
2023-10-22T15:43:23.502097Z    17 Prepare   SELECT password FROM test where username= ?
2023-10-22T15:43:23.502165Z    17 Execute   SELECT password FROM test where username= 'admin\' union select database()#'
2023-10-22T15:43:23.502600Z    17 Close stmt    
2023-10-22T15:43:23.502627Z    17 Quit  

分析预编译的原理其实可以发现,预编译其实是为了提高MySQL的运行效率而诞生(而不是为了防止sql注入),因为它可以先构建语法树然后带入查询参数,避免了一次执行一次构建语法树的繁琐,对于数据量以及查询量较大的数据库能极大提高运行效率。

从原理出发,可以看出来有些方面预编译并不能完全阻止预编译。

预编译中存在的SQL注入

宽字节

宽字节注入出现的本质就是因为数据库的编码与代码的编码不同,导致用户可以通过输入精心构造的数据通过编码转换吞掉转义字符。

看我们刚刚sql语句的执行日志可以发现对于模拟预编译理论上是存在宽字节注入的,因为它只是本地对执行的sql语句进行一次模拟的预编译然后就把语句发给数据库执行去了,而且只是使用了\来进行转义,如果我们能有什么办法吞掉这个\,那是不是我们就可以执行恶意的sql语句了呢

后端代码如果为:

<?php
$username = $_POST['username'];

$db = new PDO("mysql:host=localhost;dbname=test;charset=gbk", "root", "root");

// $db -> setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

$db->query('SET NAMES GBK');

$stmt = $db->prepare("SELECT password FROM test where username= :username");

$stmt->bindParam(':username', $username);

$stmt->execute();

$result = $stmt->fetchAll(PDO::FETCH_ASSOC);

var_dump($result);

$db = null;

?>

然后我们输入一下内容就可以利用宽字节绕过预处理实现注入

username=admin%df%27%20union%20select%20database();#

通过抓包可以看到这里确实将转义字符闭合为一个特殊字符了

这个语句在navicat里是能正常执行的,但我并没有在网页上获得输出,是因为预编译只会输出第一条语句,因此后面union的执行结果无法输出

这里如果使用真预编译就不会出现上面的这种问题

因此相比于模拟预编译,真编译的安全性大的多,现在可能的几种针对预编译的注入方法也都是在模拟预编译下实现的。

没有进行参数绑定

没有参数绑定的预编译等于没有预编译,无论是真编译还是模拟预编译,没有参数绑定等于没编译,并且由于DPO默认支持堆叠注入,我们可以通过堆叠注入先插入值然后查询插入的值获取输出结果。

后端代码:

<?php
$id = $_POST['id'];

$dbs = "mysql:host=localhost;dbname=test";
$dbname = "root";
$passwd = "root";

$conn = new PDO($dbs, $dbname, $passwd);

# 预处理语句
$stmt = $conn->prepare("SELECT * FROM test where id= $id");
$conn -> setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);

var_dump($result);

$conn=null; # 关闭链接
?>

可以看到代码中的id没有进行参数绑定

那么我们可以尝试POST提交一下使用堆叠注入来将要查询的东西插入到user表中的两个字段中,然后在进行查询:

然后再POST传入的值:

然后访问该id:

就可以看到对应插入的值了

无法预编译的位置

上面提到过,order by的后面是没法预编译的,因此遇到可控排序功能一般一注一个准,下面我们来通过日志研究一下这到底是为什么

后端代码:

<?php
$col = $_POST['col'];

$dbs = "mysql:host=localhost;dbname=test";
$dbname = "root";
$passwd = "root";

$conn = new PDO($dbs, $dbname, $passwd);
$conn -> setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

# 预处理语句(这里会自动加上单引号)
$stmt = $conn->prepare("SELECT * FROM user order by :col");

$stmt->bindParam(':col', $col);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);

var_dump($result);

$conn=null; # 关闭链接
?>

假如我们想按照password进行排序,post一个col=password

我们可以看看日志:

2023-10-27T01:23:43.100087Z   187 Connect   root@localhost on test using TCP/IP
2023-10-27T01:23:43.100579Z   187 Query SELECT * FROM test order by 'password'
2023-10-27T01:23:43.101405Z   187 Quit  

可以看到它自动给我们传入的值password的加了引号,然而这其实是与我们的目标背道而驰的:

order by在底层查询过程中是直接把order by后面这个值进行利用然后排序,如果加上引号的话数据库会索引失败,查询结果其实等同于order by NULL或者order by TRUE,本质上是一条不合法的请求。

因此无论是order by还是group by,他们后面的参数都是不能带引号的,而预编译中参数绑定的过程会自动给它们带上引号,这就导致这些位置上的参数是不能被预编译的,因为它的执行结果是错误的。

所以渗透的时候遇到疑似排序的功能我们可以大胆的去尝试sql注入,一般都能成功。

总而言之就一个思路,不能加引号的位置就不能预编译。

这里我们就可以看出预编译很明显的缺陷,当然,我们也不能错怪预编译的设计者们,因为这玩意儿本来设计之初就不是给你防注入,是用来在大批量查询时减少语法树构造的,因此出现差错也是可以理解的,当然这种差错就给了黑客可乘之机。

参考链接:

奇安信攻防社区-SQL注入&预编译 (butian.net)

预编译与sql注入 – fushulingのblog

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/403253.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【C语言】socket 层到网络接口的驱动程序之间的函数调用过程

一、socket 层到网络接口的驱动程序之间的函数调用过程概述 在 Linux 操作系统中&#xff0c;socket 层到网络接口的驱动程序之间的函数调用过程相对复杂&#xff0c;涉及多个层次的交互。以下是一个简化的概述&#xff0c;描述数据从 socket 传递到硬件驱动&#xff0c;再到硬…

新书推荐:《分布式商业生态战略:未来数字商业新逻辑与企业数字化转型新策略》

近两年&#xff0c;商业经济环境的不确定性越来越明显&#xff0c;市场经济受到疫情、技术、政策等多方因素影响越来越难以预测&#xff0c;黑天鹅事件时有发生。在国内外经济方面&#xff0c;国际的地缘政治对商业经济产生着重大的影响&#xff0c;例如供应链中断&#xff0c;…

PostgreSQL 实体化视图的使用

上周的教程中&#xff0c;通过 DVD Rental Database 示例&#xff0c;让我们了解了在 PostgreSQL 中创建实体化视图的过程。正如我们所了解的&#xff0c;PostgreSQL 实体化视图提供了一种强大的机制&#xff0c;通过预计算和存储查询结果集为物理表来提高查询性能。接下来的内…

C#_扩展方法

简述&#xff1a; 扩展方法所属类必需是静态类&#xff08;类名依据规范通常为XXXExtension&#xff0c;XXX为被扩展类&#xff09;扩展方法必需是公有的静态方法扩展方法的首个参数由this修饰&#xff0c;参数类型为被扩展类型 示例&#xff1a; static class DoubleExtens…

vue实现拖拽(vuedraggable)

实现效果: 左侧往右侧拖动&#xff0c;右侧列表可以进行拖拽排序。 安装引用&#xff1a; npm install vuedraggable import draggable from vuedraggable 使用&#xff1a; data数据&#xff1a; componentList: [{groupName: 考试题型,children: [{componentType: danxua…

【基础】提高前端的增益

低噪声&#xff0c;低偏移电压&#xff0c;低漂移-当你把信号链前端的增益提高后&#xff0c;所有的这些精密小信号处理的目标变得很简单。 这是一个很简单的概念。如图1所示&#xff0c;第二级的误差将除以第一级的增益。比如&#xff0c;第一级增益适度&#xff0c;值为10&a…

制造业客户数据安全解决方案(终端安全/文件加密/介质管理等)

针对前文制造业客户数据安全解决方案&#xff08;数据防泄密需求分析&#xff09;提到的泄密风险&#xff0c;本文详细介绍一套完整、合理的解决方案&#xff0c;通过该方案构建公司数据安全防护边界&#xff0c;自动加密、全方位保护数据安全。 PC端&#xff1a;https://isite…

Qt开发:MAC安装qt、qtcreate(配置桌面应用开发环境)

安装qt-creator brew install qt-creator安装qt brew install qt查看qt安装路径 brew info qtzhbbindembp ~ % brew info qt > qt: stable 6.6.1 (bottled), HEAD Cross-platform application and UI framework https://www.qt.io/ /opt/homebrew/Cellar/qt/6…

创建一个基于Node.js的实时聊天应用

在当今数字化社会&#xff0c;实时通讯已成为人们生活中不可或缺的一部分。无论是在社交媒体平台上与朋友交流&#xff0c;还是在工作场合中与同事协作&#xff0c;实时聊天应用都扮演着重要角色。与此同时&#xff0c;Node.js作为一种流行的后端技术&#xff0c;为开发者提供了…

10大数据恢复软件可以帮助您恢复电脑数据

您可能会非常紧张&#xff0c;因为知道有人意外地从您的硬盘驱动器中删除了您的宝贵数据&#xff08;甚至使用 ShiftDelete 从回收站中删除&#xff09;&#xff0c;并且您确实需要这些数据&#xff0c;并且没有其他备份源可以在其中找到这些数据。不要担心&#xff0c;保持冷静…

B² NETWORK空投

空投要点 众多大机构支持&#xff0c;是为数不多的有 Bitcoin 主网验证 Rollup 解决方案的 BTC Layer2&#xff0c;提前埋伏其实是普通人抢早期筹码最好的方式&#xff0c;参加 B Buzz 就是手握金铲子&#xff0c;对标eth二层网络的繁荣程度你就能想象这个前景明牌空投5%给早期…

Kotlin 进阶 学习 委托

1.接口委托 package com.jmj.jetpackcomposecompositionlocal.byStudy/*** 接口委托*/ interface HomeDao{fun getAllData():List<String> }interface ADao{fun getById(id:Int):String }class HomeDaoImpl:HomeDao{override fun getAllData(): List<String> {ret…

useRef有什么用?

看一下官网定义 useRef是一个React Hook&#xff0c;它能帮助引用一个不需要渲染的值 这句话透露出一个信息&#xff0c;不需要渲染的值可以用useRef引用&#xff0c;那需要渲染的值用什么引用呢&#xff1f;当然是useState了&#xff0c;需要渲染的值指的就是状态嘛&#xff0…

基于YOLOv8/YOLOv7/YOLOv6/YOLOv5的玉米病虫害检测系统(Python+PySide6界面+训练代码)

摘要&#xff1a;本文介绍了一种基于深度学习的玉米病虫害检测系统系统的代码&#xff0c;采用最先进的YOLOv8算法并对比YOLOv7、YOLOv6、YOLOv5等算法的结果&#xff0c;能够准确识别图像、视频、实时视频流以及批量文件中的玉米病虫害。文章详细解释了YOLOv8算法的原理&#…

防御保护--防病毒网关AV

目录 病毒 防病毒处理流量 防病毒的配置 防病毒&#xff08;AV&#xff09; --- 传统的AV防病毒的方式是对文件进行查杀。 传统的防病毒的方式是通过将文件缓存之后&#xff0c;再进行特征库的比对&#xff0c;完成检测。但是&#xff0c;因为需 要缓存文件&#xff0c;则将…

Swift Combine 使用调试器调试管道 从入门到精通二十六

Combine 系列 Swift Combine 从入门到精通一Swift Combine 发布者订阅者操作者 从入门到精通二Swift Combine 管道 从入门到精通三Swift Combine 发布者publisher的生命周期 从入门到精通四Swift Combine 操作符operations和Subjects发布者的生命周期 从入门到精通五Swift Com…

【数据结构初阶 6】二叉树:堆的基本操作 + 堆排序的实现

文章目录 &#x1f308; Ⅰ 二叉树的顺序结构&#x1f308; Ⅱ 堆的概念与性质&#x1f308; Ⅲ 堆的基本操作01. 堆的定义02. 初始化堆03. 堆的销毁04. 堆的插入05. 向上调整堆06. 堆的创建07. 获取堆顶数据08. 堆的删除09. 向下调整堆10. 判断堆空 &#x1f308; Ⅳ 堆的基本…

Spring之AOP源码解析(上)

Aop相关注解 EnableTransactionManagementEnableAspectJAutoProxyEnableAsync... 从注解切入来看看这些注解都干了什么 Import注解作用简述 注入的类一般继承ImportSelector或者ImportBeanDefinitionRegistrar接口 继承ImportSelector接口&#xff1a;selectImports方法返回…

2.22学习总结

1.营救 2.租用游艇 3.砍树 4.买礼物 5.刷题统计 砍树https://www.dotcpp.com/oj/problem3157.html 题目描述 给定一棵由 n 个结点组成的树以及 m 个不重复的无序数对 (a1, b1), (a2, b2), . . . , (am, bm)&#xff0c;其中 ai 互不相同&#xff0c;bi 互不相同&#xff0c;ai…

MYSQL-入门

一.安装和连接 1.1 安装 mysql安装教程&#xff1a; 2021MySql-8.0.26安装详细教程&#xff08;保姆级&#xff09;_2021mysql-8.0.26安装详细教程(保姆级)_mysql8.0.26_ylb呀的博客-cs-CSDN博客 workbench安装&#xff1a; MySQL Workbench 安装及使用-CSDN博客 1.2 配…