【ChaMd5安全团队】ThinkPHP是如何run起来的?

ChaMd5安全团队 2019-04-11

前言

最近ThinkPHP爆了很多的RCE,我也都对漏洞进行了分析,跟了相应的源码。这几个洞一个是任意方法调用造成的,另一个本质是请求中filter可控,而ThinkPHP的会对输入即input进行filter过滤造成的。分析漏洞不难,但是之前的我对TP的dispatch路由调度以及TP的整个运行过程并不是很清楚明了,因此就有了这篇文章。


TP版本:5.0.22

从官网下载到TP源码,首先看start.php里这一句:App::run()->send(); 跟进App类的run方法。

首先$request是一个Request::instance(),进去看一下最后就是self::$instance = new static($options);这个操作,也就是返回了Request类的实例。

说下new static这个操作也是第一次见,之前只见过new self(),这里贴一段代码就明白两者区别了。

class A {
  public static function get_self() {
    return new self();
  }

  public static function get_static() {
    return new static();
  }
}

class B extends A {}

echo get_class(B::get_self()); // A
echo get_class(B::get_static()); // B
echo get_class(A::get_static()); // A

回到源码,接下来通过self::initCommon()方法把文件convention.php的配置读到了$config数组中。

接下来会进行模块绑定的处理,首先检查App的属性bind,是否绑定到特定模块/控制器。如果App的属性bind没有设置,则读取配置的auto_bind_module。如果设置了自动绑定模块,则将入口文件名绑定为模块名称。

这里不重要,接着往下,调用了$request->filter($config['default_filter']);来将config中的过滤器绑定到request对象中。5.0.22里这个值默认是空的。

跳过下面几行,到了Hook::listen('app_dispatch', self::$dispatch);这里,调用app_dispatch的回调函数。

TP中的钩子实际就是行为拓展。

在TP的application目录中的tags.php中能看到已经定义的hook,当我们要自定义hook时,需要先add注册,再在需要用的地方listen。但是TP里貌似并没有对这几个内置的钩子的功能进行实现。

再继续看源码:

// 获取应用调度信息
$dispatch = self::$dispatch;

// 未设置调度信息则进行 URL 路由检测
if (empty($dispatch)) {
    $dispatch = self::routeCheck($request, $config);
}

dispatch获取应用的调度信息,这里的routeCheck()是路由解析的入口,跟进看一下。(TP的几个RCE分析过程中也都牵扯到了这个方法)

开始这句$path = $request->path();继续跟进,最终会到pathinfo()方法,它会根据URL中是通过兼容模式s还是CLI模式来访问返回给path进行路由解析过的值。注意TP5中,url模式已经被取消,只能采用兼容普通强制三种模式。

图片来源:SoftBlue的博客

接下来判断该请求是否进行过路由检测,路由解析过后会将结果$result返回给$dispatch。下面是访问/index/index时,$dispatch中的值。

Array
(
    [type] => module
    [module] => Array
        (
            [0] => index
            [1] => index
            [2] => null
        )
)

回到run方法,下一行$request->dispatch($dispatch);将解析的调度信息保存到全局Request对象中。

略过下面几行,直接到$data = self::exec($dispatch, $config);,跟进exec方法看一下。

第一行,根据$dispatch['type']进行不同的应用的调度。

应用调度应该算是TP启动中一个核心过程。其中有重定向、加载module、执行控制器操作、执行回调方法、返回响应等多种调度方式。不同的请求对应不同的调度方式。

这里跟进看下module部分,它根据请求加载对应module及文件。

首先module方法会检测是多模块还是单模块,TP5默认是开启多模块的。下面的一段操作主要会绑定一个module,然后进行module的初始化。跟进init方法,大致读一下,主要作用是加载了模块的配置信息等。

继续回到module,往下看到获取了控制器名以及绑定到request对象中,并通过$instance = Loader::controller来返回一个控制器实例。继续往下到了调用控制器操作的部分。

if (is_callable([$instance, $action])) {
      // 执行操作方法
      $call = [$instance, $action];
      // 严格获取当前操作方法名
      $reflect    = new ReflectionMethod($instance, $action);
      $methodName = $reflect->getName();
      $suffix     = $config['action_suffix'];
      $actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName;
      $request->action($actionName);

  } elseif (is_callable([$instance, '_empty'])) {
      // 空操作
      $call = [$instance, '_empty'];
      $vars = [$actionName];
  } else {
      // 操作不存在
      throw new HttpException(404'method not exists:' . get_class($instance) . '->' . $action . '()');
  }

  Hook::listen('action_begin', $call);

  return self::invokeMethod($call, $vars);

这部分就是通过请求URL来调用相关方法的部分,主要通过php的反射来实现。前半部分主要通过ReflectionMethod获得了action名,最后是通过return self::invokeMethod($call, $vars);调用了相应的方法。跟进invokeMethod看一下。

public static function invokeMethod($method, $vars = []){
    if (is_array($method)) {
        $class   = is_object($method[0]) ? $method[0] : self::invokeClass($method[0]);
        $reflect = new ReflectionMethod($class, $method[1]);
    } else {
        // 静态方法
        $reflect = new ReflectionMethod($method);
    }

    $args = self::bindParams($reflect, $vars);

    self::$debug && Log::record('[ RUN ] ' . $reflect->class . '->' . $reflect->name . '[ ' . $reflect->getFileName() . ' ]''info');

    return $reflect->invokeArgs(isset($class) ? $class : null, $args);
}

首先通过is_object判断method[0]是否是对象,然后通过new ReflectionMethod来获得一个反射对象,接下来通过bindParams获得改反射对象要调用的方法的参数,最后通过invokeArgs方法来调用。接下来return给module,module再返回给exec,exec又返回到run的data属性。这一步步的反射操作太强了。

绕了一大圈,继续再回到run。

可以看到,data的确是index控制器index操作返回的值,即index页面的内容。

run中接下来会检测data属性是否是Responce类的实例,然而这里data是string,很明显不是,所以要通过$response = Response::create($data, $type);处理一下,使其变成Responce对象,然后就结束run方法,return到start.php中。

start通过App::run()->send()来调用返回的Responce对象的send方法,其实核心就是一个echo $data的操作,来将返回输出到页面,这样就完成了一次请求的整个过程。


在最后补充一下TP中的自动加载的实现。在base.php中 hinkLoader::register();后,就可以进行自动加载了。

先进去看下register方法,核心是这几句:

// 自动加载常规操作
spl_autoload_register($autoload ?: 'think\Loader::autoload'truetrue);
// 注册命名空间定义
self::addNamespace([
    'think'    => LIB_PATH . 'think' . DS,
    'behavior' => LIB_PATH . 'behavior' . DS,
    'traits'   => LIB_PATH . 'traits' . DS,
]);
// 加载类库映射文件
if (is_file(RUNTIME_PATH . 'classmap' . EXT)) {
    self::addClassMap(__include_file(RUNTIME_PATH . 'classmap' . EXT));
}

也就是说,在Loader::controller()方法中,当new $class()的时候,会自动触发自动加载,而路由的解析过程中,只是进行了很多过程操作。在注入一个对象a的过程中,可能这个a对象的参数也是一个对象b,这就用到了另外的一个对象b,这时通过反射和自动加载实现的依赖注入就可以成功new出依赖的实例来注入到参数中,从而实现依赖注入。spring中的依赖注入是通过和控制反转是通过Bean来实现的,TP中的实现相对简单一些。

再写一下TP中的反射方法:

/**
 * 执行函数或者闭包方法 支持参数调用
 * @access public
 * @param string|array|Closure $function 调用的函数名
 * @param array                 $vars     函数参数
 * @return mixed
 */

public static function invokeFunction($function, $vars = [])
{
    $reflect = new ReflectionFunction($function);   //创建$function的反射函数类
    $args    = self::bindParams($reflect, $vars);    //绑定反射函数的参数
    // 记录执行信息
    self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info');
    return $reflect->invokeArgs($args);              //使用反射函数类调用函数,并返回结果
}

invokeMethod等同样原理。


参考资料:

https://www.kancloud.cn/zmwtp/tp5/

https://www.kancloud.cn/manual/thinkphp5/118003

~来一波 ChaMd5安全招聘~

御风维安

代码审计工程师

http://www.chamd5.org/jobdetail.aspx?id=746

高级代码审计工程师

http://www.chamd5.org/jobdetail.aspx?id=747


信息安全测评中心

渗透测试工程师

http://www.chamd5.org/jobdetail.aspx?id=745

产品安全测试工程师

http://www.chamd5.org/jobdetail.aspx?id=744


每日优鲜安全部

高级渗透专家

http://www.chamd5.org/jobdetail.aspx?id=735

信息安全架构师

http://www.chamd5.org/jobdetail.aspx?id=736

高级移动安全专家

http://www.chamd5.org/jobdetail.aspx?id=737

应用安全架构师

http://www.chamd5.org/jobdetail.aspx?id=738

数据安全专家/大数据安全专家

http://www.chamd5.org/jobdetail.aspx?id=739

高级研发工程师

http://www.chamd5.org/jobdetail.aspx?id=740

渗透测试工程师(黑产对抗方向)

http://www.chamd5.org/jobdetail.aspx?id=741

渗透测试工程师(Web安全攻防方向)

http://www.chamd5.org/jobdetail.aspx?id=742

安全工程师

http://www.chamd5.org/jobdetail.aspx?id=743


招新小广告

ChaMd5 ctf组 长期招新

尤其是reverse+pwn+合约的大佬

欢迎联系admin@chamd5.org



觉得不错,分享给更多人看到