Patch Diffing: MOVEit Transfer Pre-Authenticated SQL Injection Vulnerability (CVE-2023-34362) – Part1

Although, the MOVEit Transfer N-Day exploit party is over, I recently started my Patch Diffing journey, so I was looking for another target to practice my skills and survive the painful journey of patch diffing / exploit development. The analysis of unauthenticated SQL injection vulnerability in MOVEit Transfer (CVE-2023-34362) appeared to be challenging yet rewarding journey.

In this blog post, we will go over the following:

  1. Download the patched and unpatched files or installers.
  2. Install the application and perform basic configuration if needed.
  3. Identify the changed files.
  4. Identifying the changes in patched file(s).
  5. Understand if all of the identified changes are related to each other, specifically to SQL injection vulnerability. If yes, figure out how they’re related.
  6. Identify the API endpoint (entry point or HTTP request) which is vulnerable to SQL injection along with required HTTP headers, and parameters (query strings) etc.
  7. Craft our first successful HTTP request and send it locally from the same MOVEit Transfer server.
  8. Check if the vulnerable endpoint can be accessed remotely, meaning if we can send our crafted HTTP request from any remote machine that can talk to the vulnerable MOVEit Transfer server.
  9. If not, then check if there’s endpoint that will help us reach/hit the vulnerable endpoint remotely.
    • If there’s one, then identify the HTTP headers and/or parameters (query stings) required for this endpoint to reach the vulnerable code path or function.
    • Craft our first HTTP request and send it remotely.
  10. Outline the next steps for Part 2 of this blog post (identifying and exploiting SQL injection).

To start with, I downloaded the evaluation copies of MOVEit Transfer 2023, unpatched version 15.0.0.31, and patched version 15.0.2.39 from their official website, installed them on Windows Server 2022 machines, and did some basic configuration such as creating an organisation and adding a new low privileged user, User1.

The unpatched version wasn’t available for download, but manipulating the latest download URL helped me get the unpatched version too. Had it not worked, the WayBackMachine would have been the only option.

Locating the Changed Binaries

Alright, two new directories are created post successful installation, “C:\MOVEitTransfer\” and “C:\Program Files\MOVEit\” and the webroot is under “C:\MOVEitTransfer\” directory. We need to copy these directories from patched and unpatched targets, diff them to find changed files, and see what exactly has changed.

As you can see, total 133 binaries have been changed in the patched version. We can use the file command to check if it’s a native C code.

As you can see from the screenshot above, it’s mix of both, native C code and compiled binaries using Microsoft .Net. While we can feed all these changed .Net binaries to a .Net decompiler tool called ILSpy, it would be very laborious process. Also, the vendor provided DLL drop-ins in the June 9th patch, so we can take advantage of that to save us some time. I couldn’t find a link to that DLL drop-ins patch again, but will update this blog post with the URL once I get it.

This DLL drop-ins patch contains three files, midmz.dll, MOVEit.DMZ.ClassLib.dll, and MOVEitISAPI.dll. Since, the mimdz.dll and MOVEit.DMZ.ClassLib.dll are .Net compiled binaries, we can decompile them using ILSpy and look at the diff.

Once we import these files into ILSpy, we need to select them, right click, and click Load Dependencies. This will load all their dependencies. After that, to decompile the .Net code, we need to select all the loaded files, right click, and then click Decompile to new tab. Finally, we need to right click and click Save Code. While loading and decompiling all the dependencies may not be necessary, I just prefer it that way.

We need to perform these steps for both, patched and unpatched binaries.I saved the unpatched ILSpy code under the directory “MOVEit_Diff/Exported_Projects_Unpatched/” and patched code under “MOVEit_Diff/Exported_Projects_Patched/. We can now diff the “Exported_Projects_Unpatched/midmz/” and “Exported_Projects_Patched/midmz/” directories to find the differences. Same goes for the patched and unpatched “MOVEit.DMZ.ClassLib” directories.

┌──(kali㉿kali)-[~/Desktop/MOVEit_Diff]
└─$ pwd
/home/kali/Desktop/MOVEit_Diff
┌──(kali㉿kali)-[~/Desktop/MOVEit_Diff]
└─$ diff -r ./Exported_Projects_Unpatched ./Exported_projects_patched > diff.txt
┌──(kali㉿kali)-[~/Desktop/MOVEit_Diff]
└─$

Spotting the Differences

Next, we need to check what has been changed or removed in the patched files. Skimming through the diff.txt, we see that the entire class method SetAllSessionVarsFromHeaders() has been removed from MOVEit.DMZ.Classlib.DLL –> SILHttpSessionWrapper class from the patched version.

Also, a call to the method SetAllSessionVarsFromHeaders() is removed from midmz.dll –> MOVEit.DMZ.WebApp –> SILMachine2 class.

Apart from that, some code related to MOVEit.DMZ.Classlib.DLL –> UserEngine –> UserGetUsersWithEmailAddress() method along with related SQL queries is also changed. Here’s the excerpts from diff.txt:

To summarize, the following changes have been made:

  1. The SetAllSessionVarsFromHeaders() method has been removed from MOVEit.DMZ.Classlib.DLL –> SILHttpSessionWrapper class.
  2. A call to SetAllSessionVarsFromHeaders(ServerVars) is removed from midmz.dll –> MOVEit.DMZ.WebApp –> SILMachine2 class.
  3. The code related to MOVEit.DMZ.Classlib.DLL –> UserEngine class –> UserGetUsersWithEmailAddress() method is updated.
  4. Lots of SQL queries from MOVEit.DMZ.Classlib.DLL –> UserEngine class have been updated which could be the fix for this SQL injection vulnerability.

Digging a Little Deeper

Now, we need to understand if all these updated/removed methods, variables, and SQL queries are related to each other. If yes, then how they are linked and how we can control them to perform SQL injection.

Let’s analyze the decompiled SetAllSessionVarsFromHeaders() method, ServerVars variable, and X-siLock-SessVar header in ILSpy to understand as to why this code was partly removed or updated.

This is what the SetAllSessionVarsFromHeaders() method does:

  1. Takes the ServerVars string as an argument which contain HTTP request headers.
  2. Checks if the X-siLock-SessVar header is present in the array of strings derived from splitting ServerVars.
  3. If the X-siLock-SessVar header is found, split it to derive a key/value pair.
  4. Notice that the key/value pair is constructed directly from the X-siLock-SessVar value without performing any input validation or sanitization. Is SeverVars[‘X-siLock-SessVar‘] already sanitized before calling SetAllSessionVarsFromHeaders()?

The header name, X-siLock-SessVar suggests that it’s a session variable whose value is a key/value pair, for example, MySessVar: Kapil.

But where does ServerVars get values for X-siLock-SessVar header from? We can use the ILSpy’s Analyze feature to get a function call graph for SetAllSessionVarsFromHeaders().

As you can see, the SetAllSessionVarsFromHeaders() is defined in MOVEit.DMZ.Classlib.dll –> SILHTTPSessionWrapper class and it’s called from a private method DoTransaction(). The entire call graph looks like this: Machine2Main() –> DoTransaction —> SetAllSessionVarsFromHeaders().

The following screenshot shows the Machine2Main() code. As you can see, the ServerVars is assigned a value in Machine2Main() method.

The ServerVars gets its value from ServerVariables (HTTP request headers) and it’s then passed to the CrackInput() method for string comparison.

The CrackInput() method retrieves several HTTP request headers from ServerVars and one of the headers, X-siLock-Transaction‘s value is stored in a variable called InputTransaction; however, the X-siLock-Transaction header is passed to GetHeaderValue()–> MimeDecodeString() before storing it in InputTransaction.

By examining the code for GetHeaderValue() and MimeDecodeString(), we can find the answer to our previous question: Is SeverVars[‘X-siLock-SessVar’] already sanitized before calling SetAllSessionVarsFromHeaders()? The answer is no; it is not! The X-siLock-SessVar is not sanitised anywhere within the CrackInput() method. This may be our gateway to SQL injection.

After performing some checks, DoTransaction() method is called. Here’s the code for DoTransaction().

The value of X-siLock-Transaction, which appears to be modifiable by the user, is stored in a variable named InputTransaction and represents an action or transaction. The DoTransaction() method checks if it contains one of the pre-defined transactions (Switch/Case) and performs actions accordingly, for example, downloading or uploading files etc.

Also, if the X-siLock-Transaction header is set to session_setvars, then the SetAllSessionVarsFromHeaders() method is called. So far, looking at the SetAllSessionVarsFromHeaders() and DoTransaction() methods, it seems that we do control both the HTTP headers, X-siLock-SessVar and X-siLock-Transaction.

We now know that we can control the X-siLock-SessVar and X-siLock-Transaction header values and hit the SetAllSessionVarsFromHeaders() method through Machine2.aspx –> Machine2Main() –> DoTransaction().

From Machine2.aspx to SetAllSessionVarsFromHeaders() via DoTransaction()

To validate our assumption, we can try sending a simple HTTP GET request to /machine2.aspx endpoint locally from the MOVEit Transfer server itself. We will be sending an unauthenticated request without the aforementioned headers:

Though the advisory says it’s an unauthenticated SQL injection vulnerability, let’s try sending the same request with a low privileged users, User1’s session cookie. Later on, we will try to figure out how this machine2 endpoint can be accessed in an unauthenticated request.

As you can see, accessing the https://localhost/machine2.aspx without sending the required headers throws an error “Invalid Transaction ‘ ‘ . This is what we see in the DMZ_WEB.log:

It’s irrelevant for this blog post, but I wonder what would happen if an unauthenticated or may be a low privileged user downloads this log file through built-in feature (if any) or by discovering a Local File Inclusion (LFI) vulnerability. He would then have access to the currently logged in users’ session cookies, including admin and sysadmin!

Anyway, let’s try sending the X-siLock-Transaction header. Since we need to reach Machine2.aspx –> Machine2Main() –> DoTransaction() –> SetAllSessionVarsFromHeaders(), we need to set the transaction to session_setvars which in turn requires another header, X-siLock-SessVar with may be a valid key/value pair; however, for now, we will use a dummy value MySessVar: Kapil

That’s a good news! And this is what we see in the DMZ_WEB.log:

Let’s try to send this request remotely, from different machine.

That’s a bad news, but not entirely unexpected, considering our prior observation of the Machine2Main() code.

Into the Labyrinth

Though we figured out potential entry points, such as the headers, variable, and API endpoint machine2.aspx which is required for SQL injection, we aren’t able to make this HTTP request remotely because machine2.aspx is accessible only over Loopback IP. The DMZ_WEB.log confirms this:

So, what are our options?

  1. Find a way to bypass the reverse IP lookup and/or hostname check. May be setting the attacking machine’s NETBIOS name same as the target MOVEit Transfer server’s NETBIOS name?
    • Update: I tried this on my Kali machine but it didn’t work due to the strict IP type validation.
  2. Find another endpoint which will route our requests to machine2.aspx.

It seems that the only available choice is to locate an alternate endpoint capable of routing our requests to machine2.aspx. But, how can we identify such an endpoint? So far, we’ve seen three errors from machine2.aspx. We can try to grep the MOVEit_Diff directory where we’ve kept the decompiled files from ILSpy to see where these errors come from.

Apparently, none of the files that we’ve decompiled deal with these errors. Next, we can try to grep for HTTP response headers that we get from machine2.aspx.

Let’s try to grep that directory for some of the strings and errors that we saw in DMZ_WEB.log as well as the X-siLock-Transaction header that we control.

As you can see from the screenshots above, the MOVEitISAPI.dll contains X-siLock-Transaction and some of the machine2.aspx response header strings, including the string machine2.aspx! If you can recall, this is one of the DLLs from DLL drop-ins patch that we haven’t looked at yet.

Based on our previous findings, the MOVEitISAPI.dll is a native C compiled binary that cannot be decompiled or analysed by our .Net decompiler, ILSpy. To obtain the decompiled code, we will need to load the unpatched MOVEitISAPI.dll into Ghidra, go to Analyze, and export it as a C program. However, the resulting code may not be easily readable, so we may have to fix function signatures, variable names and/or export the patched and unpatched versions to BinDiff format if needed.

While searching for X-siLock-Transaction string through more than a million lines of decompiled code, we see a reference to the function FUN_1800704(). The X-siLock-Transaction header is passed to this function as an argument. Subsequently, the transaction string folder_add_by_path is passed to the function FUN_1804da72c(), and its return value is stored in a variable iVar4.

If the return value (iVar4) is zero, then later in the same code block (if (iVar4 ==0)), the function FUN_1800d8520() is invoked, which forwards the request to machine2.aspx.

However, we’re already using the X-siLock-Transaction header to trigger the session_setvars transaction. How we can go about this?

Based on our observation, and since MOVEitISAPI.dll is located under the directory “C:\MOVEitTransfer\MOVEitISAPI”, we can try sending the following request from our remote machine.

Now we’re getting different error, “Unrecognized action”. We can get more information about this event in DMZ_ISAPI.log.

It seems that the MOVEitISAPI.dll expects a query string in the request, otherwise it throws “unrecog action” error. Also, it seems that the action= is the query string it’s looking for. While searching for the error string “unrecog action” in the decompiled MOVEitISAPI.dll code, we see the following code block.

It compares the query string, probably action=, with the character array acStack_5b00 and if it’s not m2, then it calls FUN_1800d8520 which throws an error “unrecog action”. To further validate our assumption about the query string being action=, we can search it in Ghidra –> Search –> Program Text.

This confirms that there’s indeed a parameter called action= and one of the actions is m2. However, we still need to deal with another problem, sending two transactions, folder_add_by_path , and session_setvars using the same header, X-siLock-Transaction.

I played with it a little bit and noticed that it doesn’t perform strict matching on header value, so sending an additional tampered X-siLock-Transaction header should work.

I tried sending this request a few times, but kept getting 0 bytes in the response. However, looking at the DMZ_ISAPI.log, we can be sure that we’ve crafted the request correctly.

I’m not sure why, but sending this request through cURL works just fine.

Alright, we were able to reach Machine2.aspx –> DoTransaction() –> SetAllSessionVarsFromHeaders() through /moveitisapi/moveitisapi.dll?action=m2!

Summary

We started with installing patched and unpatched MOVEit Transfer targets to identify the changed binaries and gradually moved to step 7 where, based on the vulnerable entry point machine2.aspx, HTTP headers and parameters we identified, we successfully crafted our first HTTP request to reach Machine2.aspx –> DoTransaction() –> SetAllSessionVarsFromHeaders().

However, we couldn’t connect to machine2.aspx remotely because it’s accessible only over Loopback IP which forced us to look for an alternate endpoint capable of routing our requests to machine2.aspx. Fortunately, we could figure out that Machine2.aspx –> DoTransaction() –> SetAllSessionVarsFromHeaders() can be reached through /moveitisapi/moveitisapi.dll?action=m2.

So, this is how the entire flow looks like: /moveitisapi/moveitisapi.dll?action=m2 –> Machine2.aspx –> DoTransaction() –> SetAllSessionVarsFromHeaders().

What’s next?

Though it’s a lot for the part 1 of the blog post, we are not done yet. We still need to:

  1. Find and exploit the SQL injection vulnerability.
  2. Since there are lots of changes for UserGetUsersWithEmailAddress() function as well as related SQL queries in the patched version, check if that’s something can give us SQL injection.
  3. We’ve been accessing this endpoint with a low privileged user, User1. We need to find a way to access it without any authentication.

When time permits, I will work on that and post the part 2 of this blog post. Until next time 🙂

Leave a comment