Thinkphp5
ThinkPHP,是为了简化企业级应用开发和敏捷WEB应用开发而诞生的开源轻量级PHP框架。
最早诞生于2006年初,2007年元旦正式更名为ThinkPHP,并且遵循Apache2开源协议发布。ThinkPHP从诞生以来一直秉承简洁实用的设计原则,在保持出色的性能和至简的代码的同时,也注重易用性。并且拥有众多原创功能和特性,在社区团队的积极参与下,在易用性、扩展性和性能方面不断优化和改进。
某些版本的Thinkphp存在一些漏洞,比如Thinkphp 5.1.(16-22) sql注入漏洞
POC
http://********/index/index/index?orderby[id`|updatexml(1,concat(0x7,user(),0x7e),1)%23]=1
漏洞描述
在ThinkPHP 5.1.23之前的版本中存在SQL注入漏洞,该漏洞是由于程序在处理order by 后的参数时,未正确过滤处理数组的key值所造成。如果该参数用户可控,且当传递的数据为数组时,会导致漏洞的产生。(CVE-2018-16385)
一点思考
这个漏洞虽然是sql注入,但是比较鸡肋。。。
为什么这么讲呢?我们测试一下,就会发现报错注入的时候,我们只能爆出类似于user()、database()这类最基础的信息,而不能进行子查询,获取不到更加关键的信息。
这样的原因是用参数化查询PDO,将参数与查询语句分离,进而降低了漏洞风险。
PDO分析
下面将会针对thinkphp 5.1.17框架的PDO进行分析。
PDO介绍
我们可以把它看作是想要运行的 SQL 的一种编译过的模板,它可以使用变量参数进行定制。预处理语句可以带来两大好处:
- 查询仅需解析(或预处理)一次,但可以用相同或不同的参数执行多次。当查询准备好后,数据库将分析、编译和优化执行该查询的计划。对于复杂的查询,此过程要花费较长的时间,如果需要以不同参数多次重复相同的查询,那么该过程将大大降低应用程序的速度。通过使用预处理语句,可以避免重复分析/编译/优化周期。简而言之,预处理语句占用更少的资源,因而运行得更快。
- 提供给预处理语句的参数不需要用引号括起来,驱动程序会自动处理。如果应用程序只使用预处理语句,可以确保不会发生SQL 注入。(然而,如果查询的其他部分是由未转义的输入来构建的,则仍存在 SQL 注入的风险)。
预处理语句如此有用,以至于它们唯一的特性是在驱动程序不支持 PDO 将模拟处理。这样可以确保不管数据库是否具有这样的功能,都可以确保应用程序可以用相同的数据访问模式。
如果还不理解的话,我们可以看看PDO预编译执行过程
- prepare($SQL) 编译SQL语句
-
bindValue(
$param
,$value
) 将value绑定到param的位置上<?php $stmt = $dbh->prepare("INSERT INTO REGISTRY (name, value) VALUES (?, ?)"); $stmt->bindParam(1, $name); $stmt->bindParam(2, $value); // 插入一行 $name = 'one'; $value = 1; $stmt->execute(); // 用不同的值插入另一行 $name = 'two'; $value = 2; $stmt->execute(); ?>
-
execute() 执行
<?php $stmt = $dbh->prepare("CALL sp_returns_string(?)"); $stmt->bindParam(1, $return_value, PDO::PARAM_STR, 4000); // 调用存储过程 $stmt->execute(); print "procedure returned $return_value\n"; ?>
报错原因
预编译SQL语句的时候发生错误,从而产生报错
当 prepare() 时,查询语句已经发送给了数据库服务器,此时只有占位符 ?
发送过去,没有用户提交的数据;当调用到 execute()时,用户提交过来的值才会传送给数据库,他们是分开传送的,所以理论上确保不会发生SQL注入。
这个漏洞实际上就是控制了第二步的$param
变量,这个变量如果是一个SQL语句的话,那么在第二步的时候是会抛出错误使得报错(单纯的语句报错)既然如此我们实际上报错利用点在哪里呢?
实际上,在预编译的时候,也就是第一步即可利用
<?php
$params = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
];
$db = new PDO('mysql:dbname=tpdemo;host=127.0.0.1;', 'root', 'root', $params);
try {
$link = $db->prepare('SELECT * FROM users WHERE id in (:where_id, updatexml(0,concat(0xa,user()),0))');
} catch (\PDOException $e) {
var_dump($e);
}
执行发现,虽然只调用prepare(),但原SQL语句中的报错已经成功执行:
是因为这里设置了PDO::ATTR_EMULATE_PREPARES => false
。
这个选项涉及到PDO的“预处理”机制:因为不是所有数据库驱动都支持SQL预编译,所以PDO存在“模拟预处理机制”。如果说开启了模拟预处理,那么PDO内部会模拟参数绑定的过程,SQL语句是在最后execute()的时候才发送给数据库执行;如果我这里设置了PDO::ATTR_EMULATE_PREPARES => false
,那么PDO不会模拟预处理,参数化绑定的整个过程都是和Mysql交互进行的。
非模拟预处理的情况下,参数化绑定过程分两步:第一步是prepare阶段,发送带有占位符的sql语句到mysql服务器(parsing->resolution)
,第二步是多次发送占位符参数给mysql服务器进行执行(多次执行optimization->execution
)。
这时,假设在第一步执行prepare($SQL)的时候我的SQL语句就出现错误了,那么就会直接由mysql那边抛出异常,不会再执行第二步。
而在thinkphp 5.1.17中的默认配置
// PDO连接参数
protected $params = [
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false,
];
但是,在这个POC中
/public/index.php/index/index?username[0]=point&username[1]=1&username[2]=updatexml(1,concat(0x7,user(),0x7e),1)^&username[3]=0
如果你将user()改成一个子查询语句,那么结果又会爆出Invalid parameter number: parameter was not defined
的错误。应该是预编译在mysql服务端进行的,但是预编译的过程是不接触数据的 ,也就是说不会从表中将真实数据取出来,所以使用子查询的情况下不会触发报错;虽然预编译的过程不接触数据,但user()数据库函数的值还是将会编译进SQL语句,所以这里执行并爆了出来。
修改子查询语句
如果我们把user()改成一个子查询语句呢?
<?php
$params = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
];
$db = new PDO('mysql:dbname=tpdemo;host=127.0.0.1;', 'root', 'root', $params);
try {
$link = $db->prepare('SELECT * FROM `users` WHERE `id` IN (:where_id_in_0,updatexml(0,concat(0xa,(select username from users limit 1)),0)) ');
} catch (\PDOException $e) {
var_dump($e);
}
虽然我们使用了updatexml函数,但是他可能不接触数据:预编译的确是mysql服务端进行的,但是预编译的过程是不接触数据的 ,也就是说不会从表中将真实数据取出来,所以使用子查询的情况下不会触发报错;虽然预编译的过程不接触数据,但类似user()这样的数据库函数的值还是将会编译进SQL语句,所以这里执行并爆了出来。
把updatexml函数去掉
<?php
$params = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
];
$db = new PDO('mysql:dbname=tpdemo;host=127.0.0.1;', 'root', 'root', $params);
try {
$link = $db->prepare('SELECT * FROM users WHERE id IN (:where_id_in_0)union(select~1,2)');
var_dump($link);
$link->bindValue(':where_id_in_0)union(select~1,2)','1','1');
} catch (\PDOException $e) {
var_dump($e);
}
这样就会报Invalid parameter number: parameter was not defined
在上面绑定的变量中,让:符号后面的字符串中不出现空格。但是在PDO的prepare()编译sql语句这个过程中,pdo已经把(:)
内的内容认为时PDO绑定的变量,所以在第二步bindValue()
步骤中,才会报错parameter was not defined
也就说这两步数据不匹配,导致无法正常执行第三步查询我们想要得字段
总结
Thinkphp5 框架采用的PDO机制可以说从根本上已经解决了一大堆SQL方面的安全问题,但过多的信任导致这里是在参数绑定的过程中产生了注入,不过采用的PDO机制也可以说是将危害降到了最小。