Friday, 10 November 2017

CTF Writeup - Flare-On 2017 - 10: shell.php


  • Name - shell.php
  • Category - Reverse Engineering
  • Points - 1
  • Binary - Download here

The next challenge on the list was a PHP one. This is by far the one I liked the least as it requires a lot of manual work and guessing. The following shows the contents of shell.php:

<?php
    $o__o_ = base64_decode('QAYLHhIJ ... [snip] ... kCEh8NSh4PEA4eGFsaGXoo');
    $o_o = isset($_POST['o_o']) ? $_POST['o_o'] : "";
    $o_o = md5($o_o) . substr(MD5(strrev($o_o)) , 0, strlen($o_o));
    
    for ($o___o = 0; $o___o < 2268; $o___o++)
    {
        $o__o_[$o___o] = chr((ord($o__o_[$o___o]) ^ ord($o_o[$o___o])) % 256);
        $o_o.= $o__o_[$o___o];
    }
    
    if (MD5($o__o_) == '43a141570e0c926e0e3673216a4dd73d')
    {
        if (isset($_POST['o_o'])) @setcookie('o_o', $_POST['o_o']);
        $o___o = create_function('', $o__o_);
        unset($o_o, $o__o_);
        $o___o();
    }
    else
    {
        echo '<form method="post" action="shell.php"><input type="text" name="o_o" value=""/><input type="submit" value=">"/></form>';
    }
?>

The script is easy enough to understand. It basically does the following:
  • Base64 decodes a massive string (line 2)
  • Accepts a password via a POST request (line 3)
  • Concatenates it's MD5 and the substring of the MD5 of the reverse string (line 4)
  • Uses the result as the key to XOR the massive string (lines 6 - 10)
  • Creates a new function from the decrypted text (line 15)
  • Calls the function (line 17)
The aim here is to find the key and decrypt the Base64 string. There are a few constraints and possibilities here that we need to note 1) the XOR key is made up of only [0-9][a-f] characters due to it being the result of an MD5, 2) the key is of length between 33 and 64, and 3) the decrypted string has to be PHP code, i.e. made out of printable characters.

Expanding on constraint #3, notice that the key is repeated which means that a character of the key is a potential solution if it produces a printable character each time it is used. As an example let us assume that the key length is 64 and that we want to verify if character '7' is a potential candidate to being the first character of the key. Then, '7' XOR'd with the first byte of the encrypted text has to produce a printable character; the result XOR'd with the 65th byte of the encrypted text has to produce a printable character as well, and so on so forth until we exhaust the encrypted text. If it produces printable characters everywhere then '7' is a potential candidate, else not.

The following script will determine all possible characters for key lengths between 33 and 64:

import base64
import string

encoded = "QAYLHhIJ ... [snip] ... kCEh8NSh4PEA4eGFsaGXoo"
decoded = base64.b64decode(encoded)

possible_chars = "0123456789abcdef"
   
for key_len in xrange(33, 65):

    print "-- Key Length: " + str (key_len) + " --"

    for key_pos in xrange(0, key_len):
        
        potential_sol = []
    
        for possible_char in possible_chars:
            temp_possible_char = possible_char
            possible = True
            for pos in range(key_pos, len(decoded), key_len):
                temp_possible_char = chr((ord(decoded[pos]) ^ ord(temp_possible_char)))
                if temp_possible_char not in string.printable:
                    possible = False
                    break 
                    
            if possible:
                potential_sol.append(possible_char)
        
        if potential_sol:
            print key_pos, ":", potential_sol

Running the script we get:

Command Prompt
C:\>python possible_solutions.py -- Key Length: 33 -- -- Key Length: 34 -- -- Key Length: 35 -- -- Key Length: 36 -- -- Key Length: 37 -- -- Key Length: 38 -- -- Key Length: 39 -- -- Key Length: 40 -- -- Key Length: 41 -- -- Key Length: 42 -- 9 : ['4', '6', '7'] -- Key Length: 43 -- -- Key Length: 44 -- -- Key Length: 45 -- -- Key Length: 46 -- -- Key Length: 47 -- -- Key Length: 48 -- -- Key Length: 49 -- -- Key Length: 50 -- -- Key Length: 51 -- 31 : ['e'] -- Key Length: 52 -- -- Key Length: 53 -- -- Key Length: 54 -- -- Key Length: 55 -- 49 : ['c', 'f'] -- Key Length: 56 -- 39 : ['c'] -- Key Length: 57 -- -- Key Length: 58 -- -- Key Length: 59 -- 16 : ['a', 'd', 'f'] -- Key Length: 60 -- -- Key Length: 61 -- -- Key Length: 62 -- -- Key Length: 63 -- -- Key Length: 64 -- 0 : ['c', 'd', 'e'] 1 : ['a', 'b', 'c', 'd', 'e'] 2 : ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] 3 : ['8', '9'] 4 : ['2', '3', '4', '5', '6'] 5 : ['0', '1', '2', '3', '4', '6', '7', '8'] 6 : ['b', 'c', 'd', 'f'] 7 : ['8', '9'] 8 : ['0', '2', '4', '5'] 9 : ['a', 'b', 'f'] 10 : ['0', '2', '3', '4', '5', '6', '7', '8', '9'] 11 : ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] 12 : ['b', 'c', 'd'] 13 : ['8', '9'] 14 : ['0', '2', '3', '5'] 15 : ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] 16 : ['a', 'b', 'c', 'd', 'e'] 17 : ['a', 'b', 'c', 'd', 'e', 'f'] 18 : ['b', 'c', 'd', 'f'] 19 : ['2', '3', '4', '5', '7'] 20 : ['2', '3', '7'] 21 : ['5', '6', '7'] 22 : ['0', '1', '2', '4', '5', '6', '7', '8', '9'] 23 : ['0', '1', '5', '6', '7'] 24 : ['8', '9'] 25 : ['a', 'b', 'c', 'd', 'e', 'f'] 26 : ['b', 'c', 'd', 'e'] 27 : ['8', '9'] 28 : ['d', 'e'] 29 : ['8', '9'] 30 : ['0', '1', '2', '3', '4', '5', '6', '7', '9'] 31 : ['a', 'b', 'c', 'd', 'e', 'f'] 32 : ['0', '1', '2', '3', '5', '6', '7', '8', '9'] 33 : ['0', '1', '2', '3', '4', '5', '6', '7', '9'] 34 : ['0', '1', '3', '6', '7'] 35 : ['b', 'c', 'd', 'e'] 36 : ['d', 'e', 'f'] 37 : ['0', '1', '4', '6', '7'] 38 : ['2', '3', '4', '5'] 39 : ['0', '1', '4', '6', '7'] 40 : ['1', '2', '3', '4', '5', '6', '7', '8', '9'] 41 : ['0', '3', '4', '7', '8', '9'] 42 : ['a', 'e'] 43 : ['a', 'f'] 44 : ['b', 'c'] 45 : ['0', '1', '3', '6', '7'] 46 : ['8', '9'] 47 : ['1', '2', '3', '5', '6', '7', '8', '9'] 48 : ['d', 'e'] 49 : ['0', '1', '7'] 50 : ['0', '1', '5', '6', '7'] 51 : ['0', '1', '2', '3', '4', '5', '6', '7', '9'] 52 : ['a', 'b', 'c', 'f'] 53 : ['a', 'b', 'c', 'd', 'e', 'f'] 54 : ['a', 'b', 'c', 'd', 'f'] 55 : ['a', 'b', 'c', 'd', 'e', 'f'] 56 : ['0', '1', '2', '3', '5', '6', '7', '8', '9'] 57 : ['0', '1', '3', '6', '7'] 58 : ['8', '9'] 59 : ['a', 'e', 'f'] 60 : ['0', '1', '2', '3', '6', '7'] 61 : ['a', 'b', 'c', 'e', 'f'] 62 : ['a', 'e', 'f'] 63 : ['b', 'c', 'd'] C:\>


Here we've hit 2 birds with 1 stone. It is obvious from the script that the the key length is 64 as it has a potential solution for each position of the key. We also have a list of potential solutions that we can use to start decrypting the text.

So how are going to further reduce the potential solutions? Well, it's a semi-manual task; and it's at this point that the challenge becomes a bit mundane. Let's try and find out the first 4 characters of the key with this script:

import base64
import itertools

encoded = "QAYLHhIJ ... [snip] ... kCEh8NSh4PEA4eGFsaGXoo"

decoded = base64.b64decode(encoded)

pos_key = [ ['c', 'd', 'e'], ['a', 'b', 'c', 'd', 'e'], ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], ['8', '9']]  
  
for x in itertools.product(*pos_key):
    key1 = ''.join(x)
    key = key1 + ('_' * (64 - len(key1)))
    decrypted = [None] * 2268

    for x in range(2268):
        decrypted[x] = chr((ord(decoded[x]) ^ ord(key[x])) % 256)
        key = key + decrypted[x]

    print "Key :" + key1 
    print ''.join(decrypted)[:len(key1)] , '|', ''.join(decrypted)[64*10:(64*10)+len(key1)], '|', ''.join(decrypted)[64*31:(64*31)+len(key1)] , '|', ''.join(decrypted)[64*32:(64*32)+len(key1)]
    print

The script covers the first 4 characters of the 64-char key. Notice that on line 8 we put the possible solutions for the first 4 chars obtained from the previous result. For each permutation of these characters (line 10), we decrypt sections of the text (line 16), and print some parts (line 20) which we will later manually verify if they make sense or not.

Running the above script:

Command Prompt
C:\>python stage1_decrypter.py [ ... snip ... ] Key :db68 $d=& | $kex | ost# | e="s Key :db69 $d=' | $key | ost" | e="r Key :db78 $d<& | $kdx | osu# | e=#s [ ... snip ... ] C:\>


Note the string '$key' which indicates that we're on the right track with 'db69'. Using this labour-intensive, nerve-wracking method we finally get 'db6952b84a49b934acb436418ad9d93d237df05769afc796d067bccb379f2cac and a 2nd PHP script as the decrypted text:

$d='';
$key = "";

if (isset($_POST['o_o']))
    $key = $_POST['o_o'];

if (isset($_POST['hint']))
    $d = "www.p01.org";

if (isset($_POST['t'])) {
    if ($_POST['t'] == 'c') {
        $d = base64_decode('SDcGHg1feVUIEhsbDxFhIBIYFQY+VwMWTyAcOhEYAw4VLVBaXRsKADMXTWxrSH4ZS1IiAgA3GxYUQVMvBFdVTysRMQAaQUxZYTlsTg0MECZSGgVcNn9AAwobXgcxHQRBAxMcWwodHV5EfxQfAAYrMlsCQlJBAAAAAAAAAAAAAAAAAFZhf3ldEQY6FBIbGw8RYlAxGEE5PkAOGwoWVHgCQ1BGVBdRCAAGQVQ2Fk4RX0gsVxQbHxdKMU8ABBU9MUADABkCGHdQFQ4TXDEfW0VDCkk0XiNcRjJxaDocSFgdck9CTgpPDx9bIjQKUW1NWwhERnVeSxhEDVs0LBlIR0VlBjtbBV4fcBtIEU8dMVoDACc3ORNPI08SGDZXA1pbSlZzGU5XVV1jGxURHQoEK0x+a11bPVsCC1FufmNdGxUMGGE=');
        
        $key = preg_replace('/(.)../', '$1', $key);
    }

    if ($_POST['t'] == 's') {
        $d = base64_decode('VBArMg1HYn1XGAwaAw1GDCsACwkeDgABUkAcESszBEdifVdNSENPJRkrNwgcGldMHFVfSEgwOjETEE9aRlJoZFMKFzsmQRALSilMEQsXHEUrPg9ZDRAoAwkBHVVIfzkNGAgaBAhUU00AAAAAAAAAAAAAAAAASkZSVV0KDAUCHBFQHA0MFjEVHB0BCgBNTAJVX3hkAkQiFh8ESw0AG0M5MBNRGkpdWV4bVEEVdGJGRR9XGBgcAgpVCDAsCA0GGAVWBAwcBxQqKwRCGxgbVkJFR11IdHcbRFxOUkNNV0RAVXIKSgxCWk1aVkdGQVI8dxRTVl5CR0JLVAQdOStbXkRfXlxOFEULUCp2SFJIUlVGQlUtRhExMQQLJyMmIFgDTUQtYmZIRUAECB4MHhtWRHA9Dh0WSWZmWUEHHBUzYQ==');

        $key = preg_replace('/.(.)./', '$1', $key);
    }

    if ($_POST['t'] == 'w') {

        $d = base64_decode('DycdGg1hYjl8FURaAVZxPhgNOQpdMxVIRwNKc0YDCCsDVn5sJxJMHmJJOgArB1olFA0JHQN+TlcpOgFBKUEAA1M+RVUVDjsWEy8PQUEMV3IsSgJxCFY0IkJAGVY3HV9DbQsRaU1eSxl6IR0SEykOX2gnEAwZGHJHRU0OUn4hFUUADlw8UhRPNwpaJwlZE14Df1IRDi1HS30JFlZAHnRAEQ4tR0p9CRZXQB50LFkHNgNfEgROWkVLZV1bGHVbHyJMSRFZCQtGRU0bQAFpSEtBHxsLVEdaeEEUfCd2akdKYAFaJXBdT3BeHBRFV3IdXCV1PhsUXFUBBR5hXFwwdxsab1kECFoaM0FET2pEd2owBXpAC2ZAS11sMhVmJREWVlFyDV4ldFIdcUMBWlBbcl5CSGFTUCEPW08eEyYNSgJhYjl8Tk9BCUpvDxsAODBeLwUfE08AAAAAAAAAAAAAAAAAEXFkfV1wB0ctDRM=');

        $key = preg_replace('/..(.)/', '$1', $key);
    }

    while(strlen($key) < strlen($d))
        $key = $key.$key;
    $d = $d ^ $key;
}

if (strlen($d))
    echo $d;
else
    echo '<form action="shell.php" method="post"><input type="hidden" name="o_o" value="'.$key.'"><input type="radio" name="t" value="c"> Raytraced Checkboard<br> <input type="radio" name="t" value="s"> p01 256b Starfield<br> <input type="radio" name="t" value="w"> Wolfensteiny<br><input type="submit" value="Show"/></form>';

Alas, another 3 decryption exercises. This time the key is split into 3 parts by taking every other 3rd char, and each one decrypts a different string. At this point I was completely lost as to how I'm going to decrypt these texts or obtain the key so I made a leap of faith which, luckily for me, turned out to be correct. I assumed that the decryption key was the key to the challenge, i.e. the final email which, just like in all the challenges, ends with '@flare-on.com'.

If we make this assumption then the 3 partial keys in question should end with '@a-.m', 'froc' and 'leno'. The plan now is to try each of these partial keys with each of the encrypted text and hopefully get something that makes sense in one of the combinations. The following script tries '@a-.m' against the 3rd ciphertext at each position and prints out the resultant decrypted text if it is printable:

import base64
import string

cb = "DycdGg1hYjl8FURaAVZxPhgNOQpdMxVIRwNKc0YDCCsDVn5sJxJMHmJJOgArB1olFA0JHQN+TlcpOgFBKUEAA1M+RVUVDjsWEy8PQUEMV3IsSgJxCFY0IkJAGVY3HV9DbQsRaU1eSxl6IR0SEykOX2gnEAwZGHJHRU0OUn4hFUUADlw8UhRPNwpaJwlZE14Df1IRDi1HS30JFlZAHnRAEQ4tR0p9CRZXQB50LFkHNgNfEgROWkVLZV1bGHVbHyJMSRFZCQtGRU0bQAFpSEtBHxsLVEdaeEEUfCd2akdKYAFaJXBdT3BeHBRFV3IdXCV1PhsUXFUBBR5hXFwwdxsab1kECFoaM0FET2pEd2owBXpAC2ZAS11sMhVmJREWVlFyDV4ldFIdcUMBWlBbcl5CSGFTUCEPW08eEyYNSgJhYjl8Tk9BCUpvDxsAODBeLwUfE08AAAAAAAAAAAAAAAAAEXFkfV1wB0ctDRM="

c = base64.b64decode(cb)
key = "@a-.m"

for x in range(len(c)-len(key)):
    temp = ''.join(chr(ord(x) ^ ord(y)) for x,y in zip(key,c[x:]))
    printable = True
    for y in temp:
        if y not in string.printable:
            printable = False
            break
            
    if printable:
        print x, temp

Running the script:

Command Prompt
C:\>python xor_cracker.py 0 OF04` 1 g|7# 3 ZlLLT 6 "XQ;) 8 <titl 9 U%w/; 14 1_5#T 17 MX's^ 21 stein [ ... snip ... ] 318 "XQ`" 320 </bod 324 I+B!v [ ... snip ... ] C:\>


We start noticing some HTML code at positions 8 and 320. At position 21 we also get 'stein' which is part of 'Wolfensteiny' from the PHP script. Using this information we can find the rest of the partial key which turns out to be '3Oiwa_o3@a-.m'. Using the same method we get the other 2 partial keys: 't_rsaat_4froc' and 'hx__ayowkleno'

Combining the 3 partial keys we get: th3_xOr_is_waaaay_too_w34k@flare-on.com

No comments:

Post a Comment