Last year, the NetSPI red team came across a backup file for Solar Winds’ Web Help Desk software. This led to an analysis of the software and how it stored encrypted passwords, giving the red team the ability to recover the stored passwords and use them to access other systems. The root cause is that the encryption keys used to protect the data are too predictable, being either entirely static in one case, or taken from a greatly restricted keyspace.   

We reached out to Solar Winds to let them know of this issue as they were clearly trying to protect this data, and they have now issued a patch. We recommend that users of this software upgrade to the latest version, but also that access to these backup files is appropriately restricted to only those who need to access them. 

TL;DR – an attacker with access to a Web Help Desk backup file may be able to recover some of the encrypted passwords stored within it. 

Fixed in: Solar Winds Web Help Desk version 12.8.5
CVE: https://nvd.nist.gov/vuln/detail/CVE-2024-28989

We developed some PoC code for exploiting this issue, but will leave that up to the reader to replicate.

Description 

An attacker with access to the compiled application code for Web Help Desk, and a Web Help Desk database backup file can decrypt secrets stored as “{AES}<base64 string>”, which is encrypted using AES-GCM. Because AES-GCM is an authenticated cipher (AEAD), the attacker can be sure that the decrypt is genuine once they have successfully generated it. As it’s written in Java, decompilation is a matter of loading the various JAR files that make up the application into something like jd-gui, and then the original source code can be seen, for example:

public static AESGCMSymetricCryptoProvider createDefaultAESGCMNonFIPSSymetricCryptoProvider(String secretKey) { 
    Cipher encryptCipher = null; 
    Cipher decryptCipher = null; 
    MessagePadding padding = new MessagePadding(); 
    String localSecretKey = null; 
    if (secretKey == null || secretKey.length() == 0) { 
      localSecretKey = defaultKey;  // taken from crypt.defaultKey 
    } else { 
      localSecretKey = secretKey + secretKey; 
    } 

Encryption is used for storing passwords within the database and a configuration file to protect the data. One instance is the randomised password of the PostrgreSQL database created at install time. Another is a password created for querying AD/LDAP, or for sending SMTP emails, if either of these are configured. This is much better than storing these secrets as plaintext, however the keys being used for the encryption are too predictable and make it possible to decrypt some of the encrypted secrets. 

Web Help Desk performed AES-GCM encryption with what appears to be only a handful of key/nonce pairs, with one of the keys appearing in the source code named as “default key”, and another which can be derived from the database and reverse engineering the source code. Since the application is written in Java, reverse engineering is a relatively straightforward task. 

We recommend that all users of Web Help Desk upgrade their software – but there is plenty of other sensitive data that could be found in a helpdesk backup file, for example, password hashes of local users, and hence plenty of other reasons to secure these backup files to only those with a need to access them.  

Thank you to Solar Winds to being responsive to the vulnerability submission and working to resolve it! 

Background 

The class com.solarwinds.whd.symmetric.AESGCMSymetricCryptoProvider deals with creating an instance via createDefaultAESGCMNonFIPSSymetricCryptoProvider(null), which uses a default key found in a properties file ‘cryptconfig.properties’:

    defaultKey = configProps.getProperty("crypt.defaultKey"); 
    IV = configProps.getProperty("crypt.salt").getBytes(StandardCharsets.UTF_8);

The default keys can be found in whd-crypto.jar, by using a decompilation tool such as jd-gui, or by simply unzipping the jar file and looking for the file ‘cryptconfig.properties’:

The default encryption instance used this key with mode “AES/GCM/NoPadding”. 

The problems are that keys can be relatively easily recovered, and that AES-GCM is known to fail catastrophically when the same key and IV is re-used for different messages, as the attacker can recover the keystream if they know the plaintext and ciphertext of one of the messages. This gives us two separate attacks in total. 

In the first case, we already know the key, so we don’t need to perform the more complex attack, however we need to bear it in mind for later.  

Case 1a: Default key – database connection password: 

The password for the embedded database is stored in this form in .whd.properties within the install directory:

whd.db.password={AES}bzAwvd4[REDACTED] 

Running our decryption code, using the default key from the JAR file, against the above value shows the original database password given at install time: 

C:\> java -cp whd-crypto.jar;commons-codec-1.9.jar;. descramble 
57zG[REDACTED] 

Case 1b: Transformed Keys  

Elsewhere, the code uses the concept of Transformers to make the encryption/decryption different to the above, but it uses a key that is derived from data present in the database and the source code, and unfortunately the key space seemed to be very small in the version we tested.  

The relevant code – taken from JD-GUI decompiler – is shown below:

classes.com.solarwinds.whd.service.impl. SecretKeyServiceImpl 

/*    */   public String getSecretKey() { 
/* 30 */     int subscriberId = 0; 
/*    */     try { 
/* 32 */       subscriberId = this.techService.getCurrentUser().getSubscriberId().intValue(); 
/*    */     } 
/* 34 */     catch (LogicException e) { 
/* 35 */       return null; 
/*    */     }  
/* 37 */     Preference preference = this.preferenceRepository.findBySubscriberIdFetchNone(subscriberId); 
/* 38 */     if (preference == null) { 
/* 39 */       return null; 
/*    */     } 
/* 41 */     int initCode = preference.getInitCode().intValue(); 
/* 42 */     String secretKey = String.valueOf(valueForInitCode(Integer.valueOf(initCode))); 
/* 43 */     return secretKey; 
/*    */   } 
/*    */    
/*    */   private int valueForInitCode(Number initCode) { 
/* 47 */     return (initCode == null) ? 0 : (initCode.intValue() ^ 0xBABEFACE); 
/*    */   } 

In our case one value is SELECT init_code FROM public.preferences; which is 0xBABEB73C in hex on our instance. The other value is hard-coded as 0XBABEFACE. 

The calculation of the transformed key simplifies in our case to: 

String key = String.valueOf(Integer.valueOf(0xBABEB73C ^ 0xBABEFACE));  

This allows us to decrypt public.discovery_connection.password from our database for example. However, you will have noticed that 0xBABEB73C ^ 0xBABEFACE is a very small offset into the total key space, being less than 0x10000, and indeed the key can also be brute-forced trivially by iterating from 0 upwards. This example uses data from another test installation: 

$ java -cp whd-crypto.jar:commons-codec-1.9.jar:. descramble frcLMeS3nchpg_Ucxz-evzlfNUlfHLnpvKyjAYisVLmlEtyZ2ZFRRiVw7Kd5KQbR 0                                                                    

Cracking frcLMeS3nchpg_Ucxz-evzlfNUlfHLnpvKyjAYisVLmlEtyZ2ZFRRiVw7Kd5KQbR 
Start at 0 
not default key - bruteforcing... 

Key         :19950 
Decrypt     :Password1 
Hex decrypt : 50  61  73  73  77  6F  72  64  31 

Case 2: Problems of Re-using Key and Nonce 

AES-GCM must use separate nonces for separate encryptions, as otherwise an attacker who can determine a known plaintext/ciphertext pair can reconstruct the keystream, up to the length of the known plaintext. See https://frereit.de/aes_gcm/ for full details of the keystream recovery attack. It looks incredibly complicated, but the implementation of the attack is actually simple. 

We developed the following program ‘xor.py’ which will compute the keystream by XORing a known plaintext with its corresponding ciphertext. This keystream can then be used to decrypt an unknown ciphertext. 

When run, it reconstructs the keystream from the known data, and uses this with XOR again to compute the plaintext corresponding to an unknown ciphertext. 

The known ciphertext is frcLMeS3nchpg_Ucxz-evzlfNUlfHLnpvKyjAYisVLmlEtyZ2ZFRRiVw7Kd5KQbR corresponding to the known plaintext \x00\x00\x00\x09Password1, as the length is stored in the first 4 bytes (9 characters).

XORing these two gives a keystream of 126 ,183 ,11 ,56 ,180 ,214 ,238 ,187 ,30 ,236 ,135 ,120 ,246 

XORing an unknown ciphertext with this keystream gives us the plaintext corresponding to the unknown ciphertext. 

$ python3 xor.py 
xor known plain and known cipher to get keystream: 
126 ,183 ,11 ,56 ,180 ,214 ,238 ,187 ,30 ,236 ,135 ,120 ,246 , 
decrypt unknown cipher with keystream:  password

Utility of the Findings 

In the case of the red team exercise we were engaged in, we were able to recover an oauth2_client_secret value from the database and use this to obtain an Azure Access Token (via Connect-AzAccount) and proceed to access cloud resources. We also recovered a password for the account used to make LDAP queries for inventory discovery. 

Recommendation 

Web Help Desk Backups should be appropriately protected, as they can contain a wealth of data useful to an attacker, quite apart from the encryption issues described above, however backups from versions prior to 12.8.5 should definitely be treated as if a competent attacker can decrypt the encrypted fields within the database and reveal passwords for other systems that have been entered. 

More generally, while AES-GCM is a good cipher in general, it should never be used so that a nonce/IV is re-used between messages. Other ciphers are more resistant to nonce re-use if that is a concern.

Interested in learning more about NetSPI Red Team Operations? Learn how red team exercises enhance your team’s safeguards against threats.

Lastly, thank you to Phil Wilson-Smith-Kopp, Principal Security Consultant for your assistance.