### 简要描述: 我擦 写完标题后发现标题如此的长。 应该是qibo中用得最多的系统了把。 与之前我发的那个有所不同。 Fuzz。 发现qibo是不是换人了? 给分给的越来越低? 之前18 到 10 到现在的5分了? 用demo来演示演示把。 应该可以直接登录后台 懒得弄了。 如果这个洞还不给20的话 我只能呵呵了。 ### 详细说明: http://bbs.qibosoft.com/down2.php?v=v7#down 下载地址 刚下载的。 在inc/job/download.php中 ``` $url=trim(base64_decode($url)); $fileurl=str_replace($webdb[www_url],"",$url); if( eregi(".php",$fileurl) && is_file(ROOT_PATH."$fileurl") ){ die("ERR"); } if(!$webdb[DownLoad_readfile]){ $fileurl=strstr($url,"://")?$url:tempdir($fileurl); header("location:$fileurl"); exit; } if( is_file(ROOT_PATH."$fileurl") ){ $filename=basename($fileurl); $filetype=substr(strrchr($filename,'.'),1); $_filename=preg_replace("/([\d]+)_(200[\d]+)_([^_]+)\.([^\.]+)/is","\\3",$filename); if(eregi("^([a-z0-9=]+)$",$_filename)&&!eregi("(jpg|gif|png)$",$filename)){ $filename=urldecode(base64_decode($_filename)).".$filetype"; } ob_end_clean(); header('Last-Modified: '.gmdate('D, d M Y H:i:s',time()).' GMT'); header('Pragma: no-cache');...
### 简要描述: 我擦 写完标题后发现标题如此的长。 应该是qibo中用得最多的系统了把。 与之前我发的那个有所不同。 Fuzz。 发现qibo是不是换人了? 给分给的越来越低? 之前18 到 10 到现在的5分了? 用demo来演示演示把。 应该可以直接登录后台 懒得弄了。 如果这个洞还不给20的话 我只能呵呵了。 ### 详细说明: http://bbs.qibosoft.com/down2.php?v=v7#down 下载地址 刚下载的。 在inc/job/download.php中 ``` $url=trim(base64_decode($url)); $fileurl=str_replace($webdb[www_url],"",$url); if( eregi(".php",$fileurl) && is_file(ROOT_PATH."$fileurl") ){ die("ERR"); } if(!$webdb[DownLoad_readfile]){ $fileurl=strstr($url,"://")?$url:tempdir($fileurl); header("location:$fileurl"); exit; } if( is_file(ROOT_PATH."$fileurl") ){ $filename=basename($fileurl); $filetype=substr(strrchr($filename,'.'),1); $_filename=preg_replace("/([\d]+)_(200[\d]+)_([^_]+)\.([^\.]+)/is","\\3",$filename); if(eregi("^([a-z0-9=]+)$",$_filename)&&!eregi("(jpg|gif|png)$",$filename)){ $filename=urldecode(base64_decode($_filename)).".$filetype"; } ob_end_clean(); header('Last-Modified: '.gmdate('D, d M Y H:i:s',time()).' GMT'); header('Pragma: no-cache'); header('Content-Encoding: none'); header('Content-Disposition: attachment; filename='.$filename); header('Content-type: '.$filetype); header('Content-Length: '.filesize(ROOT_PATH."$fileurl")); readfile(ROOT_PATH."$fileurl"); }else{ if(eregi(".php",$fileurl)){ header("location:$fileurl"); exit; } $filename=basename($fileurl); $filetype=substr(strrchr($filename,'.'),1); $fileurl=strstr($url,"://")?$url:tempdir($fileurl); ob_end_clean(); header('Last-Modified: '.gmdate('D, d M Y H:i:s',time()).' GMT'); header('Pragma: no-cache'); header('Content-Encoding: none'); header('Content-Disposition: attachment; filename='.$filename); header('Content-type: '.$filetype); readfile($fileurl); ``` ``` $url=trim(base64_decode($url)) $fileurl=str_replace($webdb[www_url],"",$url); if( eregi(".php",$fileurl) && is_file(ROOT_PATH."$fileurl") ){ die("ERR"); ``` 这里由于是解码后再匹配 所以不能靠编码绕过。 只要匹配到.php就退出 。 测试了一下.php. 也会被匹配出。 这里还开启了i模式 所以像phP之类的大小写绕过也没办法。 难道真的没办法了? ``` if( is_file(ROOT_PATH."$fileurl") ){ $filename=basename($fileurl); $filetype=substr(strrchr($filename,'.'),1); $_filename=preg_replace("/([\d]+)_(200[\d]+)_([^_]+)\.([^\.]+)/is","\\3",$filename); if(eregi("^([a-z0-9=]+)$",$_filename)&&!eregi("(jpg|gif|png)$",$filename)){ $filename=urldecode(base64_decode($_filename)).".$filetype"; } ob_end_clean(); header('Last-Modified: '.gmdate('D, d M Y H:i:s',time()).' GMT'); header('Pragma: no-cache'); header('Content-Encoding: none'); header('Content-Disposition: attachment; filename='.$filename); header('Content-type: '.$filetype); header('Content-Length: '.filesize(ROOT_PATH."$fileurl")); readfile(ROOT_PATH."$fileurl"); ``` 在这里调用了is_file这函数来检测文件是否存在,如果存在的话才会进入这语句块。 由于匹配出.php 就会退出。 能有什么办法呢? 这里我们来fuzz is_file这函数一下。 ``` <?php for ($i=0; $i<255; $i++) { $yu = '1.ph' . chr($i); $yu1 = @is_file($yu); if (!empty($yu1)){ echo chr($i); echo "</br>"; } } ?> ``` 在本地新建一个1.php的文件。 然后is_file 看看有神么能输出来。 [<img src="https://images.seebug.org/upload/201406/271903091d064b4d8feb5aa6c2218fa0ad632110.jpg" alt="v1.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201406/271903091d064b4d8feb5aa6c2218fa0ad632110.jpg) 可以看到除开 P p 还有其他的 因为开启了i 所以P p 都不行 来试试< ``` <?Php $a=$_GET[a]; $b=is_file($a); var_dump($b); ``` [<img src="https://images.seebug.org/upload/201406/27190452c8906cc9bc6653b3f116b8104ef4eb26.jpg" alt="v2.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201406/27190452c8906cc9bc6653b3f116b8104ef4eb26.jpg) [<img src="https://images.seebug.org/upload/201406/27190516416807f66e820e1f092e89befc5dcc76.jpg" alt="v3.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201406/27190516416807f66e820e1f092e89befc5dcc76.jpg) 可以看到1.ph< 返回了true 这样不就可以绕过这个的过滤了? 因为我看英文看不怎么懂。。 那些什么翻译 翻译来又太蛋疼了 一大堆翻译错误的。 以下是我的理解 可能有错 也请大牛来指导指导了。 因为当PHP解析器解析这些函数的时候 会调用winapi 调用了Winapi的函数Findfirstfile 然后<字符被转换成了* 成了通配符。 所以导致1.ph< 找到了1.php。 也就导致了这个漏洞的产生。 这里不止is_file函数调用了这个api 大部分的函数都调用了这个api [<img src="https://images.seebug.org/upload/201406/271919565b5870796f3d04ffe7d472abc5432879.jpg" alt="v4.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201406/271919565b5870796f3d04ffe7d472abc5432879.jpg) 可以看到unlink函数用这方法就不行。 没调用这api的函数大概有unlink、rename、rmdir就这三个了。 其他的函数基本都调用了。 ___ 上面那个介绍完了, 继续回到qibocms。。 ``` if( is_file(ROOT_PATH."$fileurl") ){ $filename=basename($fileurl); $filetype=substr(strrchr($filename,'.'),1); $_filename=preg_replace("/([\d]+)_(200[\d]+)_([^_]+)\.([^\.]+)/is","\\3",$filename); if(eregi("^([a-z0-9=]+)$",$_filename)&&!eregi("(jpg|gif|png)$",$filename)){ $filename=urldecode(base64_decode($_filename)).".$filetype"; } ob_end_clean(); header('Last-Modified: '.gmdate('D, d M Y H:i:s',time()).' GMT'); header('Pragma: no-cache'); header('Content-Encoding: none'); header('Content-Disposition: attachment; filename='.$filename); header('Content-type: '.$filetype); header('Content-Length: '.filesize(ROOT_PATH."$fileurl")); readfile(ROOT_PATH."$fileurl"); ``` 在这里通过is_file的判断后。 ``` $filename=basename($fileurl); $filetype=substr(strrchr($filename,'.'),1); $_filename=preg_replace("/([\d]+)_(200[\d]+)_([^_]+)\.([^\.]+)/is","\\3",$filename); if(eregi("^([a-z0-9=]+)$",$_filename)&&!eregi("(jpg|gif|png)$",$filename)){ $filename=urldecode(base64_decode($_filename)).".$filetype"; } ``` 对这些有进行了各种处理, 但是我没搞懂对这些的处理有什么用? readfile(ROOT_PATH."$fileurl") 最后带入readfile 的是$fileurl。 Come on 利用来吧。 ``` $url=trim(base64_decode($url)); $fileurl=str_replace($webdb[www_url],"",$url); if( eregi(".php",$fileurl) && is_file(ROOT_PATH."$fileurl") ){ die("ERR"); } ``` 这里由于会先解码所以首先要自己编码一次。 这里我们来下载data/config.php 这文件。 对data/config.php base64 encode 试试 [<img src="https://images.seebug.org/upload/201406/27192432c1fd8c5fd62a85ad75e042bbf4c9b914.jpg" alt="v5.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201406/27192432c1fd8c5fd62a85ad75e042bbf4c9b914.jpg) 被匹配出了 再对data/config.ph< base64 encode [<img src="https://images.seebug.org/upload/201406/27192611a2ae7ef161efc6bed25a93106b26a95a.jpg" alt="v6.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201406/27192611a2ae7ef161efc6bed25a93106b26a95a.jpg) 成功下载到配置文件 _ 这里如何让任意文件下载变成注入? 这里qibocms 里面有一个加密解码的函数 ``` function mymd5($string,$action="EN",$rand=''){ //字符串加密和解密 global $webdb; if($action=="DE"){//处理+号在URL传递过程中会异常 $string = str_replace('QIBO|ADD','+',$string); } $secret_string = $webdb[mymd5].$rand.'5*j,.^&;?.%#@!'; //绝密字符串,可以任意设定 if(!is_string($string)){ $string=strval($string); } if($string==="") return ""; if($action=="EN") $md5code=substr(md5($string),8,10); else{ $md5code=substr($string,-10); $string=substr($string,0,strlen($string)-10); } //$key = md5($md5code.$_SERVER["HTTP_USER_AGENT"].$secret_string); $key = md5($md5code.$secret_string); $string = ($action=="EN"?$string:base64_decode($string)); $len = strlen($key); $code = ""; for($i=0; $i<strlen($string); $i++){ $k = $i%$len; $code .= $string[$i]^$key[$k]; } $code = ($action == "DE" ? (substr(md5($code),8,10)==$md5code?$code:NULL) : base64_encode($code)."$md5code"); if($action=="EN"){//处理+号在URL传递过程中会异常 $code = str_replace('+','QIBO|ADD',$code); } return $code; } ``` 这里的key是保存到配置文件里面的, 当我们拿到key过后就可以调用这函数自己来生成一个加密的字符串。 再找哪里调用了这函数来解密的。 这样就无视了qibocms的全局转义。 key 就是保存到data/config.php里面的 刚才通过任意文件下载已经拿到了。 [<img src="https://images.seebug.org/upload/201406/27192942d64879a3f7de1ac23c96601a80b7503c.jpg" alt="v7.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201406/27192942d64879a3f7de1ac23c96601a80b7503c.jpg) 还是给官方的key打个码、 来找找哪里调用了这函数的。 首先在member/yz.php里面 ``` elseif($action=='mobphone2') { if($lfjdb[mob_yz]){ showerr("请不要重复验证手机号码!"); } if(!$yznum){ showerr("请输入验证码"); }elseif(!$md5code){ showerr("资料有误"); }else{ unset($code,$mobphone,$uid); list($code,$mobphone,$uid)=explode("\t",mymd5($md5code,"DE") ); if($code!=$yznum||$uid!=$lfjuid){ showerr("验证码不对"); } } add_user($lfjuid,$webdb[YZ_MobMoney],'手机号码审核奖分'); $db->query("UPDATE {$pre}memberdata SET mobphone='$mobphone',mob_yz='1' WHERE uid='$lfjuid'"); refreshto("yz.php?job=mob","恭喜你,你的手机号码成功通过审核,你同时得到 {$webdb[YZ_MobMoney]} 个积分奖励!",10); ``` 这里调用了mymd5 而且是decode 所以解码后就能直接注入了。 而且可以发现update的表是memberdata 这个表里面groupid column 就是用来判断是不是管理员的。 而且$mobphone 是解码后来的 而且直接在set位 这里只要稍微构造一下 就可以直接update groupid=3 然后就提升自己为管理员了。 这里在之前的图片系统里提到过 就不多说了。 再继续来看看。 在inc/common.inc.php中 登录后台的时候也调用了这个 ``` if($_COOKIE["adminID"]&&$detail=mymd5($_COOKIE["adminID"],'DE',$onlineip)){ unset($_uid,$_username,$_password); list($_uid,$_username,$_password)=explode("\t",$detail); $lfjdb=$db->get_one("SELECT * FROM {$pre}memberdata WHERE uid='$_uid' AND username='$_username'"); } ``` mymd5($_COOKIE["adminID"],'DE',$onlineip) 这里解码的时候还调用了$onlineip进了第三个参数 $secret_string = $webdb[mymd5].$rand.'5*j,.^&;?.%#@!'; //绝密字符串,可以任意设定 可以看到第三个参数是进了这个变量然后带入了加密中 看看$onlineip怎么来的。 来看看全局文件 ``` if($_SERVER['HTTP_CLIENT_IP']){ $onlineip=$_SERVER['HTTP_CLIENT_IP']; }elseif($_SERVER['HTTP_X_FORWARDED_FOR']){ $onlineip=$_SERVER['HTTP_X_FORWARDED_FOR']; }else{ $onlineip=$_SERVER['REMOTE_ADDR']; } $onlineip = preg_replace("/^([\d\.]+).*/", "\\1", filtrate($onlineip)); preg_match("/[\d\.]{7,15}/", $onlineip, $onlineipArray); $onlineip = $onlineipArray[0] ? $onlineipArray[0] : '0.0.0.0'; ``` 可以看到是获取的xff 但是后面用了正则来验证ip是否合法 如果不合法的话 就return的是0.0.0.0 这里我们就随便让xff不合法就行了 然后把0.0.0.0 带入到加密函数当中 不多说了 直接调用一下函数生成一下加密的字符串。 [<img src="https://images.seebug.org/upload/201406/27195056b7e0174ed7c5cd0c673d589860007e16.jpg" alt="v8.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201406/27195056b7e0174ed7c5cd0c673d589860007e16.jpg) 在测试demo的时候发现竟然不报错。 [<img src="https://images.seebug.org/upload/201406/27195407606734ba3781246cc340534a4962b804.jpg" alt="v9.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201406/27195407606734ba3781246cc340534a4962b804.jpg) 这怎么可能呢? 后面想了一想 $secret_string = $webdb[mymd5].$rand.'5*j,.^&;?.%#@!'; //绝密字符串,可以任意设定 $rand 后面设定的是可以任意设定的 可能demo修改了。 然后果断继续利用刚才的方法下载inc/function.inc.php ``` function mymd5($string,$action="EN",$rand=''){ //字符串加密和解密 global $webdb; $secret_string = $webdb[mymd5].$rand.'5*j,.^&;?.%#@!=67987d'; //绝密字符串,可以任意设定 ``` 呵呵 demo果然修改了。 把这个修改后 继续调用一下这函数 再生成一下语句。 [<img src="https://images.seebug.org/upload/201406/271957457aa6f46dc32ea38076e4324c6f3103e1.jpg" alt="v10.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201406/271957457aa6f46dc32ea38076e4324c6f3103e1.jpg) 成功报错。 后面的不用多说了。 生成一个加密的报错注入的语句就能注入了。 不想多说。 这里应该可以直接登录后台,懒得弄了。 ### 漏洞证明: 是不需要登录后台的 是在后台登录页面 等 其他多个地方注入。 见上面。