How the Kaseya VSA Zero Day Exploit Worked

This article explains the pre-auth remote code execution exploit against Kaseya VSA Server that was used in the mass Revil ransomware attack on July 2nd, 2021. On July 5th, after an initial investigation of affected organizations, Truesec contacted Kaseya and provided a detailed technical write-up of these vulnerabilities along with forensic evidence of exploitation. Kaseya released the patch 9.5.7a (9.5.7.2994) that addresses the security issues on July 11th.

After validating the patch and verifying that the attack vector is no longer present, we believe it is time to share these details for the benefit of the community. We strongly believe this information will help the security community in their response to the attack and from a larger perspective it will help the industry understand what happened so we can address the underlying issues, and ultimately increase our capabilities to prevent future breaches.

Overview

The exploit abused four vulnerabilities in the Kaseya application that were chained as visualized in the figure below.

The high-level steps of the exploit
  1. Obtained an authenticated session by abusing a flaw in the authentication logic [CWE-304] in /dl.asp.
  2. Uploaded the revil ransomware (agent.crt) through an unrestricted upload vulnerability [CWE-434] while also bypassing the request forgery protection [CWE-352] in /cgi-bin/KUpload.dll.
  3. Uploaded the ASP payload (screenshot.jpg) in the same fashion as described in 2.
  4. Invoked the payload in screenshot.jpg through a local code injection vulnerability [CWE-94] in userFilterTableRpt.asp.
  5. Created Kaseya procedures to copy file and execute the ransomware.
  6. Executed the procedures.
  7. Removed logs and other forensic evidence.

This article will focus on the exploit and thus describe steps 1 through 4 in detail. The payloads are not in scope for this article.

Step 1 – Bypassing Authentication [CWE-304]

The threat actor first sent a POST request to the resource /dl.asp with the POST data userAgentGuid=guid.

kaseya-supply-chain-attack-targeting-msps-to-deliver-revil-ransomware-exploit
First request of the exploit: authentication bypass

In dl.asp, userAgentGuid is used in a SELECT query to lookup the database row of the agent. The agentGuid must exist due to the subsequent if statement. The threat actor used the agentGuid of the agent on the VSA server itself (more on this below).

After the lookup, dl.asp tries to see if the provided password matches the values stored in the database for that agent. The provided password is then compared in several different ways. The login flow is illustrated in the pseudo code below:

if password == hash(row[nextAgentPassword] + row[agentGuidStr]) 
    login ok 
elseif password == hash(row[curAgentPassword] + row[agentGuidStr])
    login ok
elseif password == hash(row[nextAgentPassword] + row[displayName])
    login ok
elseif password == hash(row[curAgentPassword] + row[displayName])
    login ok
elseif password == row[password]
    login failed 
else 
    login ok

The last two statements is where the interesting thing happens. In case the password equals row[password] the login will fail. However, in the case that all checks failed, it would default to an else clause that sets “loginOK” to true.

Because no password was provided in the request, the “password” variable would be NULL and loginOK would end up being true. When loginOK is set to true, the application sends the login session cookie and will eventually (if no other parameters are provided like in the attacker’s request) end up in an if clause that returns 302 redirect to the userPortal.

Response to authentication bypass request.

How did the actor obtain the AgentGuid?

The outstanding question is how the threat actor obtained around 60 (estimated number of VSA victims) unique valid agentGuids. It appears they simply knew the agentGuids before launching the attack.

Truesec have discovered several methods the attack could have been performed without prior knowledge of a valid agentGuid. An example that was also fixed in 9.5.7a is that the userAgentGuid parameter could have been <vsa_server_hostname>.root.kserver instead of an actual agentGuid. Due to what was described as legacy code, in case agentGuid is not a number, it will lookup the agentGuid automatically from the table machNameTab.

Truesec has not found and is not aware of any public evidence that show exactly how these agentGuids were obtained. Possibly, these random agentGuids might have been what limited the impact of the attack to under 60 out of around 35 000 Kaseya VSA customers.

Step 2 & 3 – Uploading files [CWE-434] [CWE-352]

All requests from this point on used the authenticated session obtained in step 1.

The threat actor started the upload by sending a GET request to /done.asp without any parameters. When receiving the request, the application creates a row in the tempData table, stages an upload folder, and finally returns a so-called loadKey value. A valid loadkey is required to perform the file upload.

kaseya-supply-chain-attack-targeting-msps-to-deliver-revil-ransomware-exploit
Request and response to obtain a valid loadKey.

To upload files the threat actor sent a multiform-data POST request to the resource /cgi-bin/KUpload.dll. The request contained the following parameters:

  • FileName (name of the file)
  • FileData (content of the file to upload)
  • LoadKey (the value obtained by GETting done.asp)
  • RedirectPath (path that the application will redirect to after successful upload)
  • PathData (folder the file will be saved in)
  • __RequestValidationToken (bypassable CSRF token)

Bypassing the CSRF protection

The __RequestValidationToken was not properly validated. For example, the value “xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx” was accepted as a valid token.

Uploading the ransomware (Agent.crt)

The first upload performed by the threat actor was a file named agent.crt. This file was an encoded version of the ransomware that was later pushed to all agents from the compromised VSA server.

kaseya-supply-chain-attack-targeting-msps-to-deliver-revil-ransomware-exploit
Upload of agent.crt.

Upon successful upload, the server returns HTTP 200 OK with a body containing a link pointing to /<redirectPath>?FileName=<filename>&PathData=<relative path>&originalName=<filename>&FileSize=<size>&TimeElapsed=<time>.

In this case, as the file upload was successful, the returned link was /done.asp?FileName=agent.crt&PathData=WebPages\ManagedFiles\VSATicketFiles\&originalName=agent.crt&FileSize=1221630&TimeElapsed=00:00:00.828

Uploading the ASP payload (Screenshot.jpg)

The threat actor uploaded another file named Screenshot.jpg. This was not a jpg file but rather a text file containing ASP code. After obtaining another loadKey value from /done.asp the threat actor uploaded the file. Partial contents of the request can be seen in the figure below (unfortunately only a partial capture was obtained and the middle part of screenshot.jpg was missing).

First part of uploaded screenshot.jpg.
kaseya-supply-chain-attack-targeting-msps-to-deliver-revil-ransomware-exploit
Last part of uploaded screenshot.jpg.

The response body link in this case was /done.asp?FileName=Screenshot.jpg&PathData=WebPages\ManagedFiles\VSATicketFiles\&originalName=Screenshot.jpg&FileSize=6188&TimeElapsed=00:00:00.016 which indicates that the file was successfully uploaded.

Step 4 – Executing the payload on the server [CWE-94]

Finally, the threat actor sent a POST request to /userFilterTableRpt.asp with the pageFilterSQLFile argument.

kaseya-supply-chain-attack-targeting-msps-to-deliver-revil-ransomware-exploit
Exploiting the code injection vulnerability.

Due to a flaw in userFilterTableRpt.asp, the contents of the specified file would be interpreted as ASP code as it was passed to the function eval. In this case, ManagedFiles/VSATicketFiles/Screenshot.jpg, the ASP code text file the threat actor just uploaded.

First, userFilterTableRpt.asp sets a variable from the POST parameter. Then it reads the contents of the specified file and passes it to eval, which will by definition interpret the value of the argument as code. The flow is illustrated in the pseudo code below:

f = open (pageFilterSQLFile) 
c = read (f) 
eval (c)  

And that is it. The ASP payload was executed and started pushing out the ransomware, and we all know the story from there.

Final Words

After a patch has been made available to customers of Kaseya VSA, and after we have validated the patch to verify that the attack vector is no longer present, we believe it is time to share these details for the benefit of the community. We strongly believe this information will help the security community in their response to the attack and from a larger perspective it will help the industry understand what happened so we can address the underlying issues, and ultimately increase our capabilities to prevent future breaches.

Finally, a big thanks to everyone in the security community who shared their findings throughout this incident. We truly appreciate the collaborative spirit that ultimately benefits everyone.

Timeline

[2021-07-02] Threat actor exploited vulnerabilities in the wild to mass-deploy ransomware.
[2021-07-04] Truesec obtained evidence that was helpful to understand the exploit.
[2021-07-05] Truesec sent a detailed write-up of the vulnerabilities along with supporting forensic evidence to Kaseya.
[2021-07-11] Kaseya released a security patch.
[2021-07-12] Truesec validated the patch.
[2021-07-13] Truesec published this article.