国庆时候想着给春秋出题顺手挖了下 没想到第五空间线下awd用上了 成功开局就把全场打down了(不是
然后就跟春秋的人说把题撤回掉了… 这两天闲了 写下分析

pbootcms的模版引擎属于是大家的老朋友了
直接放代码吧

    public function parserAfter(content)
    {
        // 默认页面信息替换content = str_replace('{pboot:pagetitle}', this->config('other_title') ?: '{pboot:sitetitle}-{pboot:sitesubtitle}',content);
        content = str_replace('{pboot:pagekeywords}', '{pboot:sitekeywords}',content);
        content = str_replace('{pboot:pagedescription}', '{pboot:sitedescription}',content);
        content = str_replace('{pboot:keyword}', get('keyword', 'vars'),content); // 当前搜索的关键字

        // 解析个人扩展标签,升级不覆盖
        if (file_exists(APP_PATH . '/home/controller/ExtLabelController.php')) {
            if (class_exists('app\home\controller\ExtLabelController')) {
                extlabel = new ExtLabelController();content = extlabel->run(content);
            }
        }

        content =this->parserSiteLabel(content); // 站点标签content = this->parserCompanyLabel(content); // 公司标签
        content =this->parserMemberLabel(content); // 会员标签content = this->parserNavLabel(content); // 分类列表
        content =this->parserSelectAllLabel(content); // CMS筛选全部标签解析content = this->parserSelectLabel(content); // CMS筛选标签解析
        content =this->parserSpecifySortLabel(content); // 指定分类content = this->parserListLabel(content); // 指定列表
        content =this->parserSpecifyContentLabel(content); // 指定内容content = this->parserContentPicsLabel(content); // 内容多图
        content =this->parserContentCheckboxLabel(content); // 内容多选调取content = this->parserContentTagsLabel(content); // 内容tags调取
        content =this->parserSlideLabel(content); // 幻灯片content = this->parserLinkLabel(content); // 友情链接
        content =this->parserMessageLabel(content); // 留言板content = this->parserFormLabel(content); // 自定义表单
        content =this->parserSubmitFormLabel(content); // 自定义表单提交content = this->parserQrcodeLabel(content); // 二维码生成
        content =this->parserPageLabel(content); // CMS分页标签解析(需置后)content = this->parserIfLabel(content); // IF语句(需置最后)
        content =this->parserLoopLabel(content); // LOOP语句(需置后,不可放到if前面,否则有安全风险)content = this->restorePreLabel(content); // 还原不需要解析的内容
        content =this->parserReplaceKeyword(content); // 页面关键词替换
        returncontent;
    }

这里的实现有两个问题
1.前面parse的内容会进入下一个parse函数 这就导致前面的标签解析结果会影响后面的标签解析结果
2.parserIfLabel中,如果if标签内容可控 会导致rce
所以思路就有了:找一个模版注入的地方,或者是在模版里找一个解析结果可控的点
而这个cms 自带一个模版注入的控制器..

//tagcontroller   
public function index()
    {

        // 在非兼容模式接受地址第二参数值
        if (defined('RVAR')) {
                _GET['tag'] = RVAR;
        }

        if (! get('tag')) {
            _404('您访问的页面不存在,请核对后重试!');
        }tagstpl = request('tagstpl');
        if (! preg_match('/^[\w]+\.html/',tagstpl)) {
            tagstpl = 'tags.html';
        }content = parent::parser(this->htmldir .tagstpl); // 框架标签解析
        content =this->parser->parserBefore(content); // CMS公共标签前置解析content = this->parser->parserPositionLabel(content, 0, '相关内容', Url::home('tag/' . get('tag'))); // CMS当前位置标签解析
        content =this->parser->parserSpecialPageSortLabel(content, - 2, '相关内容', Url::home('tag/' . get('tag'))); // 解析分类标签content = this->parser->parserAfter(content); // CMS公共标签后置解析
        this->cache(content, true);
    }

可以明显看到Url::home(‘tag/’ . get(‘tag’)进入了content
而get函数在获取参数时有这样一段

    if (is_string(data)) {data = trim(data); // 去空格data = preg_replace_r('/(x3c)|(x3e)/', '', data); // 去十六进制括号data = preg_replace_r('/pboot:if/i', 'pboot@if', data); // 过滤插入cms条件语句data = preg_replace_r('/pboot:sql/i', 'pboot@sql', data); // 过滤插入cms条件语句data = preg_replace_r('/GET\[/i', 'GET@[', data);data = preg_replace_r('/POST\[/i', 'POST@[', $data);
    }

会把标签替换掉 这就涉及前面说到的第一个缺陷了
可以在parseiflabe被调用之前 找一个标签来拼出if标签
这里我找的是parserMemberLabel中解析的user标签

 case 'password': // 密码不允许显示
                        content = str_replace(matches[0][i], '',content);
                        break;

很好 替换为空 直接waf骨灰都给扬了
而后续 parserIfLabel中还有几个waf要过
一个一个讲

                if (preg_match_all('/([\w]+)([\x00-\x1F\x7F\/\*\<\>\%\w\s\\\\]+)?\(/i', matches[1][i], matches2)) {
                    foreach (matches2[1] as value) {
                        if (function_exists(value) && ! in_array(value,white_fun)) {
                            $danger = true;
                            break;
                        }
                    }
                }

这个是第一个waf 是个白名单 匹配的是函数调用 这个很好过 动态调用就行了
形如((system))()
而第二个正则长这样

  if (preg_match('/(\([\w\s\.]+\))|(\_GET\[)|(\$_POST\[)|(\$_REQUEST\[)|(\$_COOKIE\[)|(\$_SESSION\[)|(file_put_contents)|(file_get_contents)|(fwrite)|(phpinfo)|(base64)|(`)|(shell_exec)|(eval)|(assert)|(system)|(exec)|(passthru)|(pcntl_exec)|(popen)|(proc_open)|(print_r)|(print)|(urldecode)|(chr)|(include)|(request)|(__FILE__)|(__DIR__)|(copy)|(call_user_)|(preg_replace)|(array_map)|(array_reverse)|(array_filter)|(getallheaders)|(get_headers)|(decode_string)|(htmlspecialchars)|(session_id)|(require)/i',matches[1][i])) {a = matches[1][i];
                    $danger = true;
                }

这是一个黑名单过滤
外加/(([\w\s.]+)) 拦截了前面的动态调用
但是这个正则有一个致命缺陷,不匹配符号
所以加上注释符就过了…
还有就是这个黑名单的问题 其实也很好绕了 我这里选择的是利用cms自带的函数来动态调用

function get_lg()
{
    lg = cookie('lg');
    if (!lg || ! preg_match('/^[\w\-]+/',lg)) {
        lg = get_default_lg();
        cookie('lg',lg);
    }
    return $lg;
}

这个函数会从cookie中拿内容

function get_backurl()
{
    if (! ! backurl = get('backurl')) {
        if (isset(_SERVER["QUERY_STRING"]) && ! ! get('p')) {
            return "&backurl=" . backurl;
        } else {
            return "?backurl=" .backurl;
        }
    } else {
        return;
    }
}

这个函数会从get参数backurl中拿内容
齐活 最后的payload长这样

注意cookie lg=system

Categories: 技术

0 Comments

发表评论

Avatar placeholder

您的电子邮箱地址不会被公开。 必填项已用*标注