预编译过程
开启MySQL日志功能
1
2
3
4
5
6
7
8
9
10
11查看是否开启
show global variables like "%genera%";
+------------------+----------------------------------------------------------+
| Variable_name | Value |
+------------------+----------------------------------------------------------+
| general_log | OFF |
| general_log_file | D:\MySQL5.7.26\data\AMao-Desktop.log |
+------------------+----------------------------------------------------------+
开启日志功能
set global general_log = on;执行简单预编译
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25prepare stmt_name from 'select * from user where `name`=?';
Query OK, 0 rows affected (0.00 sec)
Statement prepared
set @query_name='allen';
Query OK, 0 rows affected (0.00 sec)
execute stmt_name using @query_name;
+----+-------+--------+-----+------+
| id | name | gender | age | flag |
+----+-------+--------+-----+------+
| 1 | allen | 1 | 20 | 1 |
+----+-------+--------+-----+------+
1 row in set (0.00 sec)
set @query_name2 = 'joy';
Query OK, 0 rows affected (0.00 sec)
execute stmt_name using @query_name2;
+----+------+--------+-----+------+
| id | name | gender | age | flag |
+----+------+--------+-----+------+
| 6 | joy | 1 | 21 | 1 |
+----+------+--------+-----+------+
1 row in set (0.00 sec)查看日志
1
2
3
4
5
6
7
8Argument
Prepare select * from user where `name`=?
Query set @query_name='allen'
Query execute stmt_name using @query_name
Execute select * from user where `name`='allen'
Query set @query_name2 = 'joy'
Query execute stmt_name using @query_name2
Execute select * from user where `name`='joy'对比正常请求过程
1
2
3
4
5
6select * from user where name = 'joy';
+----+------+--------+-----+------+
| id | name | gender | age | flag |
+----+------+--------+-----+------+
| 6 | joy | 1 | 21 | 1 |
+----+------+--------+-----+------+1
2Argument
Query select * from user where name = 'joy'
普通SQL语句执行过程:
在数据库接收到SQL语句后,首先对其进行语义解析,生成语法树,随后对SQL语句进行优化并制定执行计划并执行
预编译操作过程:
在预编译过程中,数据库首先接收到带有预编译占位符的SQL语句,解析生成语法树(Lex),并缓存在cache中,然后接收对应的参数信息,从cache中取出语法树设置参数,然后再进行优化和执行。
由于参数信息传入前语法树就已生成,执行的语法结构也就无法因参数而改变,自然也就杜绝了SQL注入的出现。
TIPS:预编译可以实现一次编译、多次执行(程序将使用第一次存储于缓冲区的预编译后的模板进行解析),省去了解析优化等过程
预编译防止SQL注入的原理
预编译脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
try {
$user = 'root';
$pass = 'root';
$name = $_GET['name'];
$dbh = new PDO('mysql:host=localhost;dbname=tt', $user, $pass);
// 设置为预编译模式
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES,false);
$stmt = $dbh->prepare("select * from user where name = :user_name");
$stmt->bindParam(':user_name',$name);
if($stmt->execute()){
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
print_r($row);
}
}
} catch (PDOException $e) {
print "Error!: " . $e->getMessage() . "<br/>";
die();
}?name=joy
1
2
3
4
5
6# 日志
Connect root@localhost on tt using TCP/IP
Prepare select * from user where name = ?
Execute select * from user where name = 'joy'
Close stmt
Quit整个流程分为五步:连接、预编译、传入参数并执行、关闭预编译语句、退出
?name=joy%27and 1=1%23
1
2
3
4
5Connect root@localhost on tt using TCP/IP
Prepare select * from user where name = ?
Execute select * from user where name = 'joy\'and 1=1#'
Close stmt
Quit可以看到输入的
'
被转义了,而在引号内的#
并不会被解释成注释符由引号就想到了整型注入,进一步测试
?id=1 and 1=1#
1
2Prepare select * from user where id = ?
Execute select * from user where id = '1 and 1=1#'查看日志发现,在执行的SQL语句中,变量位置还是加了引号,因为在MySQL存在一个弱类型转换,所以两者的插叙结果是一样的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21select * from user where id =1;
+----+-------+--------+-----+------+
| id | name | gender | age | flag |
+----+-------+--------+-----+------+
| 1 | allen | 1 | 20 | 1 |
+----+-------+--------+-----+------+
select * from user where id ='1';
+----+-------+--------+-----+------+
| id | name | gender | age | flag |
+----+-------+--------+-----+------+
| 1 | allen | 1 | 20 | 1 |
+----+-------+--------+-----+------+
1 row in set (0.00 sec)
select * from user where id ='1aaa';
+----+-------+--------+-----+------+
| id | name | gender | age | flag |
+----+-------+--------+-----+------+
| 1 | allen | 1 | 20 | 1 |
+----+-------+--------+-----+------+
输入的引被转义,联想到宽字节注入,在查阅资料得知,在PHP 5.3.6之前,PDO确实存在宽字节注入的问题
上述预编译代码在 LNMP、MySQL 5.5.62、PHP 5.2.17 环境下下实测成功
模拟预编译
模拟预编译是防止某些数据库不支持预编译而设置的(如sqllite与低版本mysql)
如果模拟预处理开启,那么数据库客户端程序内部会模拟 mysql 数据库中的参数绑定这一过程,即程序会在内部模拟prepare的过程,当执行execute时,再将拼接后的完整SQL语句发送给mysql数据库执行
测试
PDO默认使用的是模拟预编译,只要注释掉上面的脚本里的
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES,false);
日志信息和执行普通SQL语句一样
1
2Connect root@localhost on tt using TCP/IP
Query select * from user where name = 'joy'
模拟预处理防止SQL注入的本质是在参数绑定过程中对参数值进行转义与过滤,这一点与真正的sql数据库预处理是不一样的
常见接口的预编译方式
接口 默认预编译方式 Python-MySQLdb 模拟预编译 Python-Pymysql 模拟预编译 Python-Oursql 预编译 PHP-Mysqli 预编译 PHP-PDO 模拟预编译 JAVA-PreparedStatement 模拟预编译
预编译中的转义
通过上面查看日志可以发现,使用预编译后,会对一些特殊字符进行转义,而这个转义实在后端的接口进行还是数据库进行的?
这里尝试阅读pdo的源码,PHP的扩展都是基于C语言,太菜了看起来很费劲,于是通过流量分析来验证:通过流量分析发现Mysql服务器收到的数据包是没有经过转义,即转义操作是在MySQL数据库进行的
预编译中的注入思路
- 白盒审计中PDO、PreparedStatement中开发者直接拼接SQL语句的行为
- 白盒审计中ORDER BY后的字段名动态传入的SQL语句;渗透测试中允许用户传入按某个字段进行排序的行为,这很有可能是直接拼接的。
- 白盒审计中ORDER BY后排序方式(ASC/DESC)动态传入的SQL语句;渗透测试中允许用户选择正序倒序排列的行为,需要抓包查看是否直接传入ASC/DESC,若是则很有可能存在拼接行为。
- 白盒审计中模糊查询是否拼接;渗透测试中针对搜索行为进行SQL注入测试。
- 白盒审计中IN语句后是否拼接。
参考
https://xz.aliyun.com/t/7132#toc-5
https://blog.nowcoder.net/n/be73b8f592504ae8b1d00368433061be
声明
- 博主初衷为分享网络安全知识,请勿利用技术做出任何危害网络安全的行为,否则后果自负,与本人无关!
- 部分学习内容来自网络,回馈网络,如涉及版权问题,请联系删除 orz
Author: AMao
Link: https://passenger-amao.github.io/2020/09/13/%E9%A2%84%E7%BC%96%E8%AF%91/
Copyright: 本站所有文章均采用 署名-非商业性使用-相同方式共享 4.0 国际(CC BY-NC-SA 4.0) 许可协议。转载请注明出处!