zoukankan      html  css  js  c++  java
  • [代码审计]云ec电商系统代码审计

    0x00 前言

    看了一下博客内最新的文章,竟然是3月28号的,一个多月没写文章了,博客都长草了。

    主要是临近毕业,事情繁多,也没有啥时间和心情静下来写。。

    不过现在的话,毕业的东西告一段落了,几乎没啥事了,总算能静下来好好学习。

    发了篇文章在t00ls,不过是精简版的,有些地方没有细讲。之前也有伙伴留言说,文章放到t00ls,有些没账号的朋友看不到。这里我搬运一下。

    0x01 基础环境
    审计版本为V1.2
    最后更新时间:2018-04-20
    用到的工具:vscode,phpstudy

    0x02 前台注入漏洞分析
    漏洞其实很简单,就是我们常见的获取ip没过滤的问题的,但这里需要点条件。
    看到incfuncitonfunctions_admin.php getip()函数

    function getip(){
           static $ip = null;
           if($ip)
             return $ip; // 不需要计算第二次.
           $ip=false;
           if($_SERVER['HTTP_VIA']){ //使用了代理
               if(!empty($_SERVER["HTTP_CLIENT_IP"])){ //设置client_ip头
                    $ip = $_SERVER["HTTP_CLIENT_IP"];
               }else if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])){
                    $ips = explode (", ", $_SERVER['HTTP_X_FORWARDED_FOR']);
                    if ($ip){
                        array_unshift($ips, $ip); $ip = '';
                    }
                    $ipss = count($ips);
                    for ($i = 0; $i < $ipss; $i++){
                         if (!preg_match('/^(10|172.16|192.168)./', $ips[$i])){
                                   $ip = $ips[$i];
                                   break;
                         }
                    }
               }
           }else{
                $ip = $_SERVER['REMOTE_ADDR'];
           }
             
           # 更兼容的获取.
            if(!$ip)//if $ip为false
            if(!$ip = getenv("REMOTE_ADDR"))
            if (!$ip = getenv("HTTP_CLIENT_IP"))
            if(!$ip = getenv("HTTP_X_FORWARDED_FOR"))
                $ip = '';
           return $ip;
    }

    看到几个判断语句,先是判断是否使用了代理访问,如果没有使用直接用$_SERVER['REMOTE_ADDR']来获取ip,这个我们是没办法控制的。
    如果使用了代理访问的话,进到判断$_SERVER['HTTP_CLIENT_IP']是否为空,不为空,直接设置$ip=$_SERVER['HTTP_CLIENT_IP'],而这个头我们是可以通过Client-Ip进行控制。
    因为这里是SERVER变量,绕过了对GPC的过滤,看到过滤文件incfunctionglobal.php

    $getfilter="\b(and|or)\b.+?(>|<|=|in|like)|\/\*.+?\*\/|<\s*script\b|\bEXEC\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\s+(TABLE|DATABASE)";
    $postfilter="\b(and|or)\b.{1,6}?(=|>|<|\bin\b|\blike\b)|\/\*.+?\*\/|<\s*script\b|\bEXEC\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\s+(TABLE|DATABASE)";
    $cookiefilter="\b(and|or)\b.{1,6}?(=|>|<|\bin\b|\blike\b)|\/\*.+?\*\/|<\s*script\b|\bEXEC\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\s+(TABLE|DATABASE)";
    
    function StopAttack($StrFiltKey,$StrFiltValue,$ArrFiltReq){  
            if(is_array($StrFiltValue))
            {
                    $StrFiltValue=implode($StrFiltValue);
            }                
            if (preg_match("/".$ArrFiltReq."/is",urldecode($StrFiltValue))){
                            print "网址有误~";
                            exit();
            }      
    }  
    
    foreach($_GET as $key=>$value){
            StopAttack($key,$value,$getfilter);
    }
    if ($_GET["p"]!=='admin'){
            foreach($_POST as $key=>$value){ 
                    StopAttack($key,$value,$postfilter);
            }
    }
    
    foreach($_COOKIE as $key=>$value){ 
            StopAttack($key,$value,$cookiefilter);
    }
    unset($_GET['_SESSION']);
    unset($_POST['_SESSION']);
    unset($_COOKIE['_SESSION']);

    全局寻找getip()使用的位置。

    找到了一处比较好利用的一个点,看到文件incmoduleuser.php

    elseif ($act == 'add_comment_reply')
            {
                    $content = isset($content) ? trim($content) : '';
                    $cid = isset($cid) ? intval($cid) : '';
                    /*if(intval($gid)==0)
                    {
                            $res['err'] = '获取商品号失败';
                            die(json_encode_yec($res));
                    }*/
                    if(intval($pid)==0)
                    {
                            $res['err'] = '回复的对象错误';
                            die(json_encode_yec($res));
                    }
                    if(!isset($content) || $content == '')
                    {
                            $res['err'] = '请输入回复内容';
                            die(json_encode_yec($res));
                    }
                    elseif(mb_strlen($content,'utf-8')>120) {
                            $res['err'] = '字数请控制在120个以内';
                            die(json_encode_yec($res));
                    }
                    if($ym_uid==0) //需要登录
                    {
                            $ym_uid =check_login(1);
                            if($ym_uid==0)
                            {
                                    $res['url'] = 'login.html';
                                    die(json_encode_yec($res));
                            }
                    }
                    dbc();                
                    if(check_comment_reply($ym_uid, getip())==false)
                    {
                            $res['err'] = '您评论的有点频繁了,请休息一小时再来吧~';
                            die(json_encode_yec($res));
                    }
                    $reply_id = get_comment_uid(intval($pid), intval($ptype));
                    $id = add_comment_reply($cid,$ym_uid, $reply_id, intval($pid), intval($ptype), role_user, $content);
                    $res['res'] = $id;
            }

    这里是用户评论的逻辑代码,看到最后一个if判断中,使用到了getip()函数,跟进check_comment_reply函数,inclibuser.php

    function check_comment_reply($uid, $ip='')
    {
            global $db;
            $where ='';
            $row_uid = $db->query("select count(*) count from ".$db->table('comment_reply')." where addtime>=".strtotime(date('Y-m-d H:i:s',strtotime("-1 hour")))." and uid=".$uid);
            $row_ip = $db->query("select count(*) count from ".$db->table('comment_reply')." where addtime>=".strtotime(date('Y-m-d H:i:s',strtotime("-1 hour")))." and ip='".$ip."'");
    
            if( (!$row_uid || count($row_uid)==0 || intval($row_uid['count']) < 10) && (!$row_ip || count($row_ip)==0 || intval($row_ip['count']) < 10))        
            {
                    return true;
            }
            
            return false;
    }

    没有过滤带进去了,那么就可以注入了。getip()有很多地方用到,但是仅限于使用query,查询的。insert,update方法都在底层做了过滤,不展开讲。而且这个站默认是开启报错的,可以直接利用报错获取数据。
    那么整理一下漏洞利用条件:
    1,需要开启会员注册,因为注册要发送验证码,很多商城可能已经废了。
    2,需要代理访问。
    妈的,不知道谁改了demo站演示账号的密码,找了个站演示:

    文字表达一下利用:
    请求URL:/user.html?act=add_comment_reply&content=1&cid=1&pid=1
    header:Client-Ip:注入payload
    获取管理账号

    获取密码,分两段获取,最后一个字符获取不出来。

    最后的密码为:f9c8c87e0a1f9d085b1008010fbd7781
    这个系统密码的加密方式是这样的:

    //加密字符串
    function encryptStr($val, $salt='')
    {        
            return md5(md5(trim($val)).$salt);
    }

    加了盐,我们需要把盐也注出来:

    之后就是拉去解一下md5,md5解不出也没有关系。
    因为这个系统底层用的是pdo,支持多语句执行。
    那么可以直接插入一个管理员,或者修改已有管理员的密码,搞完之后修改回来就好了。这里以修改管理员密码为例:

     进入后台

    0x03 后台任意文件包含

    后台有一个任意文件包含,上传图片马,访问URL

    http://example.com/admin.html?do=express_track&oauth_code=1&sp_file=图片马路径
    

    这里主要是因为sp_file没有定义,系统存在全局变量注册,可直接定义变量,看到incadmin_incpageexpress_track.php

    <?php
    if (!defined('in_mx')) {exit('Access Denied');}
     
    checkAuth($login_id, 'system');//权限检测 
     
     
    //$sp_file = plugin."oauth/".$code.'/'.$code.'.php';
    $path = plugin."express/";
    $dirs =get_dir($path); 
    
    if($oauth_code)
    {
        if(file_exists($sp_file)==false){message("获取不到文件 ".$code.'.php');}
        require($sp_file);//任意文件包含
        $row = $db->fetch('express_track', '*',  array('code' => $code));
    }
    
    $row = $db->fetchall('express_track', '*'); 
    
     //die("a".$p);
    
         
    ?>

    $sp_file未定义,无过滤,直接require,造成了文件包含。这个利用前台的头像+后台的csrf可以直接getshell。

    这个系统还有一个session固定的漏洞,结合组合拳直接进后台。

    还有之前和Adog师傅聊,他说有xss,我也没找,这里就不细说了。

    上面的两个利用无须xss,只有一个img标签的请求即可。利用原理参考我的这篇文章([代码审计]yxcms从伪xss到getshell,https://www.cnblogs.com/r00tuser/p/8419300.html)

    img请求+csrf+get请求任意文件包含=getshell

    img请求+csrf+get请求session固定=进后台

    0x04 总结

    这个系统还有很多问题,不一一细讲,抛砖引玉。

  • 相关阅读:
    map方法,以及filter方法的使用
    detach与remove区别,以及detach保留被删除的元素数据,使用
    jQuery 文档操作
    javascript 清空数组的方法
    jquery遍历数组的方式
    Oracle表空间不足处理
    css 文本超出2行就隐藏并且显示省略号
    Vim中的寄存器
    spacemacs怎样配置编辑器显示行号?
    Docker考前突击
  • 原文地址:https://www.cnblogs.com/r00tuser/p/9014869.html
Copyright © 2011-2022 走看看