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目录 ...... }
可以看到该文件有几个部分组成
除了第一步之外,都是为自动加载时查找文件路径做准备,重点说下第一步
第一步使用了 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
中,接下来就会调用Container
的get
方法实例化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()
方法,将结果输出到客户端
在具体分析流程前传参方式,首先介绍一下模块等参数
模块 : application\index
,这个index
就是一个模块,负责前台相关
控制器 : 在模块中的文件夹controller
,即为控制器,负责业务逻辑
操作 : 在控制器中定义的方法,比如在默认文件夹中application\index\controller\Index.php
中就有两个方法,index
和hello
参数 : 就是定义的操作需要传的参数
在本文中会用到两种传参方式,其他的方式可以自行了解
PATH_INFO模式 : http://127.0.0.1/public/index.php/模块/控制器/操作/(参数名)/(参数值)...
兼容模式 : 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行获取应用调度信息,利用了Request
的path
方法,继续跟进
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,接着,在下面,调用到了ReflectionClass
的newInstanceArgs
,该方法将指定的参数创建一个新的类实例,在代码中将这个实例化后的对象直接返回,返回到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传入ReflectionMethod
的invokeArgs
方法,执行$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()方法,并写入日志保存,至此,流程结束
在 \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语句中有三个判断
empty_module
为空,同样跳过这里来到漏洞点所在的位置,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框架进行依次完整分析,这样就会有更清晰的认识
欢迎师傅们斧正