Note - This is cross post from our work at Projectdiscovery Blog

Introduction

At ProjectDiscovery, our focus is on enhancing our open-source solution, Nuclei, by incorporating templates for trending CVEs. Our collaborative efforts involve constant additions of templates by the open-source community, internal template and research team to stay updated on emerging exploits.

One such notable case involves MOVEit Transfer, a widely utilized web application, which recently came under scrutiny due to a series of SQL injection (SQLi) vulnerabilities. These vulnerabilities affect various versions of MOVEit Transfer that have been released. The exploitation of these vulnerabilities could potentially grant unauthorized access to the MOVEit Transfer database, allowing attackers to manipulate and expose sensitive information.

This week, MOVEit Transfer released yet another security update addressing multiple vulnerabilities, including CVE-2023-36934, an unauthenticated SQL Injection vulnerability. Our in-house vulnerability research team deployed both a patched and an unpatched version of MOVEit Transfer for analysis, with the objective of examining the changes made in the security release and reproducing the unauthenticated SQL Injection.

In this blog post, we aim to provide a comprehensive analysis of CVE-2023-36934, shedding light on the nature of the vulnerabilities, and their potential impact, and sharing the journey of code review undertaken by our team.

Analyzing the Patch Diff

In the advisory, we observed the availability of a hot patch for certain versions through drop-in DLL files. This presents a convenient opportunity for us to streamline our search by focusing on these specific DLLs.

To examine the changes, we decompiled the DLL files of both the unpatched and patched versions and conducted a Git diff on the decompiled C# code. We were able to compare the decompiled code between the unpatched and patched versions conveniently.

After analyzing the unpatched and patched versions, we noticed that there were changes in UserEngine::UserProcessPassChangeRequest() function, which could be called from human.aspx and machine.aspx. Upon closer inspection, it became very obvious that the earlier code was vulnerable to an SQL injection if you could control the MyLoginName variable.

Initially, it may appear that an SQL Injection is possible by injecting the payload through the Arg01 parameter in the request. However, MOVEit mitigates this risk by applying sanitization through the SILUtility.XHTMLClean() function, which effectively prevents any successful exploitation of this SQLi vulnerability.

With this understanding, our focus shifted to finding a method to bypass the sanitization and manipulate the Arg01 parameter in an unsanitized manner. To comprehend our approach, it is important to grasp how the MOVEit software processes requests.

In the case of human.aspx, parameter assignment is carried out through siGlobs.GetIncomingVariables(). This function scans the request and parses the available parameters, including those from form posts, query strings, multipart forms, encrypted query strings via the ep parameter, and, finally, from the database if the parameters were previously set in the session as vars.

All the mentioned function calls primarily involve parsing the provided parameters and applying sanitization through the use of XHTMLClean() method. The sanitized values are then stored in the siGlobs object.

The final step in this process is attempting to retrieve session variables from the database. The logic behind this is as follows: if a session is provided via a cookie (ASP.NET_SessionId), the backend system will check if any variables were previously set for this session. If such variables exist, they will override the parameters received in the current request.

Values are encrypted while inserting and decrypted while reading in app Our subsequent course of action involved identifying the points at which these variables are stored in the database. Among the relevant functions, SaveArgumentsToSessionForRedirect() stood out due to its associations with unauthenticated actions.

Attempt #1

After some grepping and reading the code, we stumbled upon a particular code snippet that caught our attention. This code segment piqued our interest because it sets the value of Arg01 from the Password variable. Upon reviewing the code, we realized that Password was not subject to sanitization, implying that Arg01 would be assigned an entirely unsanitized value in this scenario.

siGlobs.FromSignon = "1";
siGlobs.Transaction = "msgpassword";
siGlobs.Arg01 = siGlobs.Password;
siGlobs.Arg02 = text3;
siGlobs.Password = "";
siGlobs.Arg07 = "";
siGlobs.SaveArgumentsToSessionForRedirect();

However, there is a complication we encountered. This code snippet also assigns values to the transaction and FromSignon variables, which disrupts our intended code flow. If the provided transaction parameter is overridden with the value msgpassword it prevents us from executing the passchangerequest transaction. Nevertheless, we were still determined to validate whether this code could indeed save an unsanitized value for Arg01 and potentially trigger an SQL injection, particularly when the transaction variable was not set in the database but through our parameter value.

To confirm this, We set up Rider debugger and manually removed the transaction variable from the database for our session and observed that although special characters were now present in Arg01, the function call to UserEngine::UserProcessPassChangeRequest() did not occur. However, when we removed the special characters from Arg01, the function call worked properly. After spending several hours debugging and investigating this approach, we realized that it was a dead end, and we needed to explore alternative methods.

Attempt #2

While exploring additional calls to the UserProcessPassChangeRequest function, we came across another invocation of the same function in SILMachine.cs (machine.aspx):

However, the sanitization process was once again applied to the LoginName variable. Feeling a bit stuck, we decided to revisit the git diff searching for potential clues. To our delight, we discovered a highly promising finding—an inconsistency in the sanitization of the Username. After sanitization, UrlDecode() was called, but it should have occurred prior to the sanitization process. This inconsistent order was affecting the LoginName variable as well.

This irregularity in the sanitization order was observed only in the GetEncryptedQueryParameters() function, creating a potential pathway for us. Our plan was to leverage the encrypted parameter ep to assign the LoginName value (URL Decoded) within the siGlobs object, while passing the transaction parameter, possibly through a POST parameter as using the transaction via query parameters (encrypted or unencrypted) was not permitted.

POST /machine.aspx?ep={encrypted{Username=sql%27injection}} HTTP/2
Host: 192.168.29.73
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Content-Type: application/x-www-form-urlencoded
Content-Length: 29

Transaction=passchangerequest

With this strategy in mind, we anticipated a straightforward path forward. However, our optimism quickly faded when we realized that the Machine.aspx page did not support encrypted query parameters. Unfortunately, this functionality was limited to human.aspx.

The Dilemma

At this point, we are presented with two potential paths to explore using human.aspx and its encrypted parameter capabilities:

  • Find a method to set the Session Variables with Transaction passchangerequest and unsanitized LoginName through encrypted parameters.

  • Find a method to set the Session Variable LoginName unsanitized through encrypted parameters without setting transaction.

After doing some grep-ing and carefully reading of code, we ruled out the possibility of the latter scenario. It became evident that the Transaction variable was only left unset in a specific scenario that occurred after authentication. Consequently, our focus shifted towards pursuing the former path mentioned earlier.

The Final Attempt

With the debugger set up, we examined each invocation of the SaveArgumentsToSessionForRedirect() function, eliminating options that wouldn’t serve our purpose. During this process, one particular call caught our attention, buried deep within a series of if/switch statements. Upon closer inspection in the debugger, we noticed that just above this call, the transaction and LoginName variables were set exactly as we desired. However, we encountered difficulty in reaching this point in the code.

try
{
	string value3 = siGlobs.MyRequest.Cookies["InitialPage"].Value;
	siGlobs.objDebug.Log(60, string.Format("{0}: InitialPage cookie found: {1}", "Human_Main", value3));
	if (Operators.CompareString(value3, CallingPage, TextCompare: false) == 0)
	{
		break;
	}
	string text6 = "";
	string text7 = value3.ToLower();
	if (Regex.IsMatch(text7, "[a-z0-9]+\\\\.aspx"))
	{
		if (Operators.CompareString(text7, "certtouser.aspx", TextCompare: false) == 0)
		{
			if (!SILUtility.StrToBool(siGlobs.FromCertToUser))
			{
				text6 = MyTarget.Substring(0, MyTarget.LastIndexOf("/") + 1) + value3;
			}
		}
		else
		{
			text6 = MyTarget.Substring(0, MyTarget.LastIndexOf("/") + 1) + value3;
		}
		if (Operators.CompareString(text6, "", TextCompare: false) != 0)
		{
			siGlobs.objDebug.Log(60, string.Format("{0}: Redirecting to {1} due to InitialPage cookie", "Human_Main", value3));
			siGlobs.SaveArgumentsToSessionForRedirect();
			siGlobs.CleanupVariables();
			siGlobs.MyResponse.Redirect(siGlobs.MakeEncryptedURLIfNec(text6));
		}
	}
}

As you can see, This specific call required us to set the InitialValue cookie with a value other than the calling page, which in this case was human.aspx. This realization shed light on the missing link that was preventing us from calling this function.

Upon making this request, we would receive a session ID, which, crucially, would have the transaction parameter set to passchangerequest and the LoginName parameter set to our SQL Injection payload. This outcome is significant as it aligns with our objective of exploiting the SQL Injection vulnerability.

Now, the only remaining task for us is to set the session cookie in the machine.aspx request and observe the execution. At this stage, we anticipate that the session variables will be successfully set, resulting in an SQL exception error due to the injected SQL payload.

This could further be exploited, to insert a new active session in the database. We could utilize Nuclei to perform these consecutive requests to generate a sysadmin session and access token.

This Nuclei template is now available in nuclei-templates GitHub repository – CVE-2023-36934

Conclusion

The series of SQL injection vulnerabilities in MOVEit Transfer serves as a reminder of the importance of these practices. Additionally, Nuclei, an open-source project, provides a powerful tool for automating security scanning and detection. By utilizing Nuclei’s templating framework and joining the open-source community, organizations can swiftly identify vulnerabilities and contribute to the continuous improvement of the tool.

By embracing Nuclei and participating in the open-source community or joining the Nuclei Cloud Beta program, organizations can strengthen their security defenses, stay ahead of emerging threats, and create a safer digital environment. Security is a collective effort, and together we can continuously evolve and tackle the challenges posed by cyber threats.