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 test61 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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机设计大赛 深度学习动物识别 - 卷积神经网络 机器视觉 图像识别

文章目录 0 前言1 背景2 算法原理2.1 动物识别方法概况2.2 常用的网络模型2.2.1 B-CNN2.2.2 SSD 3 SSD动物目标检测流程4 实现效果5 部分相关代码5.1 数据预处理5.2 构建卷积神经网络5.3 tensorflow计算图可视化5.4 网络模型训练5.5 对猫狗图像进行2分类 6 最后 0 前言 &#…

Python自动化UI测试之Selenium基础实操

1. Selenium简介 Selenium 是一个用于 Web 应用程序测试的工具。最初是为网站自动化测试而开发的&#xff0c;可以直接运行在浏览器上&#xff0c;支持的浏览器包括 IE&#xff08;7, 8, 9, 10, 11&#xff09;&#xff0c;Mozilla Firefox&#xff0c;Safari&#xff0c;Googl…

SVN忽略已提交的文件(ignore,移出版本控制)

本文适用于已安装TortoiseSVN客户端的同学。 1、右键点击要忽略的文件夹或文件&#xff0c;鼠标移到“TortoiseSVN”&#xff0c;找到“Unversion and add to ignore list”&#xff0c;选择文件夹&#xff0c;弹出提示框确认忽略。 2、设置完忽略文件后&#xff0c;还需要做…

多维时序 | Matlab实现GRU-MATT门控循环单元融合多头注意力多变量时间序列预测模型

多维时序 | Matlab实现GRU-MATT门控循环单元融合多头注意力多变量时间序列预测模型 目录 多维时序 | Matlab实现GRU-MATT门控循环单元融合多头注意力多变量时间序列预测模型预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.多维时序 | Matlab实现GRU-MATT门控循环单元融…

【Maven】介绍、下载及安装、集成IDEA

目录 一、什么是Maven Maven的作用 Maven模型 Maven仓库 二、下载及安装 三、IDEA集成Maven 1、POM配置详解 2、配置Maven环境 局部配置 全局设置 四、创建Maven项目 五、Maven坐标详解 六、导入Maven项目 方式1&#xff1a;使用Maven面板&#xff0c;快速导入项目 …

React Native框架开发介绍,以及其优点

大家好&#xff0c;我是咕噜铁蛋&#xff0c;在今天的文章中&#xff0c;我通过科技手段和大家一起探讨一下React Native框架的开发介绍以及其优点。我深知选择合适的开发工具对于项目的成功至关重要。而React Native作为一款流行的跨平台移动应用开发框架&#xff0c;其独特之…

【服务器数据恢复】FreeNAS+ESXi虚拟机数据恢复案例

服务器数据恢复环境&#xff1a; 一台服务器通过FreeNAS&#xff08;本案例使用的是UFS2文件系统&#xff09;实现iSCSI存储&#xff0c;整个UFS2文件系统作为一个文件挂载到ESXi虚拟化系统&#xff08;安装在另外2台服务器上&#xff09;上。该虚拟化系统一共有5台虚拟机&…

2024水科技大会暨技术装备成果展览会——高品质供水和饮用水水源安全保障论坛

供水与饮水安全直接关系到人民群众的生活与健康&#xff0c;切实做好城市供水与饮水安全保障工作&#xff0c;是把以人为本真正落到实处的一项紧迫任务。近年来&#xff0c;中央和地方加大了城乡供水与饮水安全保障工作的力度&#xff0c;对标最优质供水城市建设要求&#xff0…

[Angular 基础] - service 服务

[Angular 基础] - service 服务 之前的笔记就列举三个好了……没想到 Angular 东西这么多(&#xff70; &#xff70;;)……全加感觉越来越凑字数了 [Angular 基础] - 视图封装 & 局部引用 & 父子组件中内容传递 [Angular 基础] - 生命周期函数 [Angular 基础] - 自…

请简述你对SpringMVC的理解

SpringMVC是一种基于Java语言开发&#xff0c;实现了WebMVC设计模式&#xff0c;请求驱动类型 的轻量级Web框架。 采用了MVC架构模式的思想&#xff0c;通过把Model&#xff0c;View&#xff0c;Controller分离&#xff0c;将Web层进 行职责解耦&#xff0c;从而把复杂的Web应…

STM32控制数码管从0显示到99

首先 先画电路图吧&#xff01;打开proteus&#xff0c;导入相关器件&#xff0c;绘制电路图。如下&#xff1a;&#xff08;记得要保存啊&#xff01;发现模拟一遍程序就自动退出了&#xff0c;有bug&#xff0c;我是解决不了&#xff0c;所以就是要及时保存&#xff0c;自己重…

计算机组成原理(10)----微程序控制器

目录 1.微程序控制器的设计思想 2.微指令的基本格式 3.微程序控制器的基本结构 &#xff08;1&#xff09;控制存储器CM &#xff08;2&#xff09;CMAR &#xff08;3&#xff09;地址译码 &#xff08;4&#xff09;CMDR &#xff08;5&#xff09;微地址形成部件 &…

31.云原生Istio可观测性之官网Bookinfo应用实战演示

云原生专栏大纲 文章目录 可观测性kiali介绍Overview&#xff08;概观&#xff09;Application&#xff08;应用维度&#xff09;workloads&#xff08;负载维度&#xff09;Services&#xff08;服务维度&#xff09;Istio Config&#xff08;配置维度&#xff09; Kiali部署…

音频声波的主观感受

一、响度 声压是“客观”的&#xff0c;响度是“主观”的。 响度又称音量。人耳感受到的声音强弱&#xff0c;它是人对声音大小的一个主观感觉量。响度的大小决定于声音接收处的波幅&#xff0c;就同一声源来说&#xff0c;波幅传播的愈远&#xff0c;响度愈小…

React18原理: React核心对象之Update、UpdateQueue、Hook、Task对象

Update 与 UpdateQueue 对象 1 ) 概述 在fiber对象中有一个属性 fiber.updateQueue是一个链式队列&#xff08;即使用链表实现的队列存储结构&#xff09;是和页面更新有关的 2 &#xff09;Update对象相关的数据结构 // https://github.com/facebook/react/blob/v18.2.0/pa…

VSCode The preLaunchTask ‘C/C++: clang++ 生成活动文件‘ terminated with exit code -1

更改tasks.json文件里面的type为shell 选择g 选择g&#xff0c;然后点回到text.c&#xff0c;按下F5. 得到结果。 文中内容参考: 从零开始手把手教你配置属于你的VS Code_哔哩哔哩_bilibili https://blog.csdn.net/qq_63872647/article/details/128006861

【EasyV】QGIS转换至EasyV

QGIS转换至EasyV 第一步&#xff1a;导入QGIS第二步 坐标系转换第三步 集合修正第四步 重命名字段第五步 导出WGS geojson坐标第六步 导入EasyV 第一步&#xff1a;导入QGIS 第二步 坐标系转换 第三步 集合修正 第四步 重命名字段 第五步 导出WGS geojson坐标 第六步 导入EasyV…

【vue vue-seamless-scroll】解决vue-seamless-scroll鼠标悬浮才滚动或者只滚动一次就失效的问题

解决问题&#xff1a;使用vue-seamless-scroll发现只有鼠标悬浮上去才滚动&#xff0c;而且滚动一次停止了 目标效果&#xff1a; 解决方案&#xff1a; 最后发现是因为数据需要在页面挂载好就赋值&#xff0c;否则页面在加载完成后&#xff0c;数据无法自动滚动。但因为数据…

c++:蓝桥杯的基础算法2(构造,模拟)+练习巩固

目录 构造 构造的基础概念&#xff1a; 模拟 练习1&#xff1a;扫雷 练习2&#xff1a;灌溉 练习3&#xff1a;回文日期 构造 构造的基础概念&#xff1a; 构造算法是一种用于解决特定问题的算法设计方法。在C语言中&#xff0c;构造算法通常涉及到创建一个函数或类来实…

ARM服务器上部署zookeeper集群

由于ARM服务器上部署zookeeper集群,会存在加载不到主类问题,现在把遇到的问题进行总结下,问题如下: [rootnode206 apache-zookeeper-3.5.10]# bin/zkServer.sh start ZooKeeper JMX enabled by default Using config: /data1/software/apache-zookeeper-3.5.10/bin/../conf/…