环境
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
中,接下来就会调用Container
的get
方法实例化app
类,接着调用app
类中的run
方法执行应用程序,存在这几个过程
初始化应用
设定运行信息,读取初始化配置等
$this->initialize();
模块、控制器或入口绑定
默认情况下,这段函数是不执行的
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
中就有两个方法,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()方法,并写入日志保存,至此,流程结束
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框架进行依次完整分析,这样就会有更清晰的认识
欢迎师傅们斧正