Anti-malware malware

This blog post aims to share a tale from a recent pentest that I felt was too good to keep to myself.

Note: The names of the innocent have been changed to protect the guilty.

Background

The testing involved a web application that was designed to guide users through an application process that required documents to be submitted to the server via a file upload. The client for this pentest was understandably concerned about the security implications of handling untrusted user-supplied files, so they devised a system that would vet said files for malware prior to making them available for review.

The basic process for validating the files was as follows:

In terms of the architecture behind the system, a number of moving parts were introduced:

  • A cloud-based file storage container for hosting uploaded files
  • An API endpoint for generating shared access signatures (SAS) granting access to file uploads
  • A virtual machine responsible for performing antivirus checks and document manipulation of untrusted files
  • Polling functions that were executed once files were added to the cloud storage (to initiate the document sanitisation process)

Note: As a pentester, I had access to a lot of information about how the system worked behind the scenes. I had access to the source code, and the logging system, so I could validate when uploaded files were successfully or unsuccessfully processed.

SAS API Endpoint

Shared access signatures (SAS) are signed requests that provide access to Azure’s Blob Storage. Access can be either read or write, depending on the usage requirements. In this instance, the application exposed an API endpoint that would generate the write-only SAS on behalf of the user, similar to:

GET /GetSAS?upload=Myfile.png HTTP/1.1
Host: application.example.com

Which would return:

https://example.blob.core.windows.net/untrusted/Myfile.png?sv=2012-02-12&st=2009-02-09&se=2009-02-10&sr=c&sp=r&si=YWJjZGVmZw%3d%3d&sig=dD80ihBh5jfNpymO5Hg1IdiJIEvHcJpCMiCMnN%2fRnbI%3d

The source code for this component resembled the following (NOTE: The actual code was C#/Dotnet, but I’ve used Python to demonstrate the logic):

def getSAS(filename):
    blobname = filename.split("/")[-1] # strip any path characters
    blobname = "untrusted/" + blobname
    sas = cloud.GetSAS(blobname, access="write")
    return sas

There are a couple of problems with this approach.

First, the user-supplied filename was preserved by the SAS-granting API endpoint. The application generated a UUID for each file using Javascript; however, as this process was performed client-side, the UUID could simply be overwritten with any filename. This meant that the efforts the application took to generate UUIDs for uploaded files were fruitless.

Second, the above sample code failed to adequately strip non-forward-slash filesystem metacharacters out of the supplied filename. Despite removing forward slashes (by way of truncating anything preceding the final forward slash in the filename), the application happily processed requests for filenames containing path characters such as ~, \, :, ., etc. This was a stroke of luck for the tester, as the SAS API would happily grant a valid SAS for paths containing such characters.

As a result, it was possible to perform a directory traversal attack and write to additional file containers outside of the designated untrusted/ directory. For example, an attacker could write into a super secret container by using path traversal and back slashes (such as ..\..\..\..\..\supersecret) within their request:

GET /GetSAS?upload=..\..\..\..\supersecret\Myfile.png HTTP/1.1
Host: application.example.com

Which would return https://example.blob.core.windows.net/supersecret/Myfile.png?sv=2012-02-12&st=2009-02-09&se=2009-02-10&sr=c&sp=r&si=YWJjZGVmZw%3d%3d&sig=dD80ihBh5jfNpymO5Hg1IdiJIEvHcJpCMiCMnN%2fRnbI%3d

Although read access was not granted to the SAS, write access was. This meant that the attacker could overwrite known files within containers inside (or outside) of the designated container.

Whilst neat, this was by no means interesting. We have to get a shell after all. And this river was running dry.

Time to regroup.

Cloud file storage and virtual directories

While testing the previously discussed directory traversal vulnerability, I noticed some curious behaviour. By requesting an SAS for a file within a defined directory tree (such as /this/is/Myfile.png), the file upload system would happily serve the file at that path. For example:

GET /GetSAS?upload=this\is\Myfile.png HTTP/1.1
Host: application.example.com

https://example.blob.core.windows.net/untrusted/this/is/Myfile.png?sv=2012-02-12&st=2009-02-09&se=2009-02-10&sr=c&sp=r&si=YWJjZGVmZw%3d%3d&sig=dD80ihBh5jfNpymO5Hg1IdiJIEvHcJpCMiCMnN%2fRnbI%3d

As it happened, the file storage container would create a virtual directory structure to match the filename of the uploaded file.

The storage container for this example would look like this:

untrusted/
└── this
    └── is
        └── Myfile.png

This was starting to get interesting.

The virtual machine

So, at this point I knew that uploaded files would get downloaded to a virtual machine and then scanned for viruses. The application logic involved with this task resembled the following: (NOTE: Again, the actual code was C#/Dotnet, but I’ve used Python to demonstrate the logic)

def trigger_scan(filename):
    file_contents = cloud.download_file(filename) # get the file from the blob store
    tmp_path = os.path.join(create_temp_path(), filename) # store this in a safe temp directory! (assume create_temp_path() returns "C:\Temp")
    with open(tmp_path, 'wb') as f:
       f.write(file_contents)
    x = subprocess.check_output(["C:\\Program Files (x86)\\AVProgram\\scanner.exe", tmp_path])
    # [...] Check that file was not malicious, then perform subsequent document transformation

So upon successful file upload, the function trigger_scan would execute automatically, performing the necessary operations to validate and then sanitise and transform the supplied file. At first I toyed around with trying to escape out of the process execution call to no avail.

What I could do; however, was control the filename that was concatenated using the Path.Combine function using the previously discussed path traversal attack. I toyed around with uploading various files for a while, and, after a bit of local testing with a minimal PoC, I determined that if the second argument to the Dotnet method Path.Combine was an absolute path, it ignored the first argument and returned the supplied absolute path.

According to the docs, this is intended behaviour:

“If one of the specified paths is a zero-length string, this method returns the other path. If path2 contains an absolute path, this method returns path2.” (Microsoft docs)

The following sample C# code demonstrates the vulnerability:

using System;
using System.IO;

namespace Program
{
    class Program
    {
        static void Main(string[] args)
        {
            string tmp = Path.GetTempPath();
            Console.WriteLine("Tmp path prefix is: {0}", tmp);

            string x = Path.Combine(tmp, "C:\\test\\a.exe");
            Console.WriteLine("Concatenated path is: {0}", x);
        }
    }
}

When executed:

C:\Users\Hacker>Hack.exe
Tmp path prefix is: C:\Users\Hacker\AppData\Local\Temp\
Concatenated path is: C:\test\a.exe

At this point I had a revelation: I control the absolute path that the file that I am uploading will be written to.

Further testing

Using the power of supplied logs (thanks client!) I could determine certain characteristics of my exploit attempts. I started to enumerate the logged messages, with a particular focus on the error messages.

I tested using a number of samples, including a benign control group (no logged error, application successfully processed the file), the EICAR test AV file (infection found! Oh no!). I then started writing benign files to locations on the system, such as C:\Windows\System32\a.png (Permission Denied!) and C:\test.png (no logged error, application successfully processed the file). At this point I knew I was onto something. If I could successfully write a file to C:\, then the user account performing the operation must have been highly privileged.

Putting it all together

So, to recap; at this stage I:

  • Was able to issue valid SAS for arbitrary locations on the filesystem
  • Could control where the file was written to disk
  • Had the location of a file that was guaranteed to be executed upon successful upload
  • Appeared to be able to write files as a privileged user

I hope you can see where this is going.

Payload

First thing’s first, I needed a payload. I was quietly confident that no competing AV would be in play here (since I knew the AV engine in use), so I went from zero to yolo and smashed out a meterpreter reverse_https payload using msfvenom.

Staging the payload

I then needed a valid SAS for my target payload. As discussed previously I could inject valid filesystem metacharacters into my filename, with the exception of /. I was not prepared to risk using space characters for the payload, so I opted to use another Windows trick; the short name. Long story short (pun totally intended), Windows short names are a means for providing mostly-unambiguous representations of path names greater than 15 characters. As usual it’s some relic from the past that is required for backwards compatibility. Don’t think about it. It’s probably better that way.

Anyway, my payload ended up looking like this:

GET /GetSAS?upload=.\C:\\PROGRA~1\\AVProgram\\scanner.exe HTTP/1.1
Host: application.example.com

Oh yeah that’s right. My goal here was the goods. I’m not just going to compromise this server. No; I’m going to replace the AV’s executable with my payload.

Anyway, the previous request returned the SAS:

https://example.blob.core.windows.net/untrusted/C:/PROGRA~1/AVProgram/scanner.exe?sv=2012-02-12&st=2009-02-09&se=2009-02-10&sr=c&sp=r&si=YWJjZGVmZw%3d%3d&sig=dD80ihBh5jfNpymO5Hg1IdiJIEvHcJpCMiCMnN%2fRnbI%3d

Upload… annnnndddd

I then uploaded the file to the SAS endpoint, and within seconds Metasploit had made a new friend 😀

Bonus points: The trigger_scan() function was running as the NT AUTHORITY\SYSTEM user!

Bonus Bonus points: The AV program is toast at this point; I’ve replaced the binary with a malicious .exe. This has the interesting side-effect of ensuring that anytime a legitimate user uses the application as intended; I get a shell.

Bonus Bonus Bonus points: The application now outright fails to complete its primary purpose of scanning untrusted files for viruses.

Recommendations

Honestly, I am kind of astounded that any of this actually worked. Fortunately, there are numerous ways that the exploit in question could have been trivially stopped dead in its tracks.

  • Flatten filenames to a GUID <- this would have done it
    • Have the SAS endpoint generate a filename for the destination file using a GUID, rather than accepting the user-supplied one
  • Copy the file to the filesystem sans-extension
    • Extension was unnecessary in this context, given the operations required (i.e. AV scanning)
  • Don’t run services as SYSTEM/privileged users…
    • In this instance, given that the AV was executing on demand against a file, administrator privileges were unnecessary
  • Restrict storage container access for the SAS
    • Also don’t let it create virtual directories
  • (Less recommended) If you are going to accept the untrusted filename; sanitise ALL file path metacharacters
    • Probably don’t do this though. Seriously.

There’s just something deeply philosophical (and satisfying) about defeating an anti-malware system by overwriting the anti-malware program with malware.

Leave a Reply

Your email address will not be published. Required fields are marked *