当前位置:WooYun >> 漏洞信息

漏洞概要 关注数(24) 关注此漏洞

缺陷编号:wooyun-2014-087731

漏洞标题:ThinkPHP最新版本SQL注入漏洞

相关厂商:ThinkPHP

漏洞作者: phith0n

提交时间:2014-12-18 20:08

修复时间:2015-03-18 20:10

公开时间:2015-03-18 20:10

漏洞类型:SQL注射漏洞

危害等级:高

自评Rank:20

漏洞状态:厂商已经确认

漏洞来源: http://www.wooyun.org,如有疑问或需要帮助请联系 [email protected]

Tags标签:

4人收藏 收藏
分享漏洞:


漏洞详情

披露状态:

2014-12-18: 细节已通知厂商并且等待厂商处理中
2014-12-19: 厂商已经确认,细节仅向厂商公开
2014-12-22: 细节向第三方安全合作伙伴开放
2015-02-12: 细节向核心白帽子及相关领域专家公开
2015-02-22: 细节向普通白帽子公开
2015-03-04: 细节向实习白帽子公开
2015-03-18: 细节向公众公开

简要描述:

你们可以说上一个SQL注入漏洞有鸡肋性,确实TP的I函数会进行一定处理。
但这个洞,I函数也奈何不了了,通用到你没话说。
与上几个洞没有关系,非补丁造成。也是影响到3.1~3.2版本的。继续用onethink做演示。

详细说明:

如下controller即可触发SQL注入:

public function test()
{
$uname = I('get.uname');
$u = M('user')->where(array(
'uname' => $uname
))->find();
dump($u);
}


为什么?
我们看看代码。我从github下载的最新源码:https://github.com/liu21st/thinkphp
/ThinkPHP/Library/Think/Db/Driver.class.php 531行:

// where子单元分析
protected function parseWhereItem($key,$val) {
$whereStr = '';
if(is_array($val)) {
if(is_string($val[0])) {
if(preg_match('/^(EQ|NEQ|GT|EGT|LT|ELT)$/i',$val[0])) { // 比较运算
$whereStr .= $key.' '.$this->comparison[strtolower($val[0])].' '.$this->parseValue($val[1]);
}elseif(preg_match('/^(NOTLIKE|LIKE)$/i',$val[0])){// 模糊查找
if(is_array($val[1])) {
$likeLogic = isset($val[2])?strtoupper($val[2]):'OR';
if(in_array($likeLogic,array('AND','OR','XOR'))){
$likeStr = $this->comparison[strtolower($val[0])];
$like = array();
foreach ($val[1] as $item){
$like[] = $key.' '.$likeStr.' '.$this->parseValue($item);
}
$whereStr .= '('.implode(' '.$likeLogic.' ',$like).')';
}
}else{
$whereStr .= $key.' '.$this->comparison[strtolower($val[0])].' '.$this->parseValue($val[1]);
}
}elseif('bind'==strtolower($val[0])){ // 使用表达式
$whereStr .= $key.' = :'.$val[1];
}elseif('exp'==strtolower($val[0])){ // 使用表达式
$whereStr .= $key.' '.$val[1];
}elseif(preg_match('/IN/i',$val[0])){ // IN 运算
if(isset($val[2]) && 'exp'==$val[2]) {
$whereStr .= $key.' '.strtoupper($val[0]).' '.$val[1];
}else{
if(is_string($val[1])) {
$val[1] = explode(',',$val[1]);
}
$zone = implode(',',$this->parseValue($val[1]));
$whereStr .= $key.' '.strtoupper($val[0]).' ('.$zone.')';
}
}elseif(preg_match('/BETWEEN/i',$val[0])){ // BETWEEN运算
$data = is_string($val[1])? explode(',',$val[1]):$val[1];
$whereStr .= $key.' '.strtoupper($val[0]).' '.$this->parseValue($data[0]).' AND '.$this->parseValue($data[1]);
}else{
E(L('_EXPRESS_ERROR_').':'.$val[0]);
}
}else {
$count = count($val);
$rule = isset($val[$count-1]) ? (is_array($val[$count-1]) ? strtoupper($val[$count-1][0]) : strtoupper($val[$count-1]) ) : '' ;
if(in_array($rule,array('AND','OR','XOR'))) {
$count = $count -1;
}else{
$rule = 'AND';
}
for($i=0;$i<$count;$i++) {
$data = is_array($val[$i])?$val[$i][1]:$val[$i];
if('exp'==strtolower($val[$i][0])) {
$whereStr .= $key.' '.$data.' '.$rule.' ';
}else{
$whereStr .= $this->parseWhereItem($key,$val[$i]).' '.$rule.' ';
}
}
$whereStr = '( '.substr($whereStr,0,-4).' )';
}
}else {
//对字符串类型字段采用模糊匹配
$likeFields = $this->config['db_like_fields'];
if($likeFields && preg_match('/^('.$likeFields.')$/i',$key)) {
$whereStr .= $key.' LIKE '.$this->parseValue('%'.$val.'%');
}else {
$whereStr .= $key.' = '.$this->parseValue($val);
}
}
return $whereStr;
}


这就是处理where条件的函数,我们看到如下片段:

}elseif(preg_match('/BETWEEN/i',$val[0])){ // BETWEEN运算
$data = is_string($val[1])? explode(',',$val[1]):$val[1];
$whereStr .= $key.' '.strtoupper($val[0]).' '.$this->parseValue($data[0]).' AND '.$this->parseValue($data[1]);
}


当匹配/BETWEEN/i和$val[0]时,则将strtoupper($val[0])直接插入了SQL语句。
这个匹配:preg_match('/BETWEEN/i',$val[0]),明显是有问题的。因为这个匹配没加^$也就是首尾限定,所以只要我们的$val[0]中含有between时,这个匹配就可以成立,就产生了一个SQL注入。
为了防止I函数对我们输入的过滤影响,我们看看I函数:

function I($name,$default='',$filter=null,$datas=null) {
if(strpos($name,'/')){ // 指定修饰符
list($name,$type) = explode('/',$name,2);
}
if(strpos($name,'.')) { // 指定参数来源
list($method,$name) = explode('.',$name,2);
}else{ // 默认为自动判断
$method = 'param';
}
switch(strtolower($method)) {
case 'get' : $input =& $_GET;break;
case 'post' : $input =& $_POST;break;
case 'put' : parse_str(file_get_contents('php://input'), $input);break;
case 'param' :
switch($_SERVER['REQUEST_METHOD']) {
case 'POST':
$input = $_POST;
break;
case 'PUT':
parse_str(file_get_contents('php://input'), $input);
break;
default:
$input = $_GET;
}
break;
case 'path' :
$input = array();
if(!empty($_SERVER['PATH_INFO'])){
$depr = C('URL_PATHINFO_DEPR');
$input = explode($depr,trim($_SERVER['PATH_INFO'],$depr));
}
break;
case 'request' : $input =& $_REQUEST; break;
case 'session' : $input =& $_SESSION; break;
case 'cookie' : $input =& $_COOKIE; break;
case 'server' : $input =& $_SERVER; break;
case 'globals' : $input =& $GLOBALS; break;
case 'data' : $input =& $datas; break;
default:
return NULL;
}
if(''==$name) { // 获取全部变量
$data = $input;
$filters = isset($filter)?$filter:C('DEFAULT_FILTER');
if($filters) {
if(is_string($filters)){
$filters = explode(',',$filters);
}
foreach($filters as $filter){
$data = array_map_recursive($filter,$data); // 参数过滤
}
}
}elseif(isset($input[$name])) { // 取值操作
$data = $input[$name];
$filters = isset($filter)?$filter:C('DEFAULT_FILTER');
if($filters) {
if(is_string($filters)){
$filters = explode(',',$filters);
}elseif(is_int($filters)){
$filters = array($filters);
}

foreach($filters as $filter){
if(function_exists($filter)) {
$data = is_array($data) ? array_map_recursive($filter,$data) : $filter($data); // 参数过滤
}elseif(0===strpos($filter,'/')){
// 支持正则验证
if(1 !== preg_match($filter,(string)$data)){
return isset($default) ? $default : NULL;
}
}else{
$data = filter_var($data,is_int($filter) ? $filter : filter_id($filter));
if(false === $data) {
return isset($default) ? $default : NULL;
}
}
}
}
if(!empty($type)){
switch(strtolower($type)){
case 's': // 字符串
$data = (string)$data;
break;
case 'a': // 数组
$data = (array)$data;
break;
case 'd': // 数字
$data = (int)$data;
break;
case 'f': // 浮点
$data = (float)$data;
break;
case 'b': // 布尔
$data = (boolean)$data;
break;
}
}
}else{ // 变量默认值
$data = isset($default)?$default:NULL;
}
is_array($data) && array_walk_recursive($data,'think_filter');
return $data;
}


较前些版本有些改进:
1.加了类型强制转换$type,但在默认情况下$type是空的,强制类型转换是不存在的。
2.将is_array($data) && array_walk_recursive($data,'think_filter');放在最后一行。我们看看think_filter这个过滤函数:

function think_filter(&$value){
// TODO 其他安全过滤
// 过滤查询特殊字符
if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|LIKE|NOTLIKE|BETWEEN|IN)$/i',$value)){
$value .= ' ';
}
}


这个实际上就是对我之前那个漏洞的一个解决方案,将一些关键词后面加空格。但我们看到,这个正则是存在“^$”首尾限定符的。所以只有传入参数完全“等于”BETWEEN的时候才会被加上空格,而且这里加上空格也不会影响漏洞的产生,因为漏洞位置的正则没有加^$首尾限定符。
还有一个说明:之前thinkphp出了个“错误”的补丁,这个补丁已经被官方去掉了,所以不用考虑那个补丁造成的一些干扰。

漏洞证明:

那我们回到最初那段代码:

public function test()
{
$uname = I('get.uname');
$u = M('user')->where(array(
'uname' => $uname
))->find();
dump($u);
}


这个代码,我们来测试一下:

01.jpg


果然是有漏洞的,我们看看具体执行的SQL语句:

02.jpg


就在BETWEEN处。除了BETWEEN外还有IN,我就一块说明了。
Onethink演示:

03.jpg


细我就不说了,和之前一样。

修复方案:

正则一定要写明确:
/^BETWEEN$/i

版权声明:转载请注明来源 phith0n@乌云


漏洞回应

厂商回应:

危害等级:高

漏洞Rank:10

确认时间:2014-12-19 11:50

厂商回复:

感谢提出

最新状态:

暂无