thinkphp 5.1框架流程分析与RCE
2020-05-28 10:36:14 Author: xz.aliyun.com(查看原文) 阅读量:483 收藏

环境

phpstudy+thinkphp5.1.20

由于thinkphp5.1以上的版本不再支持官网下载源码,需要使用composer下载,比如说下载5.1.20版本

composer create-project --prefer-dist topthink/think tp5120

cd tp5120

vim composer.json

composer.json文件中"topthink/framework": "5.1.*"改为"topthink/framework": "5.1.20"

执行composer update 即可

框架流程

首先,看入口文件 /public/index.php ,只有三行代码

namespace think;

// 加载基础文件
require __DIR__ . '/../thinkphp/base.php';

// 支持事先使用静态方法设置Request对象和Config对象

// 执行应用并响应
Container::get('app')->run()->send();

定义命名空间,加载基础文件,然后执行应用并响应

首先来看基础文件: '/thinkphp/base.php'

其作用在于注册自动加载、注册错误处理、加载默认配置

这其中比较重要的就是自动加载功能,系统调用 Loader::register(); 方法注册自动加载,跟进该方法

public static function register($autoload = '')
    {
        // 注册系统自动加载
        spl_autoload_register($autoload ?: 'think\\Loader::autoload', true, true);

        $rootPath = self::getRootPath();

        self::$composerPath = $rootPath . 'vendor' . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR;

        // Composer自动加载支持
        ......
        // 注册命名空间定义
        ......
        // 加载类库映射文件
        ......
        // 自动加载extend目录
        ......
    }

可以看到该文件有几个部分组成

  • 注册系统自动加载
  • Composer自动加载支持
  • 注册命名空间定义
  • 加载类库映射文件
  • 自动加载extend目录

除了第一步之外,都是为自动加载时查找文件路径做准备,重点说下第一步

第一步使用了 spl_autoload_register 函数,这是一个自动加载函数,若是实例化一个未定义的类时就会触发该函数,然后会触发第一个参数作为指定的方法,可以看到此函数指定了 think\Loader::autoload 作为触发方法,继续跟进

public static function autoload($class)
    {
        if (isset(self::$classAlias[$class])) {
            return class_alias(self::$classAlias[$class], $class);
        }

        if ($file = self::findFile($class)) {

            // Win环境严格区分大小写
            if (strpos(PHP_OS, 'WIN') !== false && pathinfo($file, PATHINFO_FILENAME) != pathinfo(realpath($file), PATHINFO_FILENAME)) {
                return false;
            }

            __include_file($file);
            return true;
        }
    }

该函数首先判断$class是否在类库别名$classAlias中,在的话直接返回,不在的话向下执行findFile()findFile()就是一个利用多种方式查找文件的函数,最后会返回文件的路径,未找到会返回false,之后就利用__include_file对文件做include包含,这就是自动包含

返回到index.php中,接下来就会调用Containerget方法实例化app类,接着调用app类中的run方法执行应用程序,存在这几个过程

初始化应用

设定运行信息,读取初始化配置等

模块、控制器或入口绑定

默认情况下,这段函数是不执行的

if ($this->bindModule) {
    // 模块/控制器绑定
    $this->route->bind($this->bindModule);
} elseif ($this->config('app.auto_bind_module')) {
    // 入口自动绑定
    $name = pathinfo($this->request->baseFile(), PATHINFO_FILENAME);
    if ($name && 'index' != $name && is_dir($this->appPath . $name)) {
        $this->route->bind($name);
    }
}

路由检测

调用到了checkRoute()init()进行路由检测,这里的routeCheck()就是路由解析的入口,并且把解析的调度信息保存到全局Request对象中

$dispatch = $this->dispatch;

if (empty($dispatch)) {
    // 路由检测
    $dispatch = $this->routeCheck()->init();
}

// 记录当前调度信息
$this->request->dispatch($dispatch);

记录路由和请求信息

调试模式下,保存路由的请求信息到日志文件中

if ($this->appDebug) {
    $this->log('[ ROUTE ] ' . var_export($this->request->routeInfo(), true));
    $this->log('[ HEADER ] ' . var_export($this->request->header(), true));
    $this->log('[ PARAM ] ' . var_export($this->request->param(), true));
}

执行路由调度 :

执行请求分派到的业务逻辑

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

$response = $this->middleware->dispatch($this->request);

接下来就将得到的$response返回

回到index.php,会在index.php中调用Response类的send()方法,将结果输出到客户端

thinkphp传参方式

在具体分析流程前传参方式,首先介绍一下模块等参数

  • 模块 : application\index,这个index就是一个模块,负责前台相关

  • 控制器 : 在模块中的文件夹controller,即为控制器,负责业务逻辑

  • 操作 : 在控制器中定义的方法,比如在默认文件夹中application\index\controller\Index.php中就有两个方法,indexhello

  • 参数 : 就是定义的操作需要传的参数

在本文中会用到两种传参方式,其他的方式可以自行了解

  1. PATH_INFO模式 : http://127.0.0.1/public/index.php/模块/控制器/操作/(参数名)/(参数值)...

  2. 兼容模式 : http://127.0.0.1/public/index.php?s=/模块/控制器/操作&(参数名)=(参数值)...

路由解析过程

路由检测

首先在run函数的路由检测处下断点,在 application\index\controller 目录下新建一个test.php

接下来,我们访问 http://127.0.0.1/public/index.php/index/Test/hello/name/world ,返回phpstorm可以发现已经捕获到了

首先,路由检测调用到了 routeCheck() 方法,F7跟进看一下

public function routeCheck()
    {
        // 检测路由缓存
        if (!$this->appDebug && $this->config->get('route_check_cache')) {
            ...
        }

        // 获取应用调度信息
        $path = $this->request->path();

        // 是否强制路由模式
        ...

        // 路由检测 返回一个Dispatch对象
        $dispatch = $this->route->check($path, $must);
        ...
        return $dispatch;
    }

该函数首先根据 route_check_cache 检测是否开启了路由缓存,默认情况下该配置为false, 'route_check_cache' => false ,接下来到589行获取应用调度信息,利用了Requestpath方法,继续跟进

public function path()
    {
        if (is_null($this->path)) {
            $suffix   = $this->config['url_html_suffix'];
            $pathinfo = $this->pathinfo();

            if (false === $suffix) {
                // 禁止伪静态访问
                $this->path = $pathinfo;
            } elseif ($suffix) {
                // 去除正常的URL后缀
                $this->path = preg_replace('/\.(' . ltrim($suffix, '.') . ')$/i', '', $pathinfo);
            } else {
                // 允许任何后缀访问
                $this->path = preg_replace('/\.' . $this->ext() . '$/i', '', $pathinfo);
            }
        }

        return $this->path;
    }

继续跳转到 pathinfo() 方法

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;
    }

该方法依据请求方式跳转到不同的if判断中,由于我们利用的时pathinfo模式,所以跳转到最后一个elseif中来判断,由 $this->server 获取参数,接下来对$pathinfo进行分析,之后会去掉$pathinfo中最左侧的 / 返回,此时 $pathinfo=index/Test/hello/name/world

下面返回到path方法,将其去除正常url后缀后赋值给返回值$path

跳转回routeCheck()方法,接下来程序会执行到路由检测部分,调用route的check()方法,把$path作为$url参数传入,继续跟进

public function check($url, $must = false)
    {
        // 自动检测域名路由
        $domain = $this->checkDomain();
        $url    = str_replace($this->config['pathinfo_depr'], '|', $url);

        $completeMatch = $this->config['route_complete_match'];

        $result = $domain->check($this->request, $url, $completeMatch);

        if (false === $result && !empty($this->cross)) {
            // 检测跨域路由
            $result = $this->cross->check($this->request, $url, $completeMatch);
        }

        if (false !== $result) {
            // 路由匹配
            return $result;
        } elseif ($must) {
            // 强制路由不匹配则抛出异常
            throw new RouteNotFoundException();
        }

        // 默认路由解析
        return new UrlDispatch($this->request, $this->group, $url, [
            'auto_search' => $this->autoSearchController,
        ]);
    }

首先把$url中的 / 替换为 | ,由于这里用的是默认配置,所以会直接跳转到return行,这里的返回值实例化了一个UrlDispatch类,并传入了几个参数,这里定位到UrlDispatch定义处,可以发现这是一个路由别名, use think\route\dispatch\Url as UrlDispatch;

路由解析

接下来就是路由解析的过程

调用到autoload函数来自动加载该类,并且调用到了其父类 Dispatch 的构造方法(__construct),将参数值赋值给$this中,接下来就会跳转回routeCheck()方法,返回$dispatch,由于Url类中对父类的init()方法做了重写接下来会调用Url类中的init()方法,跟进看一下

public function init()
{
    // 解析默认的URL规则
    $result = $this->parseUrl($this->dispatch);

    return (new Module($this->request, $this->rule, $result))->init();
}

函数调用了parseUrl()对URL进行解析,继续跟进

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)) {
            return [null, null, null];
        }

        // 解析模块
        $module = $this->rule->getConfig('app_multi_module') ? array_shift($path) : null;

        if ($this->param['auto_search']) {
            $controller = $this->autoFindController($module, $path);
        } else {
            // 解析控制器
            $controller = !empty($path) ? array_shift($path) : null;
        }

        // 解析操作
        $action = !empty($path) ? array_shift($path) : null;

        // 解析额外参数
        if ($path) {
            if ($this->rule->getConfig('url_param_type')) {
                $var += $path;
            } else {
                preg_replace_callback('/(\w+)\|([^\|]+)/', function ($match) use (&$var) {
                    $var[$match[1]] = strip_tags($match[2]);
                }, implode('|', $path));
            }
        }

        $panDomain = $this->request->panDomain();

        if ($panDomain && $key = array_search('*', $var)) {
            // 泛域名赋值
            $var[$key] = $panDomain;
        }

        // 设置当前请求的参数
        $this->request->setRouteVars($var);

        // 封装路由
        $route = [$module, $controller, $action];

        if ($this->hasDefinedRoute($route, $bind)) {
            throw new HttpException(404, 'invalid request:' . str_replace('|', $depr, $url));
        }

        return $route;
    }

直接跳转到48行,可以看到框架调用到了rule的parseUrlPath方法对$url进行分割操作,将参数整理为一个数组

并将返回的$path和$var赋值给parseUrl中的$path和$var

下面利用array_shift$path进行解析即依次移出数组中的第一个元素并赋值给模块、控制器、操作和额外参数,并在封装路由处将$module$controller$action封装进$route中,返回

回到Url的init()函数中,此时流程到了return处,这里实例化了一个Dispatch的子类Module类,并调用了其init方法,F7跟进可以看到调用到了autoload函数和Request的构造方法,同样是赋值操作

跟进到init方法

public function init()
    {
        parent::init();

        $result = $this->dispatch;

        if (is_string($result)) {
            $result = explode('/', $result);
        }

        if ($this->rule->getConfig('app_multi_module')) {
            // 多模块部署
            $module    = strip_tags(strtolower($result[0] ?: $this->rule->getConfig('default_module')));
            $bind      = $this->rule->getRouter()->getBind();
            $available = false;

            if ($bind && preg_match('/^[a-z]/is', $bind)) {
                // 绑定模块
                list($bindModule) = explode('/', $bind);
                if (empty($result[0])) {
                    $module = $bindModule;
                }
                $available = true;
            } elseif (!in_array($module, $this->rule->getConfig('deny_module_list')) && is_dir($this->app->getAppPath() . $module)) {
                $available = true;
            } elseif ($this->rule->getConfig('empty_module')) {
                $module    = $this->rule->getConfig('empty_module');
                $available = true;
            }

            // 模块初始化
            if ($module && $available) {
                // 初始化模块
                $this->request->setModule($module);
                $this->app->init($module);
            } else {
                throw new HttpException(404, 'module not exists:' . $module);
            }
        }

        // 是否自动转换控制器和操作名
        $convert = is_bool($this->convert) ? $this->convert : $this->rule->getConfig('url_convert');
        // 获取控制器名
        $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;
    }

这里调用到了父类Dispatch的init方法,接下来在38行处对$result[0]也就是访问的模块做strip_tags处理,然后跳到了49行做了两个判断:第一个是判断$module是否在deny_module_list(禁止访问模块)中,第二个是判断这个模块是否存在,若是都满足则会令$available=true,这样在57行开始的判断中才会做初始化模块的操作而不是throw一个404错误出来

接下来就是对控制器和操作strip_tags的操作并且赋值给$this,设置当前请求的控制器、操作,将这些信息保存到当前的$this中

路由调度

跳转回run函数中来,记录信息这些操作略过,直接来到431行,这里调用了add函数,并将一个匿名函数作为参数传入

跟进后发现,函数中将$middleware也就是匿名函数赋值给了 $queue[route][]

接下来返回run方法,按流程走会调用到middleware类的dispatch方法,继续跟进

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

这里利用了call_user_func这个函数,把$request作为参数传入回调的solve方法,跟进看一下

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 {
                $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;
        };
    }

该函数直接把一个匿名函数作为返回值,通过use语句让该闭包函数继承$type变量,通过array_shift()函数把App类中的之前那个匿名函数赋值给$middleware,再继续将$middleware的值通过list赋给$call

接着运行到下一步时,继续通过call_user-func_array()再把App类中的匿名函数回调过来

此时匿名函数中的判断(is_null($data))成立,执行dispatch类的run函数,继续跟进

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);
    }

该函数是执行路由调度函数,直接跳到执行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'));
        } catch (ClassNotFoundException $e) {
            throw new HttpException(404, 'controller not exists:' . $e->getClass());
        }

        $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();
            } 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);
        });

        return $this->app['middleware']->dispatch($this->request, 'controller');
    }

函数在try部分调用了app类的controller函数来实例化控制器,继续跟进

public function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '')
    {
        list($module, $class) = $this->parseModuleAndClass($name, $layer, $appendSuffix);

        if (class_exists($class)) {
            return $this->__get($class);
        } elseif ($empty && class_exists($emptyClass = $this->parseClass($module, $layer, $empty, $appendSuffix))) {
            return $this->__get($emptyClass);
        }

        throw new ClassNotFoundException('class not exists:' . $class, $class);
    }

调试时获取了几个配置作为函数参数后进入controller函数,首先利用parseModuleAndClass来解析模块和类名

protected function parseModuleAndClass($name, $layer, $appendSuffix)
    {
        if (false !== strpos($name, '\\')) {
            $class  = $name;
            $module = $this->request->module();
        } else {
            if (strpos($name, '/')) {
                list($module, $name) = explode('/', $name, 2);
            } else {
                $module = $this->request->module();
            }

            $class = $this->parseClass($module, $layer, $name, $appendSuffix);
        }

        return [$module, $class];
    }

不难发现,如果在$name也就是控制器中查找到了 \ ,那么,控制器的值赋给$class,模块名赋值给$module,然后直接return

根据现在访问的url,会跳转到else的else语句中进行赋值,接下来会调用到parseClass()函数,经过了一堆处理之后返回了

$this->namespace.'\\'.($module ? $module.'\\' : '').$layer.'\\'.$path.$class;

实际上就是命名空间的路径即$class,根据命名空间的特性,知道了类命名空间之后就可以对类进行实例化,接下来继续跟进代码,回到parseModuleAndClass方法,返回了$moduel和$class

回到controller中,判断$class存在的话就会调用到__get()方法,并将$class(命名空间)传入,跟进发现 __get() 调用到了make()方法,继续跟进

public function make($abstract, $vars = [], $newInstance = false)
    {
        if (true === $vars) {
            // 总是创建新的实例化对象
            $newInstance = true;
            $vars        = [];
        }

        $abstract = isset($this->name[$abstract]) ? $this->name[$abstract] : $abstract;

        if (isset($this->instances[$abstract]) && !$newInstance) {
            return $this->instances[$abstract];
        }

        if (isset($this->bind[$abstract])) {
            $concrete = $this->bind[$abstract];

            if ($concrete instanceof Closure) {
                $object = $this->invokeFunction($concrete, $vars);
            } else {
                $this->name[$abstract] = $concrete;
                return $this->make($concrete, $vars, $newInstance);
            }
        } else {
            $object = $this->invokeClass($abstract, $vars);
        }

        if (!$newInstance) {
            $this->instances[$abstract] = $object;
        }

        return $object;
    }

在这个函数中直接看调用到的invokeClass()函数,可以发现命名空间被传入作为参数,继续

public function invokeClass($class, $vars = [])
    {
        try {
            $reflect = new ReflectionClass($class);

            if ($reflect->hasMethod('__make')) {
                $method = new ReflectionMethod($class, '__make');

                if ($method->isPublic() && $method->isStatic()) {
                    $args = $this->bindParams($method, $vars);
                    return $method->invokeArgs(null, $args);
                }
            }

            $constructor = $reflect->getConstructor();

            $args = $constructor ? $this->bindParams($constructor, $vars) : [];

            return $reflect->newInstanceArgs($args);

        } catch (ReflectionException $e) {
            throw new ClassNotFoundException('class not exists: ' . $class, $class);
        }
    }

可以看到这里首先利用到了ReflectionClass类反射了$class,接着,在下面,调用到了ReflectionClassnewInstanceArgs,该方法将指定的参数创建一个新的类实例,在代码中将这个实例化后的对象直接返回,返回到make函数中,将实例化后的对象赋值给$object,最后将其return

跳转回到exec函数中,将invokeClass函数的返回值$object赋值给$instance,接下来又重新调用了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();
            } 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);
        }

首先利用了is_callable方法对$instance和$action进行验证,接下来创建了反射类$reflect,接下来跳出if判断,执行了invokeReflectMethod()函数

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

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

这里显示利用了bindParams函数对$reflect和$vars进行处理,返回了$args:{"world"},然后将$args和$instance传入ReflectionMethodinvokeArgs方法,执行$instance即对象实例,这里可以看到直接跳转到了我们自己写的test文件中

响应输出

回到exec函数,这里可以看到会继续执行autoResponse方法,传入的$data就是我们自己写的test.php的返回结果,该函数调用了create函数,返回设定的数据包的头部信息,$response变量中,并且最后利用了new static();实例化自身Response类,接着调用了__construct()方法,可以看到这里将所有的头部信息的变量赋值给了Response类的$this,然后返回到autoResponse()方法中,将这些赋值给$response变量中

接下来跳转回Dispatch的run()方法中,把$response赋值给$data,接着又重新调用了依次autoResponse()方法,这次是用来判断$data是否是Response的实例的,成功则将$data赋值给$reponse返回

这次跳转回App的run方法,将$response返回

下面就是将数据发送到客户端的过程了,执行send函数,依次发送状态码,头部信息和返回的数据信息,接着调用到appShutdown()方法,并写入日志保存,至此,流程结束

RCE分析

\thinkphp\library\think\Container.php 中,317行的invokeFunction方法

这里调用到了call_user_func_array()这个危险函数,那么是否可以调用它来执行些危险操作呢?

首先看一下这个流程,把传入的参数$function作为一个反射类赋值给$reflect,接下来把$reflect和参数$vars传入bindParams()方法,跟进一下该方法

其实就是对参数做处理,获取到反射类的绑定参数

这里可以将当前的模块,控制器方法,操作等直接放到请求的url里,让流程直接访问到这个函数,执行call_user_func_array()函数,那么就可以根据url来构造payload

  • 正常URL : 127.0.0.1/public/index.php/index/test/hello/name/world

  • 恶意URL : 127.0.0.1/public/index/模块/Container/invokefunction

但是这个Container类并不在任何模块里,那模块应该填什么?回到上面的流程中,Module类的init方法

为了保证流程不在最后抛出404错误,就得令$module$available为true,首先,在模块正常存在的情况下,$module是一定为true的,那么需要考虑的就是$available了,在函数中部的if语句中有三个判断

  • 第一个需要$bind为true才执行,但是在默认情况下,$bind为null,所以跳过,
  • 第三个判断取出的配置 : empty_module为空,同样跳过
  • 第二个:在if语句还有两个判断,第一个先判断模块是否在禁止访问模块中,第二个判断该模块的路径是否存在,也就是说这里只需要构造一个环境中存在的模块就ok了,继续向下

这里来到漏洞点所在的位置,App.php631行的parseModuleAndClass方法

这里的参数$name就是我们的控制器名,在流程中,经过这个函数时会跳转到else判断经过parseClass函数对$name和$module进行拼接,但是注意他的if语句,若是$name存在\,就会直接返回,跳过了parseClass()函数的约束,接着后面的操作,$class就会被实例化并调用call_user_func_array()函数

这里的Container类在命名空间think下,所以可以构造think/container

这里还需要说的是,在thinkphp中,只要知道命名空间的路径,并且命名空间的路径与类库文件的路径相一致,就可以对类进行实例化

类库目录

名称 描述 类库目录
think 系统核心类库 think/library/think
traits 系统traits类库 think/library/traits
app 应用类库 Application

这下就可以构造访问的url了

127.0.0.1/public/index.php/index/think\Container/invokefunction

继续构造传入的参数

/functino/call_user_func_array/vars[0]/phpinfo/vars[1][]/1

但是在pathinfo的访问模式下,\会被浏览器自动替换为/,于是替换为兼容模式访问

http://127.0.0.1/public/index.php?s=index/think\Container/invokefunction&function=call_user_func_array&var[0]=phpinfo&vars[1][]=1

小结

本文重点在于分析thinkphp的框架流程,流程中函数调用较为复杂,建议独立的对thinkphp框架进行依次完整分析,这样就会有更清晰的认识
欢迎师傅们斧正

参考:ThinkPHP 5.1框架结合RCE漏洞的深入分析


文章来源: http://xz.aliyun.com/t/7788
如有侵权请联系:admin#unsafe.sh