PDO预编译与sql注入

刚学web安全的时候学到sql注入防御,那些文章基本上都会说利用pdo预编译就可以近乎完美防御sql注入,或者看到一些渗透经验贴,遇到sql经过预编译的网站师傅们总是会建议赶紧换个站,那么预编译究竟能不能完美防御sql注入,或者说预编译下的sql注入有什么奇技淫巧吗?

首先是第一个问题,为什么预编译或者说参数化查询可以防止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后为什么不能参数化原因,文章里是这么解释的

img

大概就是说,order by后面的字段是不能加引号的,而预编译后会自动加上引号,因为这个矛盾所以order by的后面不能进行预编译。不过当时他解释原因是因为自动加引号的setString()方法,而这个方法似乎只是java下存在的,而这篇文章我准备从原理出发研究研究php下的注入可能(其实这种思路不同语言是共通的)

真预编译与假预编译

回到最初的问题——预编译真的能完美防御sql注入吗?有没有什么奇技淫巧能绕过预编译进行注入呢?

首先,我们开启数据库的日志功能,从数据库的角度看看预编译究竟对我们的sql语句做了什么处理。

show variables like 'general%';

img

general_log显示的是是否开启日志功能,general_log_file显示的是日志位置。如果是off的话可以使用set GLOBAL general_log = 1;开启日志功能(得root),反之使用set GLOBAL general_log = 0;关闭日志。

事实上数据库里有两种预编译,一种称作模拟预编译,另一种是真正的预编译,需要格外设置。(以下测试环境为php5.4.45+apache+mysql5.7.26,对于预编译正常的分析不同环境应该影响不大,需要注意环境的是后面绕过注入的部分)

虚假的预编译

默认的,或者说现在网上一般讲的预编译是这么写的:

<?php
$username = $_POST['username'];
​
$db = new PDO("mysql:host=localhost;dbname=test", "root", "root123");
​
$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;
​
?>

我们post一个username=root

img

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

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’试试:

img

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

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= '\'root\''
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= 'root\' union select database()#'
2023-10-22T15:34:50.357303Z    11 Quit  

那为什么开发者要做一个虚假的预编译呢,那是因为一个参数——PDO::ATTR_EMULATE_PREPARES,这个选项用来配置PDO是否使用模拟预编译,默认是true,因此默认情况下PDO采用的是模拟预编译模式,设置成false以后,才会使用真正的预编译。开启这个选项主要是用来兼容部分不支持预编译的数据库(如sqllite与低版本MySQL),对于模拟预编译,会由客户端程序内部参数绑定这一过程(而不是数据库),内部prepare之后再将拼接的sql语句发给数据库执行。

真正的预编译

我们在原先的代码上把ATTR_EMULATE_PREPARES设为false取消模拟预编译

<?php
$username = $_POST['username'];$db = new PDO("mysql:host=localhost;dbname=test", "root", "root123");
$db -> setAttribute(PDO::ATTR_EMULATE_PREPARES, false);$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;?>

我们post一个username=root

img

看看日志

img

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= 'root'

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

  1. 建立连接;

  2. 构建语法树;

  3. 执行

这也是为什么我们之前说的,预编译的作用是让整个语句的功能已经提前定死,消除了sql语句的歧义。当我们输入username= ‘root’同样会没有任何输出

img

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

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= '\'root\''

这时我们再输入注入语句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= 'root\' 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语句了呢

测试环境:php5.3.29+apache2.4.39+mysql5.7.26

<?php
$username = $_POST['username'];$db = new PDO("mysql:host=localhost;dbname=test;charset=gbk", "root", "root123");
$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;?>

当我们post

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

查看日志:

2023-10-26T00:51:20.931085Z	   14 Connect	root@localhost on test using TCP/IP
2023-10-26T00:51:20.931577Z	   14 Query	SET NAMES GBK
2023-10-26T00:51:20.931809Z	   14 Query	SELECT password FROM test where username= '1\運' union select database();#'
2023-10-26T00:51:20.933058Z	   14 Quit	

这个语句在navicat里是能正常执行的,但我并没有在网页上获得输出,可能是我版本不够低?我看的那篇文章里5.2.17是可以成功实现的

img

img

这里猜猜为什么真编译是不能吞掉\执行恶意语句呢,是因为提前绑定参数了吗?因为当我们设置编码之后,日志里查询参数都被hex了:

2023-10-26T01:20:47.891775Z	   23 Prepare	SELECT password FROM test where username= ?
2023-10-26T01:20:47.891842Z	   23 Execute	SELECT password FROM test where username= 0x31DF2720756E696F6E2073656C65637420646174616261736528293B23
2023-10-26T01:20:47.892337Z	   23 Close stmt	
2023-10-26T01:20:47.892379Z	   23 Quit	

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

没有参数绑定

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

<?php
$id = $_POST['id'];$dbs = "mysql:host=localhost;dbname=test";
$dbname = "root";
$passwd = "root123";$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; # 关闭链接
?>

我们可以post一个

id=1;insert into test(id,username,password) values(114514,database(),user())

接着post id=114514

img

可以看到我们成功获取了database()以及user()的输出结果,查看日志,可以发现数据库执行了两条语句

2023-10-27T01:06:09.232609Z	  173 Connect	root@localhost on test using TCP/IP
2023-10-27T01:06:09.232961Z	  173 Query	SELECT * FROM test where id= 1;
2023-10-27T01:06:09.233159Z	  173 Query	insert into test(id,username,password) values(114514,database(),user())
2023-10-27T01:06:09.233581Z	  173 Quit	

无法预编译的位置

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

<?php
$col = $_POST['col'];$dbs = "mysql:host=localhost;dbname=test";
$dbname = "root";
$passwd = "root123";$conn = new PDO($dbs, $dbname, $passwd);
$conn -> setAttribute(PDO::ATTR_EMULATE_PREPARES, false);# 预处理语句
$stmt = $conn->prepare("SELECT * FROM test order by :col");$stmt->bindParam(':col', $col);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);var_dump($result);$conn=null; # 关闭链接
?>

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

img

你可能觉得没什么问题,我们去日志看看

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的加了引号,然而这其实是与我们的目标背道而驰的:

img

img

img

order by在底层查询过程中是直接把order by后面这个值进行利用然后排序,如果加上引号的话数据库会索引失败,查询结果其实等同于order by NULL或者order by TRUE,本质上是一条不合法的请求。因此无论是order by还是group by,他们后面的参数都是不能带引号的,而预编译中参数绑定的过程会自动给它们带上引号,这就导致这些位置上的参数是不能被预编译的,因为它的执行结果是错误的。所以渗透的时候遇到疑似排序的功能我们可以大胆的去尝试sql注入,一般都能成功。

这里也补充一下order by后面以及group by 后面怎么注入,有报错回显的直接报错注入就行了,这个简单,没有报错的话我们可以通过构造布尔条件进行注入:

img

img

可以看到随rand()中值真假的不同,排序出来的结果也是不同的,因此可以通过这个特征进行布尔注入,比如输入rand(ascii(mid((select database()),1,1))>96),如果成立和不成立输出结果显然是不同的,如果我们成功注入,输出应该是root dingzhen admin的顺序

img

通过这种方法我们就可以盲注出想要的数据。

从这个思路我们其实就不难理解为什么有些位置不能被预编译,除了order by和group by还有吗?当然有,只要是加了引号会导致语句执行结果错误的位置都是不行的:

表名:

img

列名:

img

img

limit:

img

join:

img

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

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

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

相关文章

VBA基础知识点总结

VBA教程 VBScript教程 数据类型 数字数据类型 非数字数据类型 变量&常量 可以通过Dim、Public或Private语句声明变量。 变量语法&#xff1a;Dim <<variable_name>> As <<variable_type>>&#xff08;需要在使用它们之前声明&#xff09; 常量语…

LVS+Keepalived高可用集群部署

一、高可用群集(HA)的作用 企业应用中&#xff0c;单台服务器承担应用存在单点故障的危险&#xff0c;单点故障一旦发生&#xff0c;企业服务将发生中断&#xff0c;造成极大的危害。 高可用集群是由一台主调度器和一台或多台备用调度器。在主调度器能够正常运转时&#xff0c;…

优选算法刷题笔记 2024.6.10-24.6.20

一、双指针算法(快慢指针,对撞指针) 艹&#xff0c;CSDN吞了我是十三题笔记&#xff01;&#xff01;&#xff01; 二、滑动窗口(滑动窗口) 1、找到字符串中所有字母异位词 class Solution {public List<Integer> findAnagrams(String s, String p) {int[] hash1 new in…

【Python】一文向您详细解析内置装饰器 @lru_cache

【Python】一文向您详细解析内置装饰器 lru_cache 下滑即可查看博客内容 &#x1f308; 欢迎莅临我的个人主页 &#x1f448;这里是我静心耕耘深度学习领域、真诚分享知识与智慧的小天地&#xff01;&#x1f387; &#x1f393; 博主简介&#xff1a;985高校的普通本硕&a…

TMS320F280049学习5:CPU timer中断

TMS320F280049学习5&#xff1a;CPU timer中断 文章目录 TMS320F280049学习5&#xff1a;CPU timer中断前言一、工程代码二、CPU timer时钟总结 前言 DSP的内部有3个CPU timer&#xff0c;分别是CUP timer0 / 1 / 2&#xff0c;传说CPU timer2一般在跑系统时用&#xff0c;类似…

网络基础

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 当今的时代是一个网络的时代&#xff0c;网络无处不在。而我们前面学习编写的程序都是单机的&#xff0c;即不能和其他电脑上的程序进行通信。为了实…

新火种AI|英伟达市值超越微软!AI技术如何重塑科技股价值?

作者&#xff1a;一号 编辑&#xff1a;美美 AI&#xff0c;正带着美股狂奔。 2024年&#xff0c;英伟达&#xff08;NVIDIA&#xff09;以其在人工智能&#xff08;AI&#xff09;领域的卓越表现&#xff0c;市值首次超越了科技巨头微软&#xff0c;成为全球市值最高的公司…

车载测试面试项目看这一套就够了!车载测试___自我讲解项目

面试官您好&#xff0c;我叫xx来自安微&#xff0c;今年xx岁&#xff0c;毕业于安微新华学院&#xff0c;我是从2017年开始接触软件测试行业&#xff0c;目前从事软件测试工作有5年多时间&#xff0c;第一家公司做了电商和进销存项目app和web都有做过&#xff0c;上家公司做了车…

C语言变量、指针的内存关系

1. type p ? 表示从内存地址p开始&#xff0c;开辟一段内存&#xff0c;内存大小为类型type规定的字节数&#xff0c;然后把等号右边的值写入到这段内存中。 因此&#xff0c;这块内存起点位置是p&#xff0c;结束是ptype字节数-1。 2. type* p ?表示从内存地址p开始&…

Unity3d 游戏暂停(timeScale=0)引起的deltaTime关联的系列问题解决

问题描述 游戏暂停的功能是通过设置timeScale0实现的&#xff0c;不过在暂停游戏的时候&#xff0c;需要对角色进行预览和设置&#xff0c;为了实现这个功能&#xff0c;是通过鼠标控制相机的操作&#xff0c;为了使相机的操作丝滑&#xff0c;获取鼠标操作系数乘以Time.delta…

Opencv学习项目3——pytesseract

上一次我们使用pytesseract.image_to_data(img)来检测文本&#xff0c;这次我们来只检测数字 项目演示 可以看到&#xff0c;我们只检测了数字其他的并没有检测出来 代码实现 前面两次介绍了opencv的画矩形和设置文本&#xff0c;这次就直接用了&#xff0c;不太明白的可以看…

数据资产与用户体验优化:深入挖掘用户数据,精准分析用户需求与行为,优化产品与服务,提升用户体验与满意度,打造卓越的用户体验,赢得市场认可

一、引言 在数字化时代&#xff0c;数据已经成为企业最宝贵的资产之一。通过深入挖掘和分析用户数据&#xff0c;企业能够精准把握用户需求和行为&#xff0c;从而优化产品与服务&#xff0c;提升用户体验和满意度。这不仅有助于企业在激烈的市场竞争中脱颖而出&#xff0c;还…

Java基础 - 练习(四)打印九九乘法表

Java基础练习 打印九九乘法表&#xff0c;先上代码&#xff1a; public static void multiplicationTable() {for (int i 1; i < 9; i) {for (int j 1; j < i; j) {// \t 跳到下一个TAB位置System.out.print(j "" i "" i * j "\t"…

戏剧之家杂志戏剧之家杂志社戏剧之家编辑部2024年第14期目录

文艺评论 南戏瓯剧跨文化传播研究 陈晓东;高阳;许赛梦; 3-7 论互联网时代的戏剧传播与批评——以西法大剧社和南山剧社为例 邬慧敏; 8-10 “左手荒诞&#xff0c;右手温情”——《西西弗神话》在戏剧《第七天》中的接受探究 赵稳稳; 11-13 戏剧研讨《戏剧之家》投稿…

[SAP ABAP] 数据类型

1.基本数据类型 示例1 默认定义的基本数据类型是CHAR数据类型 输出结果: 示例2 STRING数据类型用于存储任何长度可变的字符串 输出结果: 示例3 DATE数据类型用于存储日期信息&#xff0c;并且可以存储8位数字 输出结果: 提示Tips&#xff1a;日期和时间类型的变量可以直接进…

openh264 帧级码率控制源码分析

openh264 码率控制结构 关于 openh264 码率控制整体结构&#xff0c;可以参考&#xff1a;openh264 码率控制原理框架。 openh264 帧级码率控制介绍 函数关系图&#xff1a;从图可以看出&#xff0c;帧级码控的核心函数就是WelsRcPictureInitGom、WelsRcPictureInfoUpdateGo…

DAB-DETR

论文地址&#xff1a; https://arxiv.org/pdf/2201.12329 文章通过前人的经验得出&#xff0c;导致 DETR 训练速度慢的原因很大可能是因为 decoder 中 cross attention 这个模块&#xff0c;由上面的对比可以看出其与 self attention 的区别主要就在于query的不同。文章猜想两个…

【Python办公自动化之Word】

python办公⾃动化之-Word python-docx库 文章目录 python办公⾃动化之-Word1、安装python-docx库2、⽂档的结构说明3、基本操作语法3.1 打开⽂档3.2加⼊不同等级的标题3.3 添加⽂本3.4 设置字号插曲1&#xff1a;实战演示3.5 设置中⽂字体3.6 设置斜体3.7 设置粗体3.8⾸⾏缩进…

H3C防火墙抓包(图形化)

一.报文捕获 &#xff0c;然后通过wireshark查看报文 二.报文示踪 &#xff0c; 输入源目等信息&#xff0c; 查看报文的详情

mongodb 集群安装

整体架构图&#xff1a; 1. 配置域名 Server1&#xff1a; OS version: CentOS Linux release 8.5.2111 hostnamectl --static set-hostname mongo01 vi /etc/sysconfig/network # Created by anaconda hostnamemong01 echo "192.168.88.20 mong1 mongo01.com mongo…