qq官方网站登录,免费注册网站空间,网站建设仟首选金手指,wordpress 主题 汉化目录
前言
模拟预编译
真正的预编译
预编译中存在的SQL注入
宽字节
没有进行参数绑定
无法预编译的位置 前言
相信学习过SQL注入的小伙伴都知道防御SQL注入最好的方法#xff0c;就是使用预编译也就是PDO是可以非常好的防御SQL注入的#xff0c;但是如果错误的设置了…目录
前言
模拟预编译
真正的预编译
预编译中存在的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() 最后查询出来的就会使id1的那个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不再会像之前一样错误理解成查询id1的用户然后再查询版本这样看来预编译的作用就是消除了sql语句的歧义。
那么回看最初我们提出的疑问预编译真的能完美防御sql注入吗有没有什么奇技淫巧能绕过预编译进行注入呢
有一篇大佬的文章分析过预编译真的能完美防御SQL注入吗
这里面提到一个很有趣的点——预编译是将sql语句参数化刚刚的例子中 where语句中的内容是被参数化的。这就是说预编译仅仅只能防御住可参数化位置的sql注入。那么对于不可参数化的位置预编译将没有任何办法。
那么哪些是不可参数化的位置呢原作者说 为了研究原理大佬又找到了一篇文章这个应该是最早提出order by后没法参数化所以可以被sql注入的 SQL预编译中order by后为什么不能参数化原因文章里是这么解释的
大概就是说order by后面的字段是不能加引号的而预编译后会自动加上引号因为这个矛盾所以order by的后面不能进行预编译。
不过当时他解释原因是因为自动加引号的setString()方法而这个方法似乎只是java下存在的而这篇文章是从原理出发研究研究php下的注入可能(其实这种思路不同语言是共通的)
模拟预编译
后端页面代码 ?php
$username $_POST[username]; // 接收username
# 建立数据库连接
header(Content-Type:text/html;charsetutf-8);
$dbs mysql:host127.0.0.1;dbnamesort;
$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{
}
$connnull; # 关闭链接
? 执行查询namemechoy查看数据库日志 27 Connect rootlocalhost 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时生效 再执行查询namemechoy查看数据库日志 4 Connect rootlocalhost 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:hostlocalhost;dbnametest, 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 rootlocalhost 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 rootlocalhost 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 rootlocalhost 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:hostlocalhost;dbnametest, 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 rootlocalhost on test61 Prepare SELECT password FROM test where username ?61 Execute SELECT password FROM test where username admin 这时数据库中执行的顺序变成了先连接然后准备语句用问号?占位接着用输入替换问号?执行语句专业点的说法叫做 建立连接 构建语法树 执行
这也是为什么我们之前说的预编译的作用是让整个语句的功能已经提前定死消除了sql语句的歧义。
当我们输入username ‘admin’同样会没有任何输出 那么再来看看日志
我们看一下数据库的日志
2023-10-22T15:49:30.089718Z 24 Connect rootlocalhost 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 rootlocalhost 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:hostlocalhost;dbnametest;charsetgbk, 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;?
然后我们输入一下内容就可以利用宽字节绕过预处理实现注入
usernameadmin%df%27%20union%20select%20database();# 通过抓包可以看到这里确实将转义字符闭合为一个特殊字符了
这个语句在navicat里是能正常执行的但我并没有在网页上获得输出是因为预编译只会输出第一条语句因此后面union的执行结果无法输出
这里如果使用真预编译就不会出现上面的这种问题
因此相比于模拟预编译真编译的安全性大的多现在可能的几种针对预编译的注入方法也都是在模拟预编译下实现的。
没有进行参数绑定
没有参数绑定的预编译等于没有预编译无论是真编译还是模拟预编译没有参数绑定等于没编译并且由于DPO默认支持堆叠注入我们可以通过堆叠注入先插入值然后查询插入的值获取输出结果。
后端代码
?php
$id $_POST[id];$dbs mysql:hostlocalhost;dbnametest;
$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);$connnull; # 关闭链接
?
可以看到代码中的id没有进行参数绑定
那么我们可以尝试POST提交一下使用堆叠注入来将要查询的东西插入到user表中的两个字段中然后在进行查询 然后再POST传入的值
然后访问该id
就可以看到对应插入的值了
无法预编译的位置
上面提到过order by的后面是没法预编译的因此遇到可控排序功能一般一注一个准下面我们来通过日志研究一下这到底是为什么
后端代码
?php
$col $_POST[col];$dbs mysql:hostlocalhost;dbnametest;
$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);$connnull; # 关闭链接
?
假如我们想按照password进行排序post一个colpassword
我们可以看看日志
2023-10-27T01:23:43.100087Z 187 Connect rootlocalhost 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