标签 admin 下的文章

由正则引起的Wecenter拒绝服务漏洞


一年前在前公司搭建了一个wecenter程序的社区,忽然有一天发现社区打开首页都会超时,后面排查发现是php超时了,当时知道是文章引起的,但是手上还有其他项目在写,就没去跟,把文章删了就没去管他了。直到前两个星期再次发现了这种问题,刚好手上也没什么事情,就抽空去跟了下代码。

漏洞形成原因:

文章内容65k个字符,字符串太大去匹配贪婪模式,导致php timeout,看看文章的字段类型.
database.jpg

跟下代码:

入口文件 index.php 大概23行:

AWS_APP::run();

继续跟进去/system/aws_app.inc.php只看关键代码 大概104行:

$handle_controller->$action_method();

/app/article/main.php关键代码 大概36行:

public function index_action()
    {
        //...省略部分代码
        // $article_info['message'] 是文章内容
        $article_info['message'] = FORMAT::parse_attachs(nl2br(FORMAT::parse_bbcode($article_info['message'])));

        //...省略部分代码
        TPL::output('article/index');
    }

跟进去看看 FORMAT::parse_bbcode 对文章内容做了什么操作:
/system/class/cls_format.inc.php大概78行:

public static function parse_bbcode($text)
{
    if (!$text)
    {
        return false;
    }

    return self::parse_links(load_class('Services_BBCode')->parse($text));
}

感觉wecenter的版本挺乱的。
这里可以用echo rand();exit;来调试了
继续跟进去self::parse_links

public static function parse_links($str)
    {

        $str = @preg_replace_callback('/(?<!!!\[\]\(|"|\'|\)|>)(https?:\/\/[-a-zA-Z0-9@:;%_\+.~#?\&\/\/=!]+)(?!"|\'|\)|>)/i', 'parse_link_callback', $str);

        if (strpos($str, 'http') === FALSE)
        {
            $str = @preg_replace_callback('/(www\.[-a-zA-Z0-9@:;%_\+\.~#?&\/\/=]+)/i', 'parse_link_callback', $str);
        }
        // 经过调试发现 问题在这一行。传入的$str的字节大概6w左右,这里用到了贪婪模式 - - 这个地方的已经修复了:https://github.com/wecenter/wecenter/commit/177a9e8bab6aec8725f258df02f8f214e5b2469c
        $str = @preg_replace('/([a-z0-9\+_\-]+[\.]?[a-z0-9\+_\-]+@[a-z0-9\-]+\.+[a-z]{2,6}+(\.+[a-z]{2,6})?)/is', '<a href="mailto:\1">\1</a>', $str);
        echo rand();exit;
        return $str;
    }

preg_replace里面的正则暂时改成\w,发现还是php还是存在timeout,继续跟代码。
再回到/app/article/main.phpindex_action 函数的最后一行 TPL::output('article/index');
/system/class/cls_template.inc.php下的 output 函数,大概56行:

$display_template_filename = 'default/' . $template_filename;

/*省略部分代码*/

$output = self::$view->getOutput($display_template_filename);

看看self::$view怎么来的:
/system/class/cls_template.inc.php 下的 initialize 函数:

public static function initialize()
    {
        if (!is_object(self::$view))
        {
            self::$template_path = realpath(ROOT_PATH . 'views/');

            self::$view = new Savant3(
                array(
                    'template_path' => array(self::$template_path),
                    //'filters' => array('Savant3_Filter_trimwhitespace', 'filter')
                )
            );

            if (file_exists(AWS_PATH . 'config.inc.php') AND class_exists('AWS_APP', false))
            {
                self::$in_app = true;
            }
        }

        return self::$view;
}

跟进去 self::$view->getOutput 看看$output的值是什么
/system/Savant3.php 大概1004行:

public function getOutput($tpl = null)
    {
        $output = $this->fetch($tpl);
        if ($this->isError($output)) {
            $text = $this->__config['error_text'];
            return $this->escape($text);
        } else {
            return $output;
        }
    }

$this->fetch 能看到他include了模板,并且把内容return了出去

  public function fetch($tpl = null)
    {
       
        // 省略部分代码      
        } else {

            // yes.  execute the template script.  move the script-path
            // out of the local scope, then clean up the local scope to
            // avoid variable name conflicts.
            $this->__config['fetch'] = $result;
            unset($result);
            unset($tpl);

            // are we doing extraction?
            if ($this->__config['extract']) {
                // pull variables into the local scope.
                extract(get_object_vars($this), EXTR_REFS);
            }

            // buffer output so we can return it instead of displaying.
            ob_start();

            // are we using filters?
            if ($this->__config['filters']) {
                // use a second buffer to apply filters. we used to set
                // the ob_start() filter callback, but that would
                // silence errors in the filters. Hendy Irawan provided
                // the next three lines as a "verbose" fix.
                ob_start();
                include $this->__config['fetch'];
                echo $this->applyFilters(ob_get_clean());
            } else {
                // no filters being used.
                include $this->__config['fetch'];
            }

            // reset the fetch script value, get the buffer, and return.
            $this->__config['fetch'] = null;
            return ob_get_clean();
        }
    }

继续看拿到模板内容后他是怎么处理的:
在这里耽误了很久,一开始直接echo rand();exit;调试的,没注意看有多个模板:
调试代码改成:

if($display_template_filename == 'default/article/index.tpl.htm'){
     echo rand();
     exit;
}

/system/class/cls_template.inc.php 下的 output 函数,大概 134行:

//两个贪婪模式的正则,改一下就ok了。
$output = preg_replace('/[a-zA-Z0-9]+_?[a-zA-Z0-9]*\-__/', '', $output);
$output = preg_replace('/(__)?[a-zA-Z0-9]+_?[a-zA-Z0-9]*\-([\'|"])/', '\2', $output);
if($display_template_filename == 'default/article/index.tpl.htm'){
    echo rand();
    exit;
}

刚开始真没想到贪婪模式,正则现在差不多就记得点星问了,后来跟@L3m0n(柠檬) 叔叔在做题的时候提了一下,他说是贪婪模式,复习下正则吧...

为什么贪婪模式会导致php timeout?

参考:
http://www.cnblogs.com/ggkxg/p/5736064.html 正则表达式的三种模式【贪婪、勉强、侵占】的分析
http://blog.csdn.net/lxcnn/article/details/4304651 正则基础之——NFA引擎匹配原理
http://blog.csdn.net/lxcnn/article/details/4756030 正则基础之——贪婪与非贪婪模式
http://zoroeye.iteye.com/blog/2033459 <进阶-1> 正则表达式的匹配原理

贪婪模式图:
1111.jpg
2222.jpg

抽出上面的其中一条正则来说:

[a-zA-Z0-9]+_?[a-zA-Z0-9]*\-__

把正则切割成几部分:
reg.jpg

认真看贪婪模式的那张图片,假如传入的字符串是:

[img]abc

把字符串切割一下:
substr.jpg

正则在线debug:regex101

正则匹配过程如下(我说的也不一定是对,有兴趣的可以自己去看看正则表达式的匹配原理):
第一次匹配:从字符串位置0开始,子表达式"[a-zA-Z0-9]+",匹配"[",匹配失败,继续往前匹配;
第二次匹配:从字符串位置1开始,子表达式"[a-zA-Z0-9]+",匹配"i", 匹配成功,因为是贪婪模式,一直匹配到"g"那个地方才结束;
第三次匹配:从字符串位置4开始,子表达式"_?",匹配"]",同时记录备选状态,匹配失败,此时进行回溯,找到备选状态,"_?"忽略匹配;
第四次匹配:从字符串位置4开始,子表达式"[a-zA-Z0-9]*",匹配"]",同时记录备选状态,匹配失败,此时进行回溯,找到备选状态,"_?"忽略匹配;
第五次匹配:从字符串位置4开始,子表达式"-",匹配"]",匹配失败,向前查找可供回溯的状态,把控制权交给"_?",由前面匹配成功的子表达式让出已匹配的字符"g";

第六次匹配:从字符串位置3开始,子表达式"_?",匹配"g",同时记录备选状态,匹配失败,此时进行回溯,找到备选状态,"_?"忽略匹配;
第七次匹配:从字符串位置3开始,子表达式"[a-zA-Z0-9]*",匹配"g", 匹配成功;
第八次匹配:从字符串位置4开始,子表达式"-",匹配"]",匹配失败,向前查找可供回溯的状态,把控制权交给"_?",由前面匹配成功的子表达式让出已匹配的字符"mg";

第九次匹配:从字符串位置2开始,子表达式"_?",匹配"m",匹配零次或者一次,不存在这个字符,匹配零次;
第十次匹配:从字符串位置2开始,子表达式"[a-zA-Z0-9]*",匹配"m",匹配成功,因为是贪婪模式,一直匹配到"g"那个地方才结束;
第十一次匹配:从字符串位置4开始,子表达式"-",匹配"]",匹配失败,当前位置正则已经尝试了所有可能,现在从新开始匹配,之前是从i开始匹配成功的,下面从m开始匹配。
第十二次匹配:从字符串位置2开始,子表达式"[a-zA-Z0-9]+",匹配"m",匹配成功,因为是贪婪模式,一直匹配到"g"那个地方才结束;
会一直这样循环直到正则尝试过所有的位置都不能找到匹配结果才会匹配失败。

为什么会timeout?

正则是 重复的子表达式且贪婪模式 组成且不能正确匹配,字符串是超大的话,就会尝试匹配很多次很多次很多次,这就导致了php timeout了。
regex101.jpg

拒绝服务效果:
timeout.jpg

修复后:
200.jpg

修复方案:
/system/class/cls_template.inc.php下的 output 函数,大概 134-135 行(正则)修改为如下:

$output = preg_replace('/[a-zA-Z0-9_?]+\-__/', '', $output);
$output = preg_replace('/(__)?[a-zA-Z0-9_?]+\-([\'|"])/', '\2', $output);

如何避免这种问题:
1.子表达式不要重复并且都贪婪模式;
2.写完正则之后debug一下;


[生活工作] 长风破浪会有时,直挂云帆济苍海 201708


还是那句话,我觉得博客不应该只写技术,应该多写写生活

  1. 2017年5月份从老大哥那里离职,为什么会离职,应该是愧疚的走了吧,我觉得我对不起他们,花了几个月研究的被动漏扫,bug太多了,想想自己几个月前拍胸口说的没问题就脸红,以前多少有些浮躁,现在真的是对技术多了几分敬畏,总是以为就特么那点破玩意,做着做着就发现自己的技术水平根本不足以去做出一个稳定的程序;
  1. 离职之后去王松家跟他打了几天的游戏,哈哈哈哈,关键时刻还得靠王松收留,16年年初也是如此,在上海呆了几年,不知道为啥脑子一抽就跑来北京找工作了,那个时候刚好成年,之前也没做过安全相关的工作,好在乌云的大表哥给了我口饭吃,那个时候也在松阔家混吃混喝了几天;
  1. 六月份收到了个offer,btc交易相关的公司,刚开始还有点怂,自己对ico不了解,实际来了之后才发现根本不用去研究ico的安全,全是web的渣渣锅,别说了,我一天能背五个;
  1. 也是六月份中旬吧,我给自己立了个Flag,研究主动扫描器,然并卵,这么久过去了就写了个扫描端口和爆破域名的,每次运行都被速度感动的要哭,再立Flag,年底写完,为了不愧对我“Python半成品开发狗”的称号;
  1. 分公司的业务让我和@Kttzd着迷,礼尚往来,他挖一发我就挖一发,感觉还挺爽的,根本收不住手,有空就去挖一下...还为此去深圳出差了一趟,顺便见了下北斗的那群没我帅的年轻人,只是教授升职了也不说请我们去一次能开发票的保健,失望;
  1. 感觉深圳好吃的东西好多,椰子鸡、潮汕牛肉火锅、瘦肉汤饭、还有公司旁边星巴克的抹茶星冰乐,北京这个鸟地方我除了盖饭已经吃不起别的东西了。
  1. 跟分公司的同事学了几手,比如说用Python语法树来分析用户提交的代码是否有危险的函数等,本来我打算用ast库来做,然后开源的,已经写完了,没有什么技术难度,加上规则,代码都不过300行,以后有空再更新到gayhub吧;
  1. 在那边顺便审计了Numpy和Pandas两个库,N年前的代码,就是不肯优化,很多代码写的根本就不知道他想干嘛,文档也没介绍,有时候我甚至感觉那段代码就是程序员写着玩的,然后下个版本删掉就说是优化了;
  1. 最近关系比较好两个同事辞职去创业了,感觉未来在公司的日子会空虚很多,哈哈哈,先预祝大佬创业吧;
  1. 感觉我也该到找女朋友年龄了;
  1. 未来还是跟着各位大表哥学习Python学习web安全学习编程;

#Mosuan@2017.08.13

长风破浪会有时,直挂云帆济苍海

最后再来一个火影忍者的BUG:
11111.jpg