0%

分析thinkphp

php基础不好,分析下thinkphp的入口代码,后面将分析thinkphp反序列化漏洞和命令执行。

一、环境

软件 环境
phpstudy 8.1.0.1
thinkphp V5.1.35 LTS
nginx 1.15.11
php 7.3.4

二、入口文件

首先分析入口文件:public/index.php

1
2
3
4
5
6
7
<?php
namespace think;
// 加载基础文件
require __DIR__ . '/../thinkphp/base.php';
// 支持事先使用静态方法设置Request对象和Config对象
// 执行应用并响应
Container::get('app')->run()->send();

跟进 base.php 文件。

1
2
3
4
5
6
7
8
9
namespace think;
// 载入Loader类
require __DIR__ . '/library/think/Loader.php';
// 注册自动加载
Loader::register();
// 注册错误和异常处理机制
Error::register();

...

其中 Loader::register() 是用来配置类的自动加载。

2.1、类的自动加载

在php中,如果想使用其他文件中的类,必须先引用这个文件。例如,我在 test3.php 中定义一个类。

1
2
3
4
5
6
7
8
9
10
11
<?php
namespace test;
class Test{
public function __construct()
{
echo "test3.php-construct";
}
public function test_t(){
echo "wa";
}
}

然后在test4.php中调用这个类

1
2
3
4
<?php
namespace test;
$t = new Test();
$t->testt();

这时运行会产生错误。
PHP Fatal error: Uncaught Error: Class ‘test\Test’ not found in /E

但如果我使用了类的自动加载。将不再会发生错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
namespace test;
// 在new一个类的过程中,如果发现没有找到,则调用\\test\\autoload函数。
spl_autoload_register("\\test\\autoload", true, true);
function autoload($className = "")
{
echo "class name : " . $className;
if($className=="test\Test"){
// 包含需要加载的类的文件
include "test3.php";
}
}
$t = new Test();
$t->testt();

然后重新访问,一切正常。当我初始化一个类时,如果没有找到这个类,就会调用类的自动加载。

2.2、thinkphp类自动加载

Loader::register(); 中将调用下面内容:

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

通过 think\\Loader::autoload 函数进行自动加载。

2.3、调试

https//cdnjsdelivrnet/gh/imageformarktext/image/picgo2020/02/06/20200206210351png

php发现没有找到Container这个类,所以会调用 think\\Loader::autoload 函数。在下面图中可以看出,在这个函数中将会 include Container 这个类。

https//cdnjsdelivrnet/gh/imageformarktext/image/picgo2020/02/06/20200206210522png

三、run函数

thinkphp会执行 Container::get('app')->run()->send(); 在Container中确定 appthinkphp/library/think/App.php 文件。跟进该类的 run 函数。

首先是进行一系列的初始化相关操作。

1
$this->initialize();

然后设置一些列的hook点。这里的hook函数可以理解为一种 AOP编程。如果添加了监听的函数,当执行到hook点时,将会自动调用监听的函数。

1
$this->hook->listen('app_init')

接着绑定路由,如下,默认情况下这两个判断都是 false

1
2
3
4
5
6
7
8
9
10
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);
}
}

然后是进行路由选择,即根据我们的情求找到正确的Controller。

1
2
3
4
5
6
7
8
9
// 监听app_dispatch
$this->hook->listen('app_dispatch');

$dispatch = $this->dispatch;

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

最后就是进行调度。获取response。

1
2
3
4
5
$this->middleware->add(function (Request $request, $next) use ($dispatch, $data) {
return is_null($data) ? $dispatch->run() : $data; // 添加中间件
});

$response = $this->middleware->dispatch($this->request); // 调度上面添加的中间件

四、路由访问

thinkphp访问一个文件存在以下几种方式。

4.1、路由过程

APP.php 中,首先thinkphp会检测路由。依据访问的URL解析出对应的模块、控制器和动作。

1
2
3
4
5
6
7
8
9
// 监听app_dispatch
$this->hook->listen('app_dispatch');

$dispatch = $this->dispatch;

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

routeCheck()

调用本文件中的 routeCheck() 方法,URL路由检测(根据PATH_INFO)。在 routeCheck() 方法中,有两个重要的方法需要关注,一个是 $this->request->path() 用于获取当前请求URL的pathinfo信息(不含URL后缀) ,另一个是 $this->route->check 检测这个路由是否有效。

https//cdnjsdelivrnet/gh/imageformarktext/image/picgo2020/02/07/20200207123504png

path()方法

https//cdnjsdelivrnet/gh/imageformarktext/image/picgo2020/02/07/20200207135151png

然后进入 pathinfo() ,会在这里判断输入的路由格式,即判断是哪种访问方式。在thinkphp5.1中,可以使用两种方式,一种是 入口文件/模块/控制器/action 。另一种就是 入口文件?s=/模块/控制器/action

https//cdnjsdelivrnet/gh/imageformarktext/image/picgo2020/02/07/20200207135258png

回到 routeCheck() 方法中。上面的 path() 返回了路由信息。然后调用 check() 方法。该方法会返回一个对象。

https//cdnjsdelivrnet/gh/imageformarktext/image/picgo2020/02/07/20200207124547png

这里返回了一个 UrlDispatch 对象。这个对象存在 init 方法。后面会被调用。

https//cdnjsdelivrnet/gh/imageformarktext/image/picgo2020/02/07/20200207124740png

然后返回到 APP.phprun 方法中。其中 routeCheck() 会返回上面的 UrlDispatch 对象。然后调用该对象的 init 方法。

1
$dispatch = $this->routeCheck()->init();

init()方法

https//cdnjsdelivrnet/gh/imageformarktext/image/picgo2020/02/07/20200207135347png

$this→parseUrl()方法

这个函数用来解析URL地址。就是根据传入的URL参数来找到模块、控制器和操作。

https//cdnjsdelivrnet/gh/imageformarktext/image/picgo2020/02/07/20200207130915png

返回一个封装的路由信息。

https//cdnjsdelivrnet/gh/imageformarktext/image/picgo2020/02/07/20200207131041png

返回到 init 函数中,下图为解析后的内容。最后返回一个 Module 对象。

  • 这个 Module 对象的init方法也是返回 Module 对象。
  • Module 对象继承了 Dispatch 对象。 Dispatch 对象存在 run 方法。后面会被用到。

https//cdnjsdelivrnet/gh/imageformarktext/image/picgo2020/02/07/20200207135433png

接下来就是后面的路由调度。

4.2、路由调度

下图有两个步骤。

https//cdnjsdelivrnet/gh/imageformarktext/image/picgo2020/02/07/20200207135502png

其中 add() 是添加一个中间件。这个中间件就是传入的匿名函数。 $data 是在出现异常时返回的内容。如果没有出现异常,中间件将会执行 $dispatch->run() 函数。上面提到过,Module 对象继承了 Dispatch 对象。 Dispatch 对象存在 run 方法。

在下面的 $this->middleware->dispatch() 为调用中间件。即执行上面的 $dispatch->run() 函数。跟进这个函数后,继续跟进 exec() 。注意,这里的主体对象仍然是 Module 对象。

https//cdnjsdelivrnet/gh/imageformarktext/image/picgo2020/02/07/20200207135526png

exec() 函数中,会找到控制器、操作等。然后通过 invokeReflectMethod 方法进行调用。

https//cdnjsdelivrnet/gh/imageformarktext/image/picgo2020/02/07/20200207132523png

下图是我访问这个地址时的调用: http://127.0.0.1/public/index.php/index/index/hello?a=1

https//cdnjsdelivrnet/gh/imageformarktext/image/picgo2020/02/07/20200207135839png

经过对控制器调用结束后,将会执行到下面的内容。在 $this->exec() 中会调用第一次 autoResponse 函数,然后将内容变成 Response 对象。然后在下面的代码中将第二次调用 autoResponse 。这里进行验证,判断当前返回是否为 Response 对象。

https//cdnjsdelivrnet/gh/imageformarktext/image/picgo2020/02/07/20200207140457png

五、Response的send方法

其中send方法主要就是将数据发送到客户端。