ColdFusion CFMX_COMPAT lolcryption
What is CFMX_COMPAT?
CFMX_COMPAT is an old encryption algorithm available in ColdFusion Web applications framework.
In 2018, it is still exposed by the latest ColdFusion API functions Encrypt, EncryptBinary and Hash.
Since ColdFusion MX 7 (2005), all these functions accept an optional argument algorithm which allows developers to use stronger (or not) encryption algorithms. The default value is CFMX_COMPAT for encryption or MD5 for hashing.
Concerning the encryption key used with CFMX_COMPAT, the documentation says:
For the CFMX_COMPAT algorithm, any combination of any number of characters; used as a seed used to generate a 32-bit encryption key.
Several open-source implementations of the algorithm are available online: ruby, Java, nodejs and C#.
The 32-bit encryption key
So let's see how the secret key is mixed to generate the tiny 32 bits encryption key.
The code first stretches the seed to 12 bytes with some fixed data:
private void setKey(String key) {
int i;
m_Key = key;
String localKey = key;
if(isEmpty(localKey)) localKey = "Default Seed";
char[] Seed = new char[localKey.length() >= 12 ? localKey.length() : 12];
m_Key.getChars(0, m_Key.length(), Seed, 0);
int originalLength = m_Key.length();
for(i = 0; originalLength + i < 12; i++)
Seed[originalLength + i] = Seed[i];
// to be continued
The code then set up the encryption state (called LFSR A/B/C):
for(i = 0; i < 4; i++) {
m_LFSR_A = (m_LFSR_A <<= 8) | Seed[i + 4];
m_LFSR_B = (m_LFSR_B <<= 8) | Seed[i + 4];
m_LFSR_C = (m_LFSR_C <<= 8) | Seed[i + 4];
}
if(0 == m_LFSR_A)m_LFSR_A = 0x13579bdf;
if(0 == m_LFSR_B)m_LFSR_B = 0x2468ace0;
if(0 == m_LFSR_C)m_LFSR_C = 0xfdb97531;
}
No need to be called Bruce Schneier here to understand the big problem. Only 4 bytes of the secret are used. They are not even mixed, scrambled or anything. This key is copied into Seed
and then padded with itself.
So that means the following 3 encryption keys will generate the same cryptographic states:
- @BcDefghI-2018
- aaaaefgh
- ____efgh
Breaking military-level encryption
According to the code of the algorithm, it looks like the developers tried to implement a secure proprietary algorithm that mixes LFSR B with C based on the output of LFSR A.
The reality is different but the most important thing for us is that it is a very fast algorithm.
While there are probably smarter ways to solve this problem we decided to just bruteforce the 32-bit key using our beowulf cluster.
The only requirement to recover the plain-text and the partial secret password at the same time is to implement a clear-text filter to skip invalid candidate keys.
In our context, we wanted to forge arbitrary claims in encrypted tokens. Before that, we needed to decrypt a valid one. We thought that tokens were likely to be JSON dictionaries using only ASCII characters:
for (int i=0; i<M; ++i) {
key[4] = charset[i];
for (int j=0; j<M; ++j) {
key[5] = charset[j];
for (int k=0; k<M; ++k) {
key[6] = charset[k];
for (int l=0; l<M; ++l) {
key[7] = charset[l];
String K = new String(key);
Bla b = new Bla();
byte[] out = b.transformString(K, in);
if (out[0] == (byte)123 && out[out.length-1] == (byte)125 && is_ascii(out)) {
String found = new String(out, "UTF-8");
System.out.println("Found " + K + " // " + found);
i = j = k = l = M;
break;
}
}
}
}
}
Conclusion
Well... CFMX_COMPAT should not be used. The argument algorithm of ColdFusion security functions should always be specified if you don't want to end up using a weak algorithm (which also includes MD5).
To be honest with you, the author started reading the key scheduling code after starting a PoC to recover the LFSR internal states :]
So the real conclusion is: don't start trying to revert cipher rounds before looking at the key scheduling if you don't like loosing time...
Fun fact, the 2 Synacktiv security experts who initially faced the encryption algorithm attacked it in black box without knowing it was CFMX_COMPAT. They still managed to bit-flip the cipher to meet their goal.
Sad fact, after compromising the server, we realized our customer was using a strong 20+ mixed-charset password as an encryption key. This nice entropy has been lost without any respect thanks to an optional argument.