TL; DR: If you use AES-CBC (or another block cipher operating in CBC mode) to decrypt user-controlled ciphertext, validate the ciphertext with an HMAC or similar integrity check prior to decryption to avoid Padding Oracle vulnerabilities. All user-controlled input is untrusted and can be dangerous, even if it is encrypted data.
Introduction
In my last post, I closed by saying that AES in CBC (Cipher Block Chaining) mode is the best native option for symmetric block encryption in ColdFusion -- but added that it can lead to vulnerabilities if not implemented correctly. In this post we're going to look at how you should (and shouldn't) implement AES and other block ciphers if you want to avoid Padding Oracle Attacks. And although we're focused on ColdFusion, the general concepts apply to any application language.
Let's consider the code snippet below. This code is materially similar to many AES-CBC implementations in ColdFusion that I've seen in real applications, shared libraries, example code, technical documentation, and other sources that are likely to be in common, widespread use. Can you spot why it's vulnerable?
<cfscript>
decryptedVal = decrypt(COOKIE.AUTH_USER, mySecretKey, "AES/CBC/PKCS5Padding", "HEX");
</cfscript>
This code decrypts a cookie named "AUTH_USER" (which is expected to be a hex-encoded string) with AES-CBC, using "mySecretKey". But since we're not performing any integrity checking (such as a signature or HMAC) on the ciphertext to be decrypted that the user passes in the cookie, an application that implements AES-CBC in this way is likely vulnerable to a Padding Oracle Attack.
AES-CBC Refresher
Let's start with a quick refresher of how AES-CBC works. There are lots of great books, articles, and other sources that that cover it much greater detail but we'll hit the relevant parts. As shown below, AES-CBC encryption splits a message into fixed-length blocks (specifically 16-bytes blocks, for AES-CBC) and encrypts each block independently. Prior to encrypting a block, the plaintext to be encrypted is XORed with the previous block of encrypted ciphertext (or the IV, in the case of the first block of plaintext). During this process, we're creating a "chain" based on input from previous encrypted blocks -- hence the name.
AES-CBC decryption works the same way, but in reverse. After passing an encrypted block though our decryption cipher, that intermediate-state value is XORed with the previous block of ciphertext to get a block of our original plaintext message:
Message Padding
Before we cover Padding Oracle Attacks, let's talk about message padding. Message padding lets us encrypt messages of any size when using a block cipher. Block ciphers need the input to be multiple of the block size (e.g., 16 bytes for AES), but padding takes care of this for the user. There are a handful of common padding standards, but in PKCS#5/PKCS#7 (the padding standards that we'll be using), the values of the padded bytes is equal to the number of padded bytes we need. For example, we need need seven bytes of padding for the message below, so our padded plaintext would look like:
During decryption, the message padding gets checked. A typical padding check will work similar to:
- Read the final byte of decrypted plaintext, N
- If N > blocksize: PADDING ERROR
- Loop through N - 1 bytes ; if any don’t contain N : PADDING ERROR
- Remove last N bytes from the message
- No Errors = Decryption Successful! (*could still be garbage data or cause errors later)
So Swordfish[0x07][0x07][0x07][0x07][0x07][0x07][0x07] becomes Swordfish after the padding check is complete.
But if there is any type of padding error, the application with throw an exception. Here we see what a verbose padding error looks like in Adobe ColdFusion:
And the differentiation between these two end-states (padding error vs. no padding error) is what we're going to use to carry our our Padding Oracle Attack.
The Padding Oracle Attack
[ Note: to avoid any confusion, this attack has nothing to do with a certain giant database company. The "oracle" here refers to being able to divine secret, unknown information. Think the Oracle at Delphi, not the Oracle at Redwood Shores, CA. ☺ ]
If you use a block cipher in CBC mode and don't perform an integrity check on user-controlled ciphertext prior to decryption, your application is almost certainly vulnerable to a padding oracle attack. Our example today focuses on ColdFusion and AES, but any language and any block cipher operating in CBC mode can be vulnerable to the same type of flaw. Padding Oracle Attacks have been a known vulnerability class for over a decade, and some high-profile examples have included the
POODLE attack and a
framework-wide padding oracle in ASP.NET (MS10-070).
It's also worth highlighting that AES-CBC is cryptographically sound from an algorithm perspective. We're not using complex math, cracking keys, or attacking the actual encryption algorithm. Instead, we're leveraging flaws in the software implementation of AES-CBC that let us perform a side-channel attack based on application behavior and feedback -- essentially letting us circumvent encryption due to our ability to make valuable inferences. And while we do need to be able to distinguish between "padding error" vs. "no padding error", we do not need verbose error messages. Any detectable, consistent differences in these end states (such as distinct generic errors) will let us perform our attack.
So let's now walk through what an attack looks like and recall how AES-CBC decryption works. The image on the left shows the process of decrypting one block of ciphertext (C2) into one block of plaintext (P2). Note that "decryption" with our cipher and key first gets us to our intermediate state (I2), and we don't actually obtain our plaintext until we XOR I2 with C1 (our previous block of ciphertext). Also note that these XOR transformations work in either direction, from top down or bottom up.
The image below shows the logical flow of our Padding Oracle Attack. The left side shows how a single block gets decrypted. On the right - because of how padding standards and padding checks work, we can assume there's a chosen ciphertext block C'1 that will result in a new plaintext P'2 block that ends in 0x01 (one byte of padding) when XORed with I2. Recall that since there's no integrity checking on the inputted ciphertext, we can modify the ciphertext bytes in any encrypted values and make all of the guesses that we need. So now all we need to do is make a maximum of 256 guesses for the last byte of our chosen C'1 (referred to as C'1[16]) where P'2[16] is 0x01 (one byte of padding). We should get "padding errors" from our application for all values except for one -- and that's the value that we want. We can then solve for the last byte of the intermediate value I2[16], and then use and then use this value to decrypt one byte of the real plaintext, P2[16]. Next we can solve for a case where our chosen block of ciphertext results in a plaintext that ends with two bytes of padding, 0x02 0x02 -- which will give us another byte of I2. From here, we can continue byte by byte, modifying values, making guesses, observing padding behavior, and decrypt our entire ciphertext value.
And if decrypting ciphertext with no knowledge of the key isn't scary enough, be aware that Padding Oracle Attacks can be used to create entirely new blocks of ciphertext from fully-chosen plaintext too!
A Sample Vulnerable Application
Let's consider a vulnerable application that consumes an encrypted value from a URL parameter -- much like our original COOKIE.AUTH_USER example but with a different source of user-controlled data. Assuming decryption works, the application will display a username and a user role, obtained from our decrypted token. Maybe a little contrived, but very similar to how some actual user authentication schemes may work.
And since the application isn't validating the ciphertext prior to decryption, we can carry out our Padding Oracle Attack, as shown in the video below. Our automated attack tool makes short work of decrypting the encrypted token -- with no knowledge of the key or any internal application details. Watch ciphertext change to plaintext, byte by byte and block by block.
Included below is some sample vulnerable ColdFusion code you can play with, if you want to reproduce and walk through a Padding Oracle Attack on your own.
Needless to say, this is vulnerable code that should not be used or deployed anywhere other than a local, offline test environment. Usage of the sample code should be pretty simple -- if you pass in a "secret" URL parameter, the application will try to decrypt it. Without this parameter, it will output some encrypted ciphertext (that you can then pass in to decrypt):
<cfscript>
encryptionKey = "JAidBZLaYf37huVuM4MNTA=="; //our AES key
// if there's a "secret" URL parameter, we'll attempt to decrypt it
// vulnerable to POA because there's no integrity check of the ciphertext
if (isDefined("url.secret")) {
decryptedInput = decrypt( url.secret, encryptionKey, "AES/CBC/PKCS5Padding", "hex" );
writeOutput( "Decrypted Stuff: #decryptedInput# <br>" );
}
// if we don't have a "secret" URL parameter, just output some ciphertext
// you can then use this as a value to decrypt, passed in url.secret
else {
input = "Here is the super secret stuff we want to encrypt.";
encryptedInput = encrypt( input, encryptionKey, "AES/CBC/PKCS5Padding", "hex" );
writeOutput( "Encrypted Stuff: #encryptedInput# <br>" );
}
</cfscript>
This code is vulnerable since no integrity checking is done prior to decryption. Once you get the vulnerable code up and running, observe that if you change one hex value in a valid "secret" URL parameter with another valid hex value, you should get a padding error. Other ciphertext value modifications may result in other encryption errors or application errors. Then see if you can manually walk through the Padding Oracle Attack operations to decrypt one byte of ciphertext. (Keep in mind that the last byte of the decrypted plaintext will probably be a byte of padding.)
Building a fully-working Padding Oracle Attack script is an exercise left for the reader. There are many tools, tutorials, and resources to help you build a fully-working exploit. I'm partial to
Bletchly, which is a great choice if you're comfortable with Pyhton3. It does not help you
find Padding Oracle vulnerabilities, but it is very handy to automate the steps for data decryption and modification, once you've found a vulnerable application component.
This post on Padding Oracle Attacks from NCC Group may be helpful too. If you get stuck on the exploitation part, feel free to drop me a line or leave a comment.
Prevention and Detection of Padding Oracle Attacks
So how do we make our AES-CBC implementation more secure? We could potentially use an entirely different encryption algorithm, specifically something other than a block cipher in CBC mode. But let's assume we can't make that change, and need to use AES. Then the best thing to do first is to add some integrity checking to our decryption process, using something like an HMAC or signature for the ciphertext. This signature must be checked prior to decryption, and if it fails, decryption should not even be attempted. This would detect if the ciphertext has been modified, such as during all of our guesses needed for a Padding Oracle Attack.
Our new code adds a key used for signing, generates an HMAC for the ciphertext, and checks the HMAC prior to attempting to decrypt the ciphertext. There's certainly lots of room for improvements in this code -- such as moving the actions to functions and adding better error handling -- but it's only intended as a basic guide on how to quickly add integrity checking (an HMAC) prior to decryption.
<cfscript>
encryptionKey = "JAidBZLaYf37huVuM4MNTA=="; //our AES key
signingKey = "pickSomethingBetter"; //let's use a different key for signing
// A valid url.secret should now contain <CIPHERTEXT>-<HMAC>
// If there's a "secret" URL parameter, we'll attempt to decrypt it - ONLY IF the HMAC is valid
if (isDefined("url.secret")) {
// Does the "secret" contain two parts - presumably the ciphertext and an HHAMC?
secretParts = listToArray(url.secret,"-");
if (len(secretParts) != 2) {
writeOutput("nope");
cfabort();
}
// Is the HMAC valid?
if (hmac(secretParts[1], signingKey, "HMACSHA256") != secretParts[2]) {
writeOutput("nope");
cfabort();
}
decryptedInput = decrypt( secretParts[1], encryptionKey, "AES/CBC/PKCS5Padding", "hex" );
writeOutput( "<b>Decrypted Stuff:</b><br> #decryptedInput# <br>" );
}
// if we don't have a "secret" URL parameter, just output valid ciphertext with an HMAC
// you can then use this as a value to validate and decrypt, passed in url.secret
else {
input = "Here is the super secret stuff we want to encrypt.";
encryptedInput = encrypt( input, encryptionKey, "AES/CBC/PKCS5Padding", "hex" );
hmac = hmac(encryptedInput, signingKey, "HMACSHA256");
writeOutput( "<b>Encrypted Stuff:</b><br> #encryptedInput#-#hmac# <br>" );
}
</cfscript>
It's also worth noting that exploiting Padding Oracle Attacks can be noisy, since an attacker may need to make up to 256 requests per byte when making guesses. This can result in lots of requests, lots of logs, and lots of errors. Repeated, high-volume padding errors generated in your application could be an indicator of an attempted attack.
Finally - remember that encryption (and even data integrity checking) is just one aspect of security, and one approach to secure portions of an application. Often times, you may not even want to expose encrypted sensitive values to the user at all, an instead may want to pass some type of reference value, to be used to fetch (and keep) sensitive data entirely on the backend.
Closing Thoughts
The Padding Oracle Attack is just one more example of the risk of any user-controlled input. Developers may already be wary of user input that flows into SQL queries to avoid SQL injection, or rendered content to avoid Cross Site Scripting -- but all user-controlled input should be validated, even encrypted data. Encryption on its own may prevent a malicious users from reading data or intelligibly modifying data -- but it provides no native protection against blind, random, or automated modification of ciphertext. Validate the integrity of all user-controlled ciphertext with a signature, HMAC, or similar mechanism. If that validation fails, don't even attempt decryption.
As always, never trust user-controlled input.
wow - interesting post. thank you for sharing your knowledge
ReplyDeleteGood article. Thank you for writing. Scary what they can do with AES which is said to be the strongest. Lol I guess hackers will break anything sometime. That one was a new one for me and I spent the weekend fixing one of our application to add hmac checks. But I am wondering if we already have robust errors disabled in Lucee can we still be hacked before? We use a customized template based on the public one.
ReplyDeleteThanks for the comment! To answer your question, it is still possible that this could be exploited against an application that was using the generic "an error occurred" Lucee error template. There will be some nuance and dependence on how the application actually works, but you just need to be able to differentiate between when a padding error occurs and decryption fails vs. when a padding error doesn't occur and decryption succeeds (but could result in some other error). For example, let's say that your decrypted token contains a userid. There may be three distinct end cases -- 1) padding error, 2) successful decryption (no padding error) but the plaintext is an invalid userid leading to a different error condition, and 3) successful decryption where the plaintext is a valid userid. You'd need to be able to distinguish between #1 vs. #2 and #3 for a successful padding oracle attack, but even without verbose error messages, there will often be even minor differences in HTML responses for example, that you can then instrument against in your attack.
DeleteReally great write-up! As someone who never feels 100% confident in how I go about security, understanding these pit-falls is so important. I almost certainly have played around with having an encrypted cookie in the past - so seeing that as the basis for your first example is a real punch in the gut :D
ReplyDelete