Skeleton Cookie: Breaking into Safeguard with CVE-2024-45488

Introduction

In this post, we crack open an authentication bypass vulnerability we discovered in the Safeguard for Privileged Passwords product. This vulnerability, assigned CVE-2024-45488, is internally known as “Skeleton Cookie”. We’ll demonstrate how this vulnerability can be exploited to gain full administrative access to the virtual appliance. From there, an attacker can extract passwords and achieve Remote Code Execution.

We’ll cover our process for identifying the Skeleton Cookie issue, delve into some Microsoft DPAPI internals, and provide tools to assist in further DPAPI research.

Safeguard for Privileged Passwords

Safeguard for Privileged passwords is a Privileged Access Management solution made by One Identity, a subsidiary of Quest Software. Below is a description of the product from the One Identity website:

One Identity Safeguard for Privileged Passwords automates, controls and secures the process of granting privileged credentials with role-based access management and automated work flows. It can be deployed as a hardened appliance, hybrid or cloud, which eliminates concerns about securing access to the solution itself.

Vulnerability Details

You can find more details and mitigation advice regarding CVE-2024-45488 in our advisory below:

Analysing Session Cookies

SafeGuard for Privileged Passwords (SPP) exposes a web interface with a login form on TCP port 443, as shown in the following screenshot.

RSTS Login Page

RSTS Login Page

Submitting a valid username and password results in the application returning a session cookie named stsIdentity0, with a base64 encoded value - such as the following:

AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAAwf9Ae502MkO7NHbliGpOMgAAAAACAAAAAAAQZgAAAAEAACAAAACse6NA+djCsd1m3mIN5jGsvPqhK2KPrTsPRa37ZxvaJwAAAAAOgAAAAAIAACAAAABs5Uk8v66K0Y5R2BFQ+O+wqB1TZaRjC3lfk81Ctpdj80AAAAALtE/P4sSyLHCon/9K6H+LiAJIBRYeybHM8nYkWY0LiFiyl37UTzRfRoVwzOB7bU7OI/mukXyh5Vko7Bavu5NpQAAAACwc6QWZqlP7MiICzEWjo05rRdZFKJ4cnWAingBbQ+ldkRX/gzZ0LT7ibTR2B5BU94HCNr3rxoAnbCn6xb7+SiM=

The code relating to the decryption and verification of this session cookie could be found within the .NET executable at:

  • C:\Pangaea\CurrentVersion\Service\Rsts\rsts.exe

The Rsts.AuthenticationCookieData.TryDecryptCookie function contained the following code, indicating that cookies were encrypted with the DPAPI key of the account used to start the RSTS service.

TryDecryptCookie Function

TryDecryptCookie Function

Setting a breakpoint on the function allowed the structure of the decrypted cookie to be examined:

Decrypted Cookie Value

Decrypted Cookie Value

The graphic below shows the various fields that make up the decrypted cookie value, none of which contain information that cannot be reasonably derived.

Decrypted Cookie Structure

Decrypted Cookie Structure

Examining the arguments passed to the ProtectedData.Unprotect() function revealed that no additional entropy was used. This means that anyone with a valid DPAPI key could encrypt their own cookie, which could then be successfully decrypted by the TryDecryptCookie() function.

No Additional Entropy

No Additional Entropy

Finding a Valid DPAPI Key

Checking the DPAPI keys for the RstsService user on the trial version of the virtual appliance showed a pre-existing key, as well as one that was generated following the per-installation setup process:

User DPAPI MasterKeys

User DPAPI MasterKeys

For the assessed trial version (which at the time of testing was an older build), encrypting the cookie using the pre-existing key was sufficient to cause the TryDecryptCookie() function to decrypt a valid cookie value. However, it appeared that this pre-existing key varied across release versions and virtualisation platforms. Therefore, to build a more comprehensive exploit that worked on multiple versions and platforms, we needed to dig deeper into DPAPI internals.

DPAPI Side Quest

Looking again at the code for the TryDecryptCookie() method, we can see that it uses the ProtectedData.Unprotect .NET API method from the System.Security.Cryptography namespace. This method takes a byte array containing the encrypted data, some optional additional entropy (which is omitted in this case), and the protection scope - which in our case is specified as DataProtectionScope.CurrentUser.

The protection scope parameter given to the .NET ProtectedData.Unprotect method is an enum type of DataProtectionScope, which can be one of the following values:

NameValueMeaning
CurrentUser0The protected data is associated with the current user. Only threads running under the current user context can unprotect the data.
LocalMachine1The protected data is associated with the machine context. Any process running on the computer can unprotect data. This enumeration value is usually used in server-specific applications that run on a server where untrusted users are not allowed access.

It is described in the documentation as per below:

One of the enumeration values that specifies the scope of data protection that was used to encrypt the data.

As our TryDecryptCookie code specifies the CurrentUser value for the scope, this would seem to suggest that only the current user (i.e. the account that the RSTS service is running under), could decrypt the data (and thus limited to using the master-keys available to that user). Or at least, that specifying either value would not have any effect whatsoever. However, this did not quite line up with our understanding of the underlying API.

As shown below, the ProtectedData.Unprotect .NET method simply calls the CryptUnprotect unmanaged API function from crypt32.dll.

Unprotect Function

Unprotect Function

When the scope parameter is set to DataProtectionScope.LocalMachine, the .NET method adds a flag with the value of 4 and passes it in the dwFlags argument to crypt32!CryptUnprotectData. However, looking at the supported values for the dwFlags parameter shows that 4 is not a documented value:

ValueMeaning
CRYPTPROTECT_UI_FORBIDDEN (0x1)This flag is used for remote situations where the user interface (UI) is not an option. When this flag is set and UI is specified for either the protect or unprotect operation, the operation fails and GetLastError returns the ERROR_PASSWORD_RESTRICTION code.
CRYPTPROTECT_VERIFY_PROTECTION (0x40)This flag verifies the protection of a protected BLOB. If the default protection level configured of the host is higher than the current protection level for the BLOB, the function returns CRYPT_I_NEW_PROTECTION_REQUIRED to advise the caller to again protect the plaintext contained in the BLOB.

In fact, the 4 value maps to the CRYPTPROTECT_LOCAL_MACHINE flag, but this is normally only specified for the encrypt mode of the API, crypt32!CryptProtectData.

ValueMeaning
CRYPTPROTECT_LOCAL_MACHINE (0x4)When this flag is set, it associates the data encrypted with the current computer instead of with an individual user. Any user on the computer on which CryptProtectData is called can use CryptUnprotectData to decrypt the data.
CRYPTPROTECT_UI_FORBIDDEN (0x1)This flag is used for remote situations where presenting a user interface (UI) is not an option. When this flag is set and a UI is specified for either the protect or unprotect operation, the operation fails and GetLastError returns the ERROR_PASSWORD_RESTRICTION code.
CRYPTPROTECT_AUDIT (0x10)This flag generates an audit on protect and unprotect operations. Audit log entries are recorded only if szDataDescr is not NULL and not empty.

Here it states that by setting the CRYPTPROTECT_LOCAL_MACHINE when encrypting the data, it is possible to specify that any user on the computer can decrypt the data using CryptUnprotectData, meaning that decryption is not tied to a user-specific master-key, but instead uses machine specific keys.

In the remarks section of the CryptProtectData documentation, it also states that the scope is determined by a flag that is set when encrypting the data:

If the CRYPTPROTECT_LOCAL_MACHINE flag is set when the data is encrypted, any user on the computer where the encryption was done can decrypt the data.

So to summarise, from the documentation we can deduce:

  • The CryptProtectData API dwFlags parameter can specify the protection scope.
  • The CryptUnprotectData API dwFlags parameter cannot specify the protection scope.
  • The ProtectedData.Unprotect .NET method calls CryptUnprotectData, specifying the protection scope via the dwFlags, despite not being a documented flag.

Which is quite confusing.

This becomes clearer if we have a look at the underlying structure of the DPAPI blob. A DPAPI blob is the data structure output by the CryptProtectData API. This structure is not officially documented by Microsoft, however there are a number of third-party resources and tools which provide insight.

In the following image of the DPAPI blob structure, we can see that the guidMasterKey value is specified as part of the blob structure itself. This is included in the non-encrypted part of the structure, which specifies various metadata fields, such as the dwFlags fields and the description (if specified).

The following DPAPI blob definition can be found in the mimikatz source code:

DPAPI Blob Structure

DPAPI Blob Structure

This confirms that the master key GUID and the protection scope are defined in the DPAPI blob itself. These values are read during decryption (in dpapisrv!SPCryptUnprotect) to select the master-key used for decryption.

Finally, to confirm the hypothesis that the scope parameter is ignored, we can use the following C# code:

using System;
using System.Text;
using System.Security.Cryptography;

namespace DataProtection
{
    internal class Program
    {
        static void Main(string[] args)
        {
            byte[] inputData = Encoding.ASCII.GetBytes("Hello, DPAPI!");

            // Encrypt the data with the LocalMachine scope
            byte[] encryptedData = ProtectedData.Protect(inputData, null, DataProtectionScope.LocalMachine);
            Console.WriteLine($"encryptedData: {BitConverter.ToString(encryptedData).Replace("-", string.Empty)}");

            // Now try to decrypt it with tue CurrentUser scope
            byte[] decrypted = ProtectedData.Unprotect(encryptedData, null, DataProtectionScope.CurrentUser);
            Console.WriteLine($"decrypted: {Encoding.UTF8.GetString(decrypted)}");
        }
    }
}

Which outputs:

Test Harness Output

Test Harness Output

From this test we can see that specifying a protection scope via the .NET ProtectedData.Unprotect API method has no effect.

Exploitation

Now we know that we can specify any master key GUID in the DPAPI blob, and that we can force the protection scope to Local Machine by setting the dwFlags in the blob, this opens up the possibility to use additional master-keys which might be the same on multiple versions of the appliance.

When decrypting data protected with the Local Machine scope, the following directory is used to select the master-key file (specified in the guidMasterKey blob field):

  • C:\Windows\System32\Microsoft\Protect\S-1-5-18

Looking at the virtual machine disk image, we found additional keys in this directory which dated back to 2019, as shown below:

System DPAPI Keys

System DPAPI Keys

This key was found to be present on all vulnerable versions of the appliance (both VMWare and Hyper-V).

To test this theory, a valid cookie string (stored in cookie.txt) was encrypted using the 98dc3c79-9aa5-4efc-927f-ccec24eaa14e DPAPI master key.

With the contents of cookie.txt being:

local,admin,Primary,Password,20240530T161652Z,20241231T163652Z

In order to specify the desired master-key GUID and protection scope flags, we modified the SharpDPAPI tool to include a new protect action, which allows you to specify the path to an arbitrary master-key file (via the /mkfile argument), and adds the Local Machine protection scope (via the /local argument). The source code for the modified version can be found here. A Python script which can create DPAPI blobs can also be found here.

Modified SharpDPAPI Tool

Modified SharpDPAPI Tool

As shown in the above screenshot, the path to the 98dc3c79-9aa5-4efc-927f-ccec24eaa14e master-key is provided, along with the /local flag. In order to decrypt/use the master-key, we also need to provide the password for the key. This can be extracted from the LSA secrets (HKLM\SECURITY registry hive) using various public tools, or with the mimikatz lsadump::secrets command. The value we need is the DPAPI_SYSTEM secret.

The tool then outputs a base64 encoded string, which is the encrypted cookie value.

To confirm the data is well-formed, attempting to decrypt this cookie with mimikatz on the SPP server shows the master key that is required:

mimikatz # dpapi::blob /in:foo.bin /unprotect
**BLOB**
  <snip>
  guidMasterKey      : {98dc3c79-9aa5-4efc-927f-ccec24eaa14e}
</snip>

The cookie value could then be manually submitted to the SPP server via a GET request in order to retrieve a JSON Web Token, as shown below:

GET Request

GET Request

Decoding the token shows that it is associated with the admin user:

Decoding the JWT

Decoding the JWT

The value of this token could then be sent to the /appliance endpoint, resulting in an authenticated session, by making a request to: https://server/appliance/?access_token=<token>

Exploit Demo

The following video demonstrates exploitation of the authentication bypass.

Vulnerability Check Script

To determine if your instance is vulnerable to CVE-2024-45488, you can use the following script. This script sends a cookie with an invalid username in the cookie. If the server is vulnerable, it will return a JWT in the response that contains the same invalid username, indicating that the server is vulnerable.

#!/bin/bash

if [ -z "$1" ]; then
  echo "Usage: $0 <hostname_or_ip>"
  exit 1
fi

# Cookie with username=CVE-2024-45488
response=$(curl -s -k -b 'CsrfToken=aaa; stsIdentity0=AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAAeTzcmKWa/E6Sf8zsJOqhTgQAAAACAAAAAAAQZgAAAAEAACAAAAB7IyJaIMTIAVf54mYSaV7ONPPzQfbMYTAjNqLvXbFvbwAAAAAOgAAAAAIAACAAAADH1aoSs1rsBIPtdATOTISeuubW9oiZRAA4PxY4qN/5n1AAAABP9SIp760UDBeb3+Q8wJKLzVhkqcjSwT9XuVtEQgjiphWcT999R+7B6gUdv36gbsilcaC43d6znvQmflSEGELLhXsXK3fvuWiEVSAoQ/5YT0AAAABaqPccjcvpjE4xwaXt0yjo4Ug9SK8Y+p+3fMPkHVSognOZOJoAD4oTYZCzHTWgAeXwLgNPChiMTOqEq7mi6RRX' \ 
  "https://$1/RSTS/UserLogin/LoginController?response_type=token&redirect_uri=https%3A%2F%2Flocalhost&loginRequestStep=6&csrfTokenTextbox=aaa")

if echo $response | grep -q "access_token="; then
  echo "[+] The server is VULNERABLE to CVE-2024-45488"
else
  echo "[-] The server is NOT vulnerable to CVE-2024-45488"
fi

Post Exploitation

Once an attacker has gained an authenticated administrative session on the appliance, they can carry out any action that a legitimate administrator user would be capable of. This includes the ability to reconfigure settings on the appliance, or modify policies to allow extraction of passwords stored in managed accounts or personal vaults.

Backup Extraction

If the appliance is configured to use the default backup encryption setting (which uses a hardcoded RSA key), then the attacker can also download and decrypt a copy of any appliance backups. As such, it is strongly recommended that GPG encryption is used for backups. However, it’s worth noting, an attacker with administrative access to the device could change this setting.

The following screenshot shows an example of decrypting the backups protected with the default encryption method:

Decrypting a Backup

Decrypting a Backup

The backup contains various sensitive data, including a copy of the Cassandra database, and the keys required to decrypt it:

Decrypted Backup Content

Decrypted Backup Content

Remote Code Execution

Due to the closed nature of the virtual appliance, it is not typical for security software such as EDR to be installed on the underlying Operating System. This makes it an attractive target for attackers looking to maintain persistence.

As mentioned above, the SPP virtual appliance provides a backup and restore functionality, which can be secured using encryption methods such as passwords, GPG keys, or an embedded RSA key (the default option). Despite the encryption, if an attacker gains access to the private key - through configuration of their own GPG key or using the pre-installed RSA key - they can craft and encrypt malicious backup files.

A crafted malicious backup can be restored by an administrative user, leading to code execution on the appliance. During the restore process, backup contents such as the Registry.json file can be manipulated to introduce arbitrary commands or malicious settings.

For example, the Registry.json file contains registry settings that are applied to the system during restoration. By modifying entries within this file, attackers can insert keys such as:

{
  "HKEY_LOCAL_MACHINE\\Software\\Pangaea\\ApplianceState\\ApplianceA2AStateIsDisabled": "true",
  "HKEY_LOCAL_MACHINE\\Software\\Pangaea\\ApplianceState\\ApplianceSessionStateIsExternal": "false",
  "HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\w32tm.exe\\Debugger": "C:\\RestoreData\\install.exe"
}

The RestoreRegistry function in Pangaea.Maintenance.BackupRestore/Utilities/RestoreUtility.cs blindly applies the registry keys in this file, enabling an attacker to execute arbitrary code by abusing well-known registry-based persistence techniques such as Image File Execution Options (IFEO).

In addition to registry manipulation, other vectors within the backup files (such as modifying .sql scripts) may also allow for exploitation.

A demo video showing exploitation of the RCE issue to execute a Havoc C2 beacon can be seen below:

Mitigation Advice

We reported the RCE issue to the vendor, whilst also pointing out that the backup encryption issue seems to be somewhat documented.

The vendor acknowledged that the identified issue is a known issue in the Safeguard virtual appliance. They advised that their upcoming version 8.0 release will include improvements in both documentation and the product itself to emphasise the recommendation of using a password or GPG key on backups for additional protection. The vendor also pointed out that while the virtual appliance offers convenience and flexibility, it lacks the hardware security layer present in their physical appliances, which are not affected by this vulnerability and provide a higher level of security.

You May Also Like