【挖坟】PHPCMS v9.6.0 SQL注入

大致审了一整个CMS后,决定实际上跟一下一个近期的漏洞。于是挑了PHPCMS v9.6.0好几个月前的洞,一方面资料比较多,一方面自己比较菜。因为不是第一分析者,所以采用从绕过点、传参进库的点到利用点的方式记录。

首先搭建PHPCMS v9.6.0环境
下载地址:
PHPCMS v9.6.0
配置数据库,一路Next就可以了

先看绕过点:

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('"','"',$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;
}

同时过滤了%27*,如果只进行一次这个函数就有可能被绕过,绕过形式%*27

显然安全函数的使用不可能在短期内回溯完

再看传参进库点:

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);
        echo $a_k."<br>";
        if(isset($i)) $i = $id = intval($i);
        echo $id;
        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']);

这里对$a_k变量进行了一个parse_str()也就是原有的$id会被变量覆盖掉。但是注意到下面有一句

if(isset($i)) $i = $id = intval($i);

也就是如果存在$i就将$id赋上强制类型转换后的$i
假如不存在$i,就会导致$id不产生变化
$id变量也就完全可控
再看下面的

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

$id变量直接进入了疑似数据库操作的语句里(phpcms不熟悉,虽然大家都说是查询,但是还是跟进去看看)=>/phpcms/libs/classes/model.class.php

final public function get_one($where = '', $data = '*', $order = '', $group = '') {
        if (is_array($where)) $where = $this->sqls($where);
        return $this->db->get_one($data, $this->table_name, $where, $order, $group);
    }

final public function sqls($where, $font = ' AND ') {
        if (is_array($where)) {
            $sql = '';
            foreach ($where as $key=>$val) {
                $sql .= $sql ? " $font `$key` = '$val' " : " `$key` = '$val'";
            }
            return $sql;
        } else {
            return $where;
        }
    }

再进db_classget_one()

public function get_one($data, $table, $where = '', $order = '', $group = '') {
        $where = $where == '' ? '' : ' WHERE '.$where;
        $order = $order == '' ? '' : ' ORDER BY '.$order;
        $group = $group == '' ? '' : ' GROUP BY '.$group;
        $limit = ' LIMIT 1';
        $field = explode( ',', $data);
        array_walk($field, array($this, 'add_special_char'));
        $data = implode(',', $field);

        $sql = 'SELECT '.$data.' FROM `'.$this->config['database'].'`.`'.$table.'`'.$where.$group.$order.$limit;
        echo $sql;
        $this->execute($sql);
        $res = $this->fetch_next();
        $this->free_result();
        return $res;
    }

数据库类没有多余操作,因此可以注入
继续回溯$a_k变量

$a_k = sys_auth($a_k, 'DECODE', pc_base::load_config('system','auth_key'));

sys_auth跟过去是个加密解密函数
而这里是进行一个解密操作
想要注入先加密

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;
        $_COOKIE[$var] = $value;
        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);
        }
    }

全局搜一下貌似只有set_cookie()方法中的明文可控

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;
        $_COOKIE[$var] = $value;
        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);
        }
    }

set_cookie函数对于$v没有任何校验
那就set_cookie呗
全局跟一遍 太多了就不筛选了 这是Ven师傅利用的地方那就直接进吧

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

???
$json_str可控

    $json_str = $att_arr_exist ? $att_arr_exist.'||'.$json_str : $json_str;

显然直接利用不会存在这个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变量就理所应当的直接控制了
然后就回到了$arr['src']变量去绕过一开始的安全函数

然后就是看phpcms的路由了 ==> phpcms/libs/classes/param.class.php
默认的获取的路由是 ==> ?m=content&c=index&a=init ==> array('m'=>'content', 'c'=>'index', 'a'=>'init')
然后对相应的参数进行解析m是模型 c是类 a是方法

思路: 首先从attachment模型用attachment类中的swfupload_json获取加密的cookie ==> 再从index模型用down类中的init方法注入
在获取加密cookie前attachment中有自动加载会校验userid,因此在进行前还应该获取userid:

http://url/index.php?m=wap&c=index&a=init&siteid=1

获取有注入的加密cookie

http://url/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id=%*27+[sqli]

获取注入信息

/index.php?m=content&c=down&a=init&a_k=encoded_cookie

参考文章:
phpcms-v9中url路由规则文件分析
PHPCMS V9.6.0注入分析

(:3[▓▓] (:3[▓▓▓▓▓▓▓▓▓] (¦3[▓▓]
我宣布修仙失败

1 条评论

  1. [...]【挖坟】PHPCMS v9.6.0 SQL注入[...]

发表评论