Tuesday, October 30, 2012

OvertheWire - Natas Wargame Level 11 Writeup

Level 11

Using the credentials obtained from the previous post, we can log in to Level 11 where we are presented with the following screen:


We can immediately see that this challenge will have to do something with cookies, since the description mentions that 'Cookies are protected with XOR encryption.' Let's take a look at the cookies we do have:


We can see that we have a cookie called 'data', which appears to be base64 encoded. Before going any further, let's take a look at the source to see what exactly we're dealing with:

 <html>  
 <head><link rel="stylesheet" type="text/css" href="http://www.overthewire.org/wargames/natas/level.css"></head>  
 <?  
 $defaultdata = array( "showpassword"=>"no", "bgcolor"=>"#ffffff");  
 function xor_encrypt($in) {  
   $key = '<censored>';  
   $text = $in;  
   $outText = '';  
   // Iterate through each character  
   for($i=0;$i<strlen($text);$i++) {  
   $outText .= $text[$i] ^ $key[$i % strlen($key)];  
   }  
   return $outText;  
 }  
 function loadData($def) {  
   global $_COOKIE;  
   $mydata = $def;  
   if(array_key_exists("data", $_COOKIE)) {  
   $tempdata = json_decode(xor_encrypt(base64_decode($_COOKIE["data"])), true);  
   if(is_array($tempdata) && array_key_exists("showpassword", $tempdata) && array_key_exists("bgcolor", $tempdata)) {  
     if (preg_match('/^#(?:[a-f\d]{6})$/i', $tempdata['bgcolor'])) {  
     $mydata['showpassword'] = $tempdata['showpassword'];  
     $mydata['bgcolor'] = $tempdata['bgcolor'];  
     }  
   }  
   }  
   return $mydata;  
 }  
 function saveData($d) {  
   setcookie("data", base64_encode(xor_encrypt(json_encode($d))));  
 }  
 $data = loadData($defaultdata);  
 if(array_key_exists("bgcolor",$_REQUEST)) {  
   if (preg_match('/^#(?:[a-f\d]{6})$/i', $_REQUEST['bgcolor'])) {  
     $data['bgcolor'] = $_REQUEST['bgcolor'];  
   }  
 }  
 saveData($data);  
 ?>  
 <h1>natas11</h1>  
 <div id="content">  
 <body style="background: <?=$data['bgcolor']?>;">  
 Cookies are protected with XOR encryption<br/><br/>  
 <?  
 if($data["showpassword"] == "yes") {  
   print "The password for natas12 is <censored><br>";  
 }  
 ?>  
 <form>  
 Background color: <input name=bgcolor value="<?=$data['bgcolor']?>">  
 <input type=submit value="Set color">  
 </form>  
 <div id="viewsource"><a href="index-source.html">View sourcecode</a></div>  
 </div>  
 </body>  
 </html>  

Let's walkthrough the PHP code and see what it does:

  • We first define an array with the following values
    • showpassword - no
    • bgcolor - #ffffff
  • Then, we create an 'xorencrypt' function, which takes a string as input and performs an XOR operation with a censored key.
  • We then create a 'loadData' function which loads the default array into $mydata, checks to see if there is data in our cookie, and if so attempts to base64_decode it, perform the XOR operation on it, and decode the JSON into an array.
    • If this is indeed an array, and the proper values are found, the values of mydata are updated with the values from the cookie.
    • If not, the default values are kept
  • The value of $mydata is returned.
  • We then create a 'saveData' function which is used to set a cookie to data which is JSON encoded, XOR'd, and then base64 encoded. 
  • The execution of the PHP first calls loadData to process any cookie data.
  • Then the background color request value is handled
  • Finally, whatever data has been loaded is set as our cookie value.
We can see at the end of the source that if $data['showpassword'] is set to 'yes', then the password will be displayed. Therefore, this is our goal.

Let's stop and think about exactly what we need to do, and what we have to do it. We want to obtain the password for natas12, which is only displayed if the 'showpassword' key of $data is set to 'yes'. This would only occur if during the loadData function our cookie contained the encoded, encrypted version of the array. However, to get the encrypted version of any input, we first need the key so that when it is unencrypted by the 'loadData' function, it will be usable.

We are given the following pieces of data:
  • The default array used to generate the default cookie value
  • The default cookie value
  • The functions used to encrypt and encode our data
The key to this challenge is to realize that we don't need to brute force the key. The property of XOR encryption that will drastically help us in this challenge is that while:

Original_Data XOR KEY = Encrypted_Data

The following is also true:

Original_Data XOR Encrypted_Data = KEY

This helps us because we can see above that we are given the default original data, as well as the default encrypted data. Let's use the following PHP code (recycling some of their functions) to generate our key:

 <?  
 $orig_cookie = base64_decode('ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw');  
 function xor_encrypt($in) {  
   $text = $in;  
   $key = json_encode(array( "showpassword"=>"no", "bgcolor"=>"#ffffff"));  
   $outText = '';  
   // Iterate through each character  
   for($i=0;$i<strlen($text);$i++) {  
   $outText .= $text[$i] ^ $key[$i % strlen($key)];  
   }  
   return $outText;  
 }  
 print xor_encrypt($orig_cookie);
 ?>  

In this example, we are using the base64_decoded value of our cookie as our Encrypted_Data and we are using the json_encoded default array as our Original_Data. We know these are the two values to use because these are the values sent to the XOR function in the loadData and saveData functions.

Running this code gives us the following:

 root@bt:~/natas# php5 level11.php  
 qw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jq  

We can see that this key is the value 'qw8j' repeated many times. Therefore, we can deduce that 'qw8j' is our key, which we can use on any input to generate the encrypted output.

Let's adjust our PHP code to XOR the json_encode(d) new array, which sets the 'showpassword' value to 'yes':

 function xor_encrypt() {  
   $text = json_encode(array( "showpassword"=>"yes", "bgcolor"=>"#ffffff"));  
   $key = "qw8J";    
   $outText = '';  
   // Iterate through each character  
   for($i=0;$i<strlen($text);$i++) {  
   $outText .= $text[$i] ^ $key[$i % strlen($key)];  
   }  
   return $outText;  
 }  
 print base64_encode(xor_encrypt());  

We now use the json_encoded representation of our adjusted array to be XOR'd with our derived key. Executing the code gives us the following:

 root@bt:~/natas# php5 level11.php  
 ClVLIh4ASCsCBE8lAxMacFMOXTlTWxooFhRXJh4FGnBTVF4sFxFeLFMK  

Let's use this value as our cookie and see what happens:


As we hoped, the value was decoded, unencrypted, then json_decoded successfully, and the password is returned. We can use these credentials to log in to the next level.

I want to give a 'Thank you!' to Reddit user NearOrFar. I had derived the correct key, but did not recognize that it was 4 repeating characters. When using the long key to obtain the new cookie, since the key did not end on a 'complete cycle' of the 4 character key, the resulting array was skewed, and was not being encrypted correctly. NearOrFar was able to help diagnose this problem, and after making the truncation to 4 characters, the issue was solved.

More writeups to come.

-Jordan

3 comments:

  1. I made a similar mistake to you regarding the length of the key. However, where you reduced the keylength to the actual 4 characters, I just (rather more messily) extended the key by one character in order to fit the length of the new message to be encrypted.

    By far the most interesting of the challenges so far. On to 12!

    ReplyDelete
  2. Interesting, I used the same process and the encrypted cookie data I got returned by the php script was "ClVLAh4ASAsCBE8FAxMaUFMOXRlTWxoIFhRXBh4FGlBTVF4MFxFeDFMK" (slightly different, and thus false). Any ideas as to what I did wrong?

    ReplyDelete
    Replies
    1. Just figured it out, put a lowercase 'j' in the key rather than a "J". Silly me!

      Delete