当前位置: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

厂商回复:

感谢提出

最新状态:

暂无


漏洞评价:

评论

  1. 2014-12-18 20:13 | ′雨。 ( 普通白帽子 | Rank:1231 漏洞数:190 | Only Code Never Lie To Me.)

    前排顶师傅。

  2. 2014-12-18 20:14 | 牛肉包子 ( 普通白帽子 | Rank:254 漏洞数:64 )

    前排顶师傅。

  3. 2014-12-18 20:17 | 浮萍 ( 普通白帽子 | Rank:555 漏洞数:118 | 默默潜水)

    前排顶师傅。

  4. 2014-12-18 20:19 | 玉林嘎 ( 普通白帽子 | Rank:758 漏洞数:96 )

    前排顶师祖。

  5. 2014-12-18 20:19 | Ev1l ( 实习白帽子 | Rank:68 漏洞数:20 | 问题真实存在但影响不大。联系邮箱security...)

    前排顶师傅。顺道看雨神

  6. 2014-12-18 20:27 | 爱上平顶山 认证白帽子 ( 核心白帽子 | Rank:2738 漏洞数:547 | [不戴帽子]异乡过客.曾就职于天朝某机构.IT...)

    ...........

  7. 2014-12-18 20:52 | Power ( 实习白帽子 | Rank:54 漏洞数:22 | 还需要等待.........)

    前排顶师傅。

  8. 2014-12-18 21:33 | 啊L川 ( 普通白帽子 | Rank:195 漏洞数:39 | 菜鸟 ,菜渣, 菜呀!)

    太残暴了

  9. 2014-12-18 22:02 | 浩天 认证白帽子 ( 普通白帽子 | Rank:915 漏洞数:79 | 度假中...)

    别乱发,同志们,当心封号

  10. 2014-12-18 22:16 | phith0n 认证白帽子 ( 核心白帽子 | Rank:656 漏洞数:107 | 一个想当文人的黑客~)

    @浩天 吓哭了

  11. 2014-12-18 22:17 | phith0n 认证白帽子 ( 核心白帽子 | Rank:656 漏洞数:107 | 一个想当文人的黑客~)

    @浩天 为啥把我的也删了……哭哭哭

  12. 2014-12-18 22:57 | 秋风 ( 普通白帽子 | Rank:438 漏洞数:44 | 码农一枚,关注互联网安全)

    NB!

  13. 2014-12-18 23:12 | 泳少 ( 普通白帽子 | Rank:231 漏洞数:79 | ★ 梦想这条路踏上了,跪着也要...)

    又来了?

  14. 2014-12-18 23:22 | backtrack丶yao ( 普通白帽子 | Rank:290 漏洞数:107 | "><img src=x onerror=alert(666666);> <im...)

    P神又来了

  15. 2014-12-18 23:31 | 风情万种 ( 普通白帽子 | Rank:181 漏洞数:63 | 不再相信爱了)

    牛逼

  16. 2014-12-19 08:52 | Ev1l ( 实习白帽子 | Rank:68 漏洞数:20 | 问题真实存在但影响不大。联系邮箱security...)

    @′雨。 雨神说好的交友呢 一百块都不给我

  17. 2014-12-19 11:10 | 酱油党 ( 路人 | Rank:2 漏洞数:2 | 跳槽观望中)

    刚改完又爆! 这是要折腾死我的节奏么?

  18. 2014-12-19 12:26 | Coner ( 路人 | Rank:3 漏洞数:1 )

    框架的sql注入,责任是不好划分的,就连thinkphp官方提供的所谓的“防止SQL注入”的方法,使用不当也还是有问题的,何况其他一些不同的sql操作:(,所以,这一巴掌还是让onethink接着把:)

  19. 2014-12-19 12:27 | Coner ( 路人 | Rank:3 漏洞数:1 )

    普通的sql操作-->改上面错误

  20. 2014-12-19 12:50 | Zombiecc ( 路人 | Rank:0 漏洞数:2 | just for fun.)

    这~完全是爆炸的节奏

  21. 2014-12-19 12:54 | phith0n 认证白帽子 ( 核心白帽子 | Rank:656 漏洞数:107 | 一个想当文人的黑客~)

    @Coner 不是使用不当,正常SQL操作,不进行字符串拼接,是框架的问题。

  22. 2014-12-19 13:54 | Coner ( 路人 | Rank:3 漏洞数:1 )

    @phith0n 你所说的“正常SQL操作”是官方的add,save等函数?是官方文档中的“CURD操作”中的那些方法?

  23. 2014-12-19 14:13 | phith0n 认证白帽子 ( 核心白帽子 | Rank:656 漏洞数:107 | 一个想当文人的黑客~)

    @Coner 举个例子吧,比如如下代码即可注入: $uname = I('get.uname'); $u = M('user')->where(array( 'uname' => $uname ))->find();

  24. 2014-12-19 14:14 | phith0n 认证白帽子 ( 核心白帽子 | Rank:656 漏洞数:107 | 一个想当文人的黑客~)

    @Coner 就是CURD里的方法。增删改查。

  25. 2014-12-19 14:31 | Coner ( 路人 | Rank:3 漏洞数:1 )

    @phith0n 貌似官方并没有说人家提供的where函数可以防止sql注入吧?不过人家倒是说了,如果要解决sql注入,请这样用:http://document.thinkphp.cn/manual_3_2.html#sql_injection (你这个漏洞有onethink,所以我觉得这一巴掌还是应该onethink担着,而不是thinkPHP

  26. 2014-12-19 14:42 | phith0n 认证白帽子 ( 核心白帽子 | Rank:656 漏洞数:107 | 一个想当文人的黑客~)

    @Coner 那就不止onethink担着了,thinsns,thinkcmf,几乎所有thinkphp开发的网站都得担着,只要用数组查询的都会出SQL注入,而数组查询又是文档推荐的方式。倒是,如果如果用字符串拼接的方式,反而不会出漏洞。或者用参数化查询,但国内开发者有几个真正用上参数化查询的。而且我交漏洞的目的不是让谁去承担这个责任,而是告诉thinkphp这个地方的问题,官方文档推荐的方法存在SQL注入。包括我自己开发的应用都存在漏洞,希望官方去改正,仅此而已。其实onethink和thinkphp的区别无非是2000或500吧,如果thinkphp真能完美解决我项目中遇到的安全问题,我不拿钱又何妨(反正拿tp这个洞已经刷过一些cms了)。况且thinkphp自己都确认了,毫无怨言。

  27. 2014-12-19 15:26 | Coner ( 路人 | Rank:3 漏洞数:1 )

    @phith0n 哈哈哈哈哈,如果你认为官方给你的建议及例子默认是安全的写法,那这就是你的问题了,官方在文档中明确说了,如果要防止sql注入请使用官方指定的方法,你为什么不去追这个问题呢?不过我也承认很多基于thinkphp的cms存在这种问题是一种安全问题,因为很多开发者都类似:)并且,如果这样子考虑问题的话,那thinkphp这个框架问题实在是太多了:)

  28. 2014-12-19 15:33 | phith0n 认证白帽子 ( 核心白帽子 | Rank:656 漏洞数:107 | 一个想当文人的黑客~)

    @Coner 随便咯,我提出来让国内安全环境更好。顺便赚钱,管那么多。发钱的人认为是怎样就是怎样。

  29. 2014-12-19 16:20 | pandas ( 普通白帽子 | Rank:585 漏洞数:58 | 国家一级保护动物)

    @phith0n 最后这句话说得不好,差评要打屁股

  30. 2014-12-19 19:57 | 寂寞的瘦子 ( 普通白帽子 | Rank:242 漏洞数:53 | 一切语言转汇编理论)

    @phith0n 说明php还不成熟,j2ee刚好相反,国内开发者有几个java程序员还在用字符串拼接的!!!

  31. 2014-12-22 16:16 | 辣辣飞侠 ( 路人 | Rank:11 漏洞数:1 | 维护网络安全从我做起)

    @phith0n 现在都有安全防护性质的软件护体 基本很难成功

  32. 2014-12-23 10:14 | xfkxfk 认证白帽子 ( 核心白帽子 | Rank:2179 漏洞数:338 | 呵呵!)

    @Coner @phith0n 千错万错都是程序猿的错,TP官方也是这么说的

  33. 2014-12-23 12:01 | phith0n 认证白帽子 ( 核心白帽子 | Rank:656 漏洞数:107 | 一个想当文人的黑客~)

    @xfkxfk 我这个洞就是tp代码上的错,跟程序员没关系,官方回复也无言以对。

  34. 2014-12-24 01:38 | Anonymous.L ( 实习白帽子 | Rank:37 漏洞数:8 | 最后一位关注xxxx的人 , 孤独之人)

    @phith0n 大侠是做研发的还是专门做安全的啊?感觉能找出这些的必须要有研发的功底才行哈?

  35. 2014-12-24 22:44 | 会飞的猪 ( 路人 | Rank:16 漏洞数:2 | 爱渗透,爱生活。洗洗更健康。)

    感这个漏洞觉像后门

  36. 2014-12-25 12:36 | ksc ( 路人 | Rank:22 漏洞数:9 | 张三的好朋友)

    @phith0n 漏洞不漏洞的先不说 起码这是个坑 很大的坑 关键是官方文档里面没有重点说明那就是个"漏洞"了

  37. 2014-12-25 20:14 | zhxs ( 实习白帽子 | Rank:32 漏洞数:19 | Jyhack-TeaM:http://bbs.jyhack.com/)

    你特么把我吓哭了,赔钱

  38. 2014-12-29 17:22 | F4ck3R ( 路人 | Rank:2 漏洞数:1 | 嘻嘻~)

    前排顶师傅。

  39. 2015-01-13 12:56 | 777 ( 路人 | Rank:19 漏洞数:3 | I'M 777 thank's)

    @zhxs 求细节看不到- -

  40. 2015-03-18 20:54 | 残雪 ( 实习白帽子 | Rank:34 漏洞数:7 | 屌丝一枚擅长扯淡)

    师傅牛逼

  41. 2015-03-19 07:55 | 小森森 ( 路人 | Rank:11 漏洞数:2 | 不中二 枉少年)

    错误应该归咎于ThinkPHP这个国产框架的设计问题?(我猜)未经严谨设计,按照想象中的来,……