先看漏洞函数: add_action

function add_action(){ 
[....]
        if($_POST['submit']){
[....]
            $_POST=$this->post_trim($_POST);
            $_POST['mans'] = (int)$_POST['mans'];
            $_POST = yun_iconv('utf-8','gbk',$_POST);
            $_POST['status']=$this->config['com_fast_status'];
            $_POST['ctime']=time();
            $_POST['edate']=strtotime("+".(int)$_POST['edate']." days");
            $password=md5(trim($_POST['password']));
            if(is_uploaded_file($_FILES['pic']['tmp_name'])){
                $upload=$this->upload_pic("../data/upload/once/",false);
                $pictures=$upload->picture($_FILES['pic']);
                $pic=str_replace("../data/upload/once/","data/upload/once/",$pictures);
                $_POST['pic']=$pic;
            }
            unset($_POST['submit']);
            $id=intval($_POST['id']);
            if($id<1){
[....]
            }else{
                $arr=$TinyM->GetOncejobOne(array('id'=>$id,'password'=>$password),array('field'=>'pic,id'));
                if($arr['id']){
                    $data['mans']=$_POST['mans'];
                    $data['title']=$_POST['title'];
                    $data['require']=$_POST['require'];
                    $data['companyname']=$_POST['companyname'];
                    $data['phone']=$_POST['phone'];
                    $data['linkman']=$_POST['linkman'];
                    $data['provinceid']=$_POST['provinceid'];
                    $data['cityid']=$_POST['cityid'];
                    $data['three_cityid']=$_POST['three_cityid'];
                    $data['address']=$_POST['address'];
                    $data['status']=$this->config['com_fast_status'];
                    $data['password']=$password;
                    $data['edate']=$_POST['edate'];
                    if ($_POST['pic']!=''){
                        $data['pic']=$_POST['pic'];
                    }else{
                        $data['pic']=$arr['pic'];
                    }
                    $nid=$TinyM->UpdateOncejob($data,array("id"=>$id));
                    if($this->config['com_fast_status']=="0"){
                        $msg="操作成功,等待审核!";
                    }else{
                        $msg="操作成功!";
                    }
[....]

首先在这个 cms 中,单引号传进去先转义然后被替换成了中文的,所以直接通过单引号逃逸是不现实的
在这个函数流程中,当满足有post发包并且 id >= 1的时候,他做了两件事,首先先从job_one 表中根据 id 获取数据,然后将 POST 包里的数据取出来插入数据库中进行更新,在此之前有这么个操作:
$_POST = yun_iconv('utf-8','gbk',$_POST);

这是为了出现乱码的情况,去转码一次
在整理 POST 包数据的时候,注意这一点

if ($_POST['pic']!=''){
                        $data['pic']=$_POST['pic'];
                    }else{
                        $data['pic']=$arr['pic'];
                    }

如果 pic 为空,那么会用数据库中的 pic 进行填充

我们先跟进 yun_iconv 里看看:

function yun_iconv($in_charset,$out_charset,$str){
    if(is_array($str)){
        foreach($str as $k=>$v){

              $str[$k]=iconv($in_charset,$out_charset,$v);

        }
        return $str;
    }else{

        return iconv($in_charset,$out_charset,$str);

    }    
}

就只是处理了一下数组,没什么特别的
那现在跟进 UpdateOncejob 函数看看,他是怎么更新数据的

function UpdateOncejob($Values=array(),$Where=array()){
        $WhereStr=$this->FormatWhere($Where);
        $ValuesStr=$this->FormatValues($Values);
        return $this->DB_update_all('once_job',$ValuesStr,$WhereStr);
    }

POST 里的数据被传入了 FormatValues 函数,继续跟

function FormatValues($Values){
        $ValuesStr='';
        foreach($Values as $k=>$v){
            if(preg_match("/^[a-zA-Z0-9_]+$/",$k)){
                 if(preg_match('/^[0-9]+$/', $k)){
[....]
                }else{
                    $ValuesStr.=',`'.$k.'`=\''.$v.'\'';
                }
            }

        }
        return substr($ValuesStr,1);
    }

这个函数简单来说就是 key 值不为纯数字字符串的话,就返回类似 key = " ' value' " 的字符串形式

而后又带入了 DB_update_all 函数里,跟进去

function DB_update_all($tablename, $value, $where = 1,$pecial=''){
        if($pecial!=$tablename){

            $where =$this->site_fetchsql($where,$tablename);
        } 
        $SQL = "UPDATE `" . $this->def . $tablename . "` SET $value WHERE ".$where; 
        $this->db->query("set sql_mode=''");
        $return=$this->db->query($SQL);
        return $return;
    }

直接将传入了 $value 字符串拼接进了 update 语句中
在从控制器到执行sql语句之前的流程中,他只是对 post 的值,包裹了一次单引号

因为起初有 iconv 转码所有的 POST,所以我们可不可以通过构造payload,去吃掉它加上的单引号

现在我们去看看他的过滤形式,首先,经过了 db.safety.php 文件里的过滤流程,因为流程太复杂,就不贴了,看看几处字符串替换,和正则匹配的地方

$str = preg_replace('/([\x00-\x08\x0b-\x0c\x0e-\x19])/', '', $str);

这里替换的都是 0开头的,也就替换到 0x19 ,高位字符一律不管

if(preg_match("/select|insert|update|delete|load_file|outfile/is", $str)){
        exit(safe_pape());
    }
    if(preg_match("/select|insert|update|delete|load_file|outfile/is", $str2)){
        exit(safe_pape());
    }

这里替换了关键字,而后也有对关键字进行中文替换、拦截,特别是 360waf 里

那么首先这个漏洞触发点他是一个 UPDATE ,入库的时候可以是 16 进制字符串,那么完美绕过所有关键字拦截,但是有个问题

"0x"=>"Ox"

0x 被处理成了 Ox
但是随后就经过了一次 strip_tags 处理,所以这里也是可以绕过的, 0<x>x 就行了</x>

那么目前就是,能够控制插入数据库的数据了,还没有引发注入,还记得开头说的对 pic 字段的处理吗,二次注入点就在这里

现在就可以构造payload了,选择在 'three_cityid' 段利用 iconv 去吃掉执行sql前为其值加上的单引号,造成单引号逃逸
我们选择 \xe98ca6 ,它转成 gbk 就变成了 \xe55c(从 gbk 表中随便选一个 5c 结尾的就行),如果我们在其后加上一个 \ 字符串就是 \xe98ca65c ,首先经过转义变成 \xe98ca65c5c,然后经过 iconv 处理变成了 \xe55c5c5c,而他数据库又是这样设置的:
character_set_client=binary
所以,目前更新数据库的时候变成了 \xe\\\ 多了一个 \ ,造成了单引号逃逸,从而可以在后面的字段值中载入 payload,插入 16 进制字符串

开始构造payload:
three_cityid 处插入 utf8 字符,然后自己添加 address 字段
二次注入的sql payload为如下字符串的16进制:
', qq=version(), email=user() where 1#
0x272c2071713d76657273696f6e28292c20656d61696c3d757365722829207768657265203123

然后这里有个问题就是,它上传用的是 multipart/form-data; 表单形式,那么想要把 \xe98ca6 传过去就不好弄,我选择修改它的 Content-Type 为application/x-www-form-urlencoded,这样就可以通过 url 编码传值过去,然后自己将 表格形式的键值改成 key=values 形式

额....因为相关规定,我们就只看看其触发过程中的 sql 记录

首先是 update 时:

接着就是漏洞触发点时的记录:

最后造成的影响:

源链接

Hacking more

...