xrCMS代码审计:存储型XSS
2019-12-31 10:28:52 Author: xz.aliyun.com(查看原文) 阅读量:317 收藏

审计迅睿cms,发现了两个XSS,漏洞已提交补天,分享一下

网站介绍

迅睿CMS是采用PHP7技术全新开发的产品,程序只能运行在PHP7的环境之上
迅睿CMS在原有FineCMS的基础上进行优化结构和吸取编程经验,采用国外CodeIgniter4框架。

建站

本地起phpstudy安装迅睿cms

后台路径:/admin.php

后台密码:admin admin

后台开启注册

漏洞测试

进入注册界面,构造如图所示数据

账号:" onclick="alert(1)"
密码:test

登录后台查看用户

可以看到有这个用户,当点击时触发代码

查看源代码

查看数据库,已经插入到了数据库中

代码审计

查看注册处代码(碍于篇幅仅放了关键代码)

......

$post = \Phpcmf\Service::L('input')->post('data', true);

...... # 进行格式之类的验证

# 表单验证
list($data, $return, $attach) = \Phpcmf\Service::L('Form')->validation($post, null, $field);

......

    $rt = \Phpcmf\Service::M('member')->register($groupid, [
        'username' => (string)$post['username'],
        'phone' => (string)$post['phone'],
        'email' => (string)$post['email'],
        'password' => dr_safe_password($post['password']),
    ], $data[1]);
    if ($rt['code']) {
        // 注册成功
        $this->member = $rt['data'];
        $remember = 0;
        // 保存本地会话
        \Phpcmf\Service::M('member')->save_cookie($this->member, $remember);
        // 附件归档
        SYS_ATTACHMENT_DB && $attach && \Phpcmf\Service::M('Attachment')->handle(
            $this->member['id'],
            \Phpcmf\Service::M()->dbprefix('member').'-'.$rt['code'],
            $attach
        );
        // 手机认证成功
        if ($this->member_cache['register']['sms']) {
            \Phpcmf\Service::M()->db->table('member_data')->where('id', $this->member['id'])->update(['is_mobile' => 1]);
        }
        $this->_json(1, 'ok', [
            'url' => urldecode(\Phpcmf\Service::L('input')->xss_clean($_POST['back'] ? $_POST['back'] : MEMBER_URL)),
            'sso' => \Phpcmf\Service::M('member')->sso($this->member, $remember),
            'member' => $this->member,
        ]);
    } else {
        $this->_json(0, $rt['msg'], ['field' => $rt['data']['field']]);
    }

``

跟踪post方法

// post解析
    public function post($name, $xss = true) {
        $value = isset($_POST[$name]) ? $_POST[$name] : false;
        return $xss ? $this->xss_clean($value) : $value;
    }

传入的data数据,其实就是获取的username,password和password2

一路跟踪xss_clean函数(代码很长)

public function xss_clean($str, $is_image = FALSE)
    {
        if (is_numeric($str)) {
            return $str;
        } elseif (!$str) {
            return '';
        } 

        // Is the string an array?
        if (is_array($str))
        {
            foreach ($str as $key => &$value)
            {
                $str[$key] = $this->xss_clean($value);
            }

            return $str;
        }

        // Remove Invisible Characters
        $str = remove_invisible_characters($str);

        /*
         * URL Decode
         *
         * Just in case stuff like this is submitted:
         *
         * <a href="http://%77%77%77%2E%67%6F%6F%67%6C%65%2E%63%6F%6D">Google</a>
         *
         * Note: Use rawurldecode() so it does not remove plus signs
         */
        if (stripos($str, '%') !== false)
        {
            do
            {
                $oldstr = $str;
                $str = rawurldecode($str);
                $str = preg_replace_callback('#%(?:\s*[0-9a-f]){2,}#i', array($this, '_urldecodespaces'), $str);
            }
            while ($oldstr !== $str);
            unset($oldstr);
        }

        /*
         * Convert character entities to ASCII
         *
         * This permits our tests below to work reliably.
         * We only convert entities that are within tags since
         * these are the ones that will pose security problems.
         */
        $str = preg_replace_callback("/[^a-z0-9>]+[a-z0-9]+=([\'\"]).*?\\1/si", array($this, '_convert_attribute'), $str);
        $str = preg_replace_callback('/<\w+.*/si', array($this, '_decode_entity'), $str);

        // Remove Invisible Characters Again!
        $str = remove_invisible_characters($str);

        /*
         * Convert all tabs to spaces
         *
         * This prevents strings like this: ja  vascript
         * NOTE: we deal with spaces between characters later.
         * NOTE: preg_replace was found to be amazingly slow here on
         * large blocks of data, so we use str_replace.
         */
        $str = str_replace("\t", ' ', $str);

        // Capture converted string for later comparison
        $converted_string = $str;

        // Remove Strings that are never allowed
        $str = $this->_do_never_allowed($str);

        /*
         * Makes PHP tags safe
         *
         * Note: XML tags are inadvertently replaced too:
         *
         * <?xml
         *
         * But it doesn't seem to pose a problem.
         */
        if ($is_image === TRUE)
        {
            // Images have a tendency to have the PHP short opening and
            // closing tags every so often so we skip those and only
            // do the long opening tags.
            $str = preg_replace('/<\?(php)/i', '<?\\1', $str);
        }
        else
        {
            $str = str_replace(array('<?', '?'.'>'), array('<?', '?>'), $str);
        }

        /*
         * Compact any exploded words
         *
         * This corrects words like:  j a v a s c r i p t
         * These words are compacted back to their correct state.
         */
        $words = array(
            'javascript', 'expression', 'vbscript', 'jscript', 'wscript',
            'vbs', 'script', 'base64', 'applet', 'alert', 'document',
            'write', 'cookie', 'window', 'confirm', 'prompt', 'eval'
        );

        foreach ($words as $word)
        {
            $word = implode('\s*', str_split($word)).'\s*';

            // We only want to do this when it is followed by a non-word character
            // That way valid stuff like "dealer to" does not become "dealerto"
            $str = preg_replace_callback('#('.substr($word, 0, -3).')(\W)#is', array($this, '_compact_exploded_words'), $str);
        }

        /*
         * Remove disallowed Javascript in links or img tags
         * We used to do some version comparisons and use of stripos(),
         * but it is dog slow compared to these simplified non-capturing
         * preg_match(), especially if the pattern exists in the string
         *
         * Note: It was reported that not only space characters, but all in
         * the following pattern can be parsed as separators between a tag name
         * and its attributes: [\d\s"\'`;,\/\=\(\x00\x0B\x09\x0C]
         * ... however, remove_invisible_characters() above already strips the
         * hex-encoded ones, so we'll skip them below.
         */
        do
        {
            $original = $str;

            if (preg_match('/<a/i', $str))
            {
                $str = preg_replace_callback('#<a(?:rea)?[^a-z0-9>]+([^>]*?)(?:>|$)#si', array($this, '_js_link_removal'), $str);
            }

            if (preg_match('/<img/i', $str))
            {
                $str = preg_replace_callback('#<img[^a-z0-9]+([^>]*?)(?:\s?/?>|$)#si', array($this, '_js_img_removal'), $str);
            }

            if (preg_match('/script|xss/i', $str))
            {
                $str = preg_replace('#</*(?:script|xss).*?>#si', '[removed]', $str);
            }
        }
        while ($original !== $str);
        unset($original);

        /*
         * Sanitize naughty HTML elements
         *
         * If a tag containing any of the words in the list
         * below is found, the tag gets converted to entities.
         *
         * So this: <blink>
         * Becomes: <blink>
         */
        $pattern = '#'
            .'<((?<slash>/*\s*)((?<tagName>[a-z0-9]+)(?=[^a-z0-9]|$)|.+)' // tag start and name, followed by a non-tag character
            .'[^\s\042\047a-z0-9>/=]*' // a valid attribute character immediately after the tag would count as a separator
            // optional attributes
            .'(?<attributes>(?:[\s\042\047/=]*' // non-attribute characters, excluding > (tag close) for obvious reasons
            .'[^\s\042\047>/=]+' // attribute characters
            // optional attribute-value
                .'(?:\s*=' // attribute-value separator
                    .'(?:[^\s\042\047=><`]+|\s*\042[^\042]*\042|\s*\047[^\047]*\047|\s*(?U:[^\s\042\047=><`]*))' // single, double or non-quoted value
                .')?' // end optional attribute-value group
            .')*)' // end optional attributes group
            .'[^>]*)(?<closeTag>\>)?#isS';

        // Note: It would be nice to optimize this for speed, BUT
        //       only matching the naughty elements here results in
        //       false positives and in turn - vulnerabilities!
        do
        {
            $old_str = $str;
            $str = preg_replace_callback($pattern, array($this, '_sanitize_naughty_html'), $str);
        }
        while ($old_str !== $str);
        unset($old_str);

        /*
         * Sanitize naughty scripting elements
         *
         * Similar to above, only instead of looking for
         * tags it looks for PHP and JavaScript commands
         * that are disallowed. Rather than removing the
         * code, it simply converts the parenthesis to entities
         * rendering the code un-executable.
         *
         * For example: eval('some code')
         * Becomes: eval('some code')
         */
        $str = preg_replace(
            '#(alert|prompt|confirm|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si',
            '\\1\\2(\\3)',
            $str
        );

        // Same thing, but for "tag functions" (e.g. eval`some code`)
        // See https://github.com/bcit-ci/CodeIgniter/issues/5420
        $str = preg_replace(
            '#(alert|prompt|confirm|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)`(.*?)`#si',
            '\\1\\2`\\3`',
            $str
        );

        // Final clean up
        // This adds a bit of extra precaution in case
        // something got through the above filters
        $str = $this->_do_never_allowed($str);

        /*
         * Images are Handled in a Special Way
         * - Essentially, we want to know that after all of the character
         * conversion is done whether any unwanted, likely XSS, code was found.
         * If not, we return TRUE, as the image is clean.
         * However, if the string post-conversion does not matched the
         * string post-removal of XSS, then it fails, as there was unwanted XSS
         * code found and removed/changed during processing.
         */
        if ($is_image === TRUE)
        {
            return ($str === $converted_string);
        }

        return $str;
    }

,过滤了/%0[0-8bcef]/,'/%1[0-9a-f]/','/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S',对特殊字符进行转义,将\t过滤为空,过滤了<a,<img,<script|xss标签,神奇的是下面这串代码

function _compact_exploded_words($matches)
{
    return preg_replace('/\s+/s', '', $matches[1]).$matches[2];
}

$words = array(
    'javascript', 'expression', 'vbscript', 'jscript', 'wscript',
    'vbs', 'script', 'base64', 'applet', 'alert', 'document',
    'write', 'cookie', 'window', 'confirm', 'prompt', 'eval'
);
foreach ($words as $word)
{
    $word = implode('\s*', str_split($word)).'\s*';
    $str = preg_replace_callback('#('.substr($word, 0, -3).')(\W)#is', '_compact_exploded_words', $str);
}

乍一看像是过滤了XSS的关键字,事实上它只是过滤了关键字中的空白符,类似于这种a l e r t(1),会被过滤为alert(1),但对关键字并没有任何过滤

下面才是针对关键字做的过滤,理论上是配合上面的空白符删除配合使用的

#(alert|prompt|confirm|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)`(.*?)`#si

匪夷所思的是代码要求关键字后必须跟两个反引号,即alert`1`会被过滤,但普通的alert(1)反而只是进行了html实体转换,而onclick中的代码是被当做JavaScript代码执行的,即使符号被转义也可以执行

继续往下

下面的验证字段参数为除了$post变量外,其他为空值,相当于没有验证,略过

继续向下
跟踪register方法

......
    $member['name'] = !$member['name'] ? '' : dr_strcut($member['name'], intval(\Phpcmf\Service::C()->member_cache['register']['cutname']), '');
    $member['salt'] = substr(md5(rand(0, 999)), 0, 10); // 随机10位密码加密码
    $member['password'] = $member['password'] ? md5(md5($member['password']).$member['salt'].md5($member['password'])) : '';
    $member['money'] = 0;
    $member['freeze'] = 0;        $member['spend'] = 0;
    $member['score'] = 0;
    $member['experience'] = 0;        $member['regip'] = (string)\Phpcmf\Service::L('input')->ip_address();
    $member['regtime'] = SYS_TIME;
    $member['randcode'] = rand(100000, 999999);
    !$member['username'] && $member['username'] = '';
    $rt = $this->table('member')->insert($member);
......

由于根本不存在name字段,因此直接将数据插入到表中,插入途中也没有任何过滤

可以直接闭合双引号插入XSS代码

需要注意的是该账号只能登录一次,即退出后再次登录会提示该用户不存在,应该是因为闭合了某个标签,具体原因并未深入分析

另外一个存储型XSS

除了注册有一个,在登录时还有一个存储型XSS,不过它并不能在后台中起作用,只能在用户的个人界面,因此不再分析,只贴出来供各位表哥了解一下

注册一个test用户

漏洞测试

登录时抓包并将user-agent头修改为如图所示代码

账号管理->登陆记录中即可触发代码

以下为效果图

但是这个洞其实并没有实际意义2333,因为在后台,它是这样的

根本没有标签,不过可能会有别的利用方式,,如果有所发现再来更新文章


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