zoukankan      html  css  js  c++  java
  • ThinkPHP5代码执行的简单分析

    漏洞影响版本:

    • ThinkPHP 5.0.5-5.0.22
    • ThinkPHP 5.1.0-5.1.30

    漏洞复现:

      一.mac的debug环境搭建。

        一键化环境搭建工具: mamp pro ,调试工具 PHPstorm

        打开mamp pro,设置左上角的file->Edit Template, 设置httpd.conf (监听本地)

          ServerName 127.0.0.1:8087

          Listen 127.0.0.1:8087

        打开mamp pro,设置左上角的file->Edit Template,设置PHP.ini 选择你的PHP版本 

    zend_extension="/Applications/MAMP/bin/php/php7.2.10/lib/php/extensions/no-debug-non-zts-20170718/xdebug.so"
    xdebug.idekey=PHPSTORM
    xdebug.remote_connect_back = 1
    xdebug.remote_enable=on
    xdebug.remote_port = 9001
    xdebug.remote_handler = dbgp
    xdebug.auto_trace = 1
    xdebug.remote_log = /tmp/xdebug.log

        其余的在PHPstorm上设置,设置完不行尝试加上XDEBUG_SESSION_START=xxxx ,xxxx为你debug开启的等待的key

      最后,环境搭的头疼。

    --------

    poc: http://127.0.0.1:8087/tp5/public/index.php?s=index/ hink emplatedriverfile/read&cacheFile=/etc/passwd

    先贴上调用栈

    File.php:51, think	emplatedriverFile->read()
    Container.php:395, ReflectionMethod->invokeArgs()    //反射调用
    Container.php:395, thinkApp->invokeReflectMethod()  
    Module.php:135, think
    outedispatchModule->think
    outedispatch{closure}()
    Middleware.php:186, call_user_func_array:{/thinkphp/library/think/Middleware.php:186}()
    Middleware.php:186, thinkMiddleware->think{closure}()
    Middleware.php:130, call_user_func:{/thinkphp/library/think/Middleware.php:130}()
    Middleware.php:130, thinkMiddleware->dispatch()
    Module.php:140, think
    outedispatchModule->exec()
    Dispatch.php:168, think
    outedispatchModule->run()
    App.php:432, thinkApp->think{closure}()
    Middleware.php:186, call_user_func_array:{/thinkphp/library/think/Middleware.php:186}()
    Middleware.php:186, thinkMiddleware->think{closure}()
    Middleware.php:130, call_user_func:{/thinkphp/library/think/Middleware.php:130}()
    Middleware.php:130, thinkMiddleware->dispatch()
    App.php:435, thinkApp->run()
    index.php:21, {main}()
    

      

    最开始进入/tp5/public/index.php

    <?php
    // +----------------------------------------------------------------------
    // | ThinkPHP [ WE CAN DO IT JUST THINK ]
    // +----------------------------------------------------------------------
    // | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
    // +----------------------------------------------------------------------
    // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
    // +----------------------------------------------------------------------
    // | Author: liu21st <liu21st@gmail.com>
    // +----------------------------------------------------------------------
    
    // [ 应用入口文件 ]
    namespace think;
    
    // 加载基础文件
    require __DIR__ . '/../thinkphp/base.php';
    
    // 支持事先使用静态方法设置Request对象和Config对象
    
    // 执行应用并响应
    Container::get('app')->run()->send();
    

      

     加载基础文件,调用app应用,调用run()方法,

    App.php:375, thinkApp->run(),在run方法中会对路由进行检测
    App.php:402, $dispatch = $this->routeCheck()->init();
    routeCheck()中会去执行pathinfo()方法,取$_GET['s']里面的值
    public function pathinfo()
        {
            if (is_null($this->pathinfo)) {
                if (isset($_GET[$this->config['var_pathinfo']])) {
                    // 判断URL里面是否有兼容模式参数
                    $pathinfo = $_GET[$this->config['var_pathinfo']];
                    unset($_GET[$this->config['var_pathinfo']]);
                } elseif ($this->isCli()) {
                    // CLI模式下 index.php module/controller/action/params/...
                    $pathinfo = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : '';
                } elseif ('cli-server' == PHP_SAPI) {
                    $pathinfo = strpos($this->server('REQUEST_URI'), '?') ? strstr($this->server('REQUEST_URI'), '?', true) : $this->server('REQUEST_URI');
                } elseif ($this->server('PATH_INFO')) {
                    $pathinfo = $this->server('PATH_INFO');
                }
    
                // 分析PATHINFO信息
                if (!isset($pathinfo)) {
                    foreach ($this->config['pathinfo_fetch'] as $type) {
                        if ($this->server($type)) {
                            $pathinfo = (0 === strpos($this->server($type), $this->server('SCRIPT_NAME'))) ?
                            substr($this->server($type), strlen($this->server('SCRIPT_NAME'))) : $this->server($type);
                            break;
                        }
                    }
                }
    
                $this->pathinfo = empty($pathinfo) || '/' == $pathinfo ? '' : ltrim($pathinfo, '/');
            }
    
            return $this->pathinfo;
        }
    

      从$_GET['s']中取参

    App.php:583, thinkApp->routeCheck()        $dispatch = $this->route->check($path, $must);  // 返回一个Url对象, index|	hink	emplatedriverfile|read

    return new UrlDispatch($this->request, $this->group, $url, [ 'auto_search' => $this->autoSearchController, ]);

    在URL类中没找到含4个参数的构造函数,调用父类Dispatch的构造函数。

    
    

    接着再初始化Url对象的init();方法。

     第一步解析默认的URL规则。调用parseUrl($this->dispatch) ,返回URL规则

        public function init()
        {
            // 解析默认的URL规则
            $result = $this->parseUrl($this->dispatch);
    
            return (new Module($this->request, $this->rule, $result))->init();
        }
    

      

    protected function parseUrl($url)
    {
        $depr = $this->rule->getConfig('pathinfo_depr');
        $bind = $this->rule->getRouter()->getBind();
    
        if (!empty($bind) && preg_match('/^[a-z]/is', $bind)) {
            $bind = str_replace('/', $depr, $bind);
            // 如果有模块/控制器绑定
            $url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr);
        }
    
        list($path, $var) = $this->rule->parseUrlPath($url);
        if (empty($path)) {
    

      使用"/"进行分割,拿到 [模块/控制器/操作]

    public function parseUrlPath($url)
        {
        ....
        ....
            } elseif (strpos($url, '/')) {
                // [模块/控制器/操作]
                $path = explode('/', $url);
            } elseif (false !== strpos($url, '=')) {
                // 参数1=值1&参数2=值2...
                $path = [];
                parse_str($url, $var);
            } else {
                $path = [$url];
            }
    
            return [$path, $var];
        }
    

      

     从$result = $this->parseUrl($this->dispatch); 拿到封装好的路由规则。

    接着往回看return (new Module($this->request, $this->rule, $result))->init();

        public function init()
        {
            // 解析默认的URL规则
            $result = $this->parseUrl($this->dispatch);
    
            return (new Module($this->request, $this->rule, $result))->init();
        }

    在Module类中没有构造函数,在调用Dispatch父类的构造函数,在调用Module类中init()函数。将转换控制器和操作名赋值给$this,也转换控制器和操作名封装到request里面,返回当前类

    public function init()
        {
            parent::init();
            $result = $this->dispatch;
    
            if ($this->rule->getConfig('app_multi_module')) {
                // 多模块部署
                $module    = strip_tags(strtolower($result[0] ?: $this->rule->getConfig('default_module')));
                ...
                ...
                } elseif (!in_array($module, $this->rule->getConfig('deny_module_list')) && is_dir($this->app->getAppPath() . $module)) {
                    $available = true;
                } 
               ...
               ...
                // 模块初始化
                if ($module && $available) {
                    // 初始化模块
                    $this->request->setModule($module);
                    $this->app->init($module);
                } else {
                    throw new HttpException(404, 'module not exists:' . $module);
                }
            }
            // 获取控制器名
            $controller       = strip_tags($result[1] ?: $this->rule->getConfig('default_controller'));
            $this->controller = $convert ? strtolower($controller) : $controller;
            // 获取操作名
            $this->actionName = strip_tags($result[2] ?: $this->rule->getConfig('default_action'));
            // 设置当前请求的控制器、操作
            $this->request
                ->setController(Loader::parseName($this->controller, 1))
                ->setAction($this->actionName);
    
            return $this;
        }

      

    引用启明的分析:

    这里存在第一个对$module的判断,需要让$available等于true,这就需要is_dir($this->app->getAppPath() . $module)成立。官方demo给出的模块是index,而实际开发程序不一定存在该模块名,
    所以构造payload时这里是一个注意点。 

    在回到最开始的app模块

    public function run(){ 
    ..... $this->middleware->add(function (Request $request, $next) use ($dispatch, $data) { return is_null($data) ? $dispatch->run() : $data; }); $response = $this->middleware->dispatch($this->request); ..... }

      

    创建一个闭包函数,然后执行$this->middleware->dispatch($this->request);

        public function dispatch(Request $request, $type = 'route')
        {
            return call_user_func($this->resolve($type), $request);
        }
    

      

    使用call_user_func回调函数,将$request作为参数传进resolve, hinkMiddleware::resolve

    protected function resolve($type = 'route')
        {
            return function (Request $request) use ($type) {
    
                $middleware = array_shift($this->queue[$type]);
    
                if (null === $middleware) {
                    throw new InvalidArgumentException('The queue was exhausted, with no response returned');
                }
    
                list($call, $param) = $middleware;
    
                try {
                    //TODO此处的参数要在看一下
                    $response = call_user_func_array($call, [$request, $this->resolve($type), $param]);
                } catch (HttpResponseException $exception) {
                    $response = $exception->getResponse();
                }
    
                if (!$response instanceof Response) {
                    throw new LogicException('The middleware must return Response instance');
                }
    
                return $response;
            };
        }
    

      

    进入到call_user_func_array() ,继续回调,将[$request, $this->resolve($type), $param]作为参数传进去。

    这里的$call参数是个闭包函数,会调用之前app模块的闭包函数。在app.php:431

    #app.php:431
    function (Request $request, $next) use ($dispatch, $data) { return is_null($data) ? $dispatch->run() : $data; }

    Dispatch.php:168, think outedispatchModule->run()
    App.php:432, thinkApp->think{closure}()

    public function run()
        {
            $option = $this->rule->getOption();
    
            // 检测路由after行为
            if (!empty($option['after'])) {
                $dispatch = $this->checkAfter($option['after']);
    
                if ($dispatch instanceof Response) {
                    return $dispatch;
                }
            }
    
            // 数据自动验证
            if (isset($option['validate'])) {
                $this->autoValidate($option['validate']);
            }
    
            $data = $this->exec();
    
            return $this->autoResponse($data);
        }
    

      

    这时候会执行$data = $this->exec();

    public function exec()
        {
            // 监听module_init
            $this->app['hook']->listen('module_init');
    
            try {
                // 实例化控制器
                $instance = $this->app->controller($this->controller,
                    $this->rule->getConfig('url_controller_layer'),
                    $this->rule->getConfig('controller_suffix'),
                    $this->rule->getConfig('empty_controller'));
    
                if ($instance instanceof Controller) {
                    $instance->registerMiddleware();
                }
            } catch (ClassNotFoundException $e) {
                throw new HttpException(404, 'controller not exists:' . $e->getClass());
            }
    .....
    return $this->app['middleware']->dispatch($this->request, 'controller');

    这里有看到了熟悉的$this->app['middleware']->dispatch($this->request, 'controller'); 

    只不过这里不再是route,而是controller,这里的controller将会再次调用exec()函数里面的闭包函数controller

    $this->app['middleware']->controller(function (Request $request, $next) use ($instance) {
    // 获取当前操作名
    $action = $this->actionName . $this->rule->getConfig('action_suffix');

    if (is_callable([$instance, $action])) {
    // 执行操作方法
    $call = [$instance, $action];

    // 严格获取当前操作方法名
    $reflect = new ReflectionMethod($instance, $action);
    $methodName = $reflect->getName();
    $suffix = $this->rule->getConfig('action_suffix');
    $actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName;
    $this->request->setAction($actionName);

    // 自动获取请求变量
    $vars = $this->rule->getConfig('url_param_type')
    ? $this->request->route()
    : $this->request->param();
    $vars = array_merge($vars, $this->param);
    } elseif (is_callable([$instance, '_empty'])) {
    // 空操作
    $call = [$instance, '_empty'];
    $vars = [$this->actionName];
    $reflect = new ReflectionMethod($instance, '_empty');
    } else {
    // 操作不存在
    throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()');
    }

    $this->app['hook']->listen('action_begin', $call);

    $data = $this->app->invokeReflectMethod($instance, $reflect, $vars);

    return $this->autoResponse($data);
    });
      

    通过闭包函数controller()进行反射,跟进invokeReflectMethod

    $data = $this->app->invokeReflectMethod($instance, $reflect, $vars);

    public function invokeReflectMethod($instance, $reflect, $vars = [])
    {
    $args = $this->bindParams($reflect, $vars);

    return $reflect->invokeArgs($instance, $args);
    }

    最后就调用传入的方法和参数,进行反射。

    至此,简单分析完了,学习到了使用简单闭包的方法。

    官方修复方式:连接

    添加如下正则,对控制器进行判断。只允许a-zA-Z.这样的字符通过

    if (!preg_match('/^[A-Za-z](w)*$/', $controller)) {
                throw new HttpException(404, 'controller not exists:' . $controller);
            }
    

    参考来源:

    https://paper.seebug.org/760/

    https://laravel-china.org/articles/5388/closures-and-anonymous-functions-of-php-new-features

    https://github.com/top-think/framework/commit/adde39c236cfeda454fe725d999d89abf67b8caf

  • 相关阅读:
    两个不同于LR和jmeter的性能测试工具
    一个基于集成jenkins的测试平台
    sparkR原理
    Python 出现需要使用fPIC重新编译的问题
    eclipse中使用jython
    R中逻辑运算
    R语言的一些笔记
    Distributed R
    R语言和大数据
    Mysql command line
  • 原文地址:https://www.cnblogs.com/yangxiaodi/p/10115716.html
Copyright © 2011-2022 走看看