Description
Description: Strings are not always an alternative.
Hints: APC 3.1.13 with PHP 5.4 was used.
Attachment: reversing200.zip
Resolution
We have here a mysterious file named cache.data. Since file command can not properly identify it, we try to find some useful strings in it issuing command strings
We get some interesting stuff in here :
/var/www/html/index.php <html> <head></head> <body> <form method="POST" action="login.php"> <input type="text" name="token"> <input type="submit" value="Login"> </form> </body> </html> /var/www/html/login.php [..] AzDGCrypt azdgcrypt EKO{this_is_not_the_flag} [..] e88ef51d4112b999380444ce48488762 sha1 sha1 Welcome master, your key is EKO{ [...]
EKO{this_is_not_the_flag} wasn’t the flag, obviously.
The description talked about APC, that’s where we are now heading. After some research, we discovered a function in php documentation called apc_bin_loadfile().
So we’ll compile php 5.4 with APC last version in order to load this cache file and try to get some information from it.
./bin/php -d extension=/tmp/php/php-5.4.45/compiled/lib/php/extensions/no-debug-non-zts-20100525/apc.so -d apc.enable_cli=1 -d apc.stat=0 -r "apc_bin_loadfile('cache.data'); print_r(apc_cache_info());"
This gives us some useful piece of information :
Array ( [num_slots] => 1031 [ttl] => 0 [num_hits] => 0 [num_misses] => 0 [num_inserts] => 2 [expunges] => 0 [start_time] => 1442496130 [mem_size] => 21568 [num_entries] => 2 [file_upload_progress] => 1 [memory_type] => mmap [locking_type] => pthread mutex Locks [cache_list] => Array ( [0] => Array ( [type] => file [device] => 0 [inode] => 0 [filename] => /var/www/html/index.php [num_hits] => 0 [mtime] => 1442496130 [creation_time] => 1442496130 [deletion_time] => 0 [access_time] => 1442496130 [ref_count] => 0 [mem_size] => 1200 ) [1] => Array ( [type] => file [device] => 0 [inode] => 0 [filename] => /var/www/html/login.php [num_hits] => 0 [mtime] => 1442496130 [creation_time] => 1442496130 [deletion_time] => 0 [access_time] => 1442496130 [ref_count] => 0 [mem_size] => 20368 ) ) [deleted_list] => Array ( ) [slot_distribution] => Array ( [231] => 1 [850] => 1 ) )
From this result, we understand that this cache file contains 2 php pages. Since file_get_contents doesn’t work to get source code (APC doesn’t cache the file_get_contents()), we’re going to include this two files and disassemble them.
For this operation, we used vld php extension to understand php opcodes, and it really saved us some time.
./bin/php -d extension=/tmp/php/php-5.4.45/compiled/lib/php/extensions/no-debug-non-zts-20100525/apc.so -d apc.enable_cli=1 -d apc.stat=0 -d extension=/tmp/php/php-5.4.45/compiled/lib/php/extensions/no-debug-non-zts-20100525/vld.so -d vld.active=0 -d vld.execute=1 -r "apc_bin_loadfile('cache.data'); include '/var/www/html/login.php';"
Which gave us :
Finding entry points Branch analysis from position: 0 Jump found. Position 1 = -2 function name: (null) number of ops: 4 compiled vars: none line #* E I O op fetch ext return operands ------------------------------------------------------------------------------------- 2 0 E > SEND_VAL '%2Ftmp%2Fcache.data' 1 DO_FCALL 1 'apc_bin_loadfile' 3 2 INCLUDE_OR_EVAL '%2Fvar%2Fwww%2Fhtml%2Flogin.php', REQUIRE 8 3 > RETURN 1 branch: # 0; line: 2- 8; sop: 0; eop: 3; out1: -2 path #1: 0, Finding entry points Branch analysis from position: 0 Jump found. Position 1 = 5, Position 2 = 55 Branch analysis from position: 5 Jump found. Position 1 = 15, Position 2 = 22 Branch analysis from position: 15 Jump found. Position 1 = 23, Position 2 = 29 Branch analysis from position: 23 Jump found. Position 1 = 30, Position 2 = 54 Branch analysis from position: 30 Jump found. Position 1 = 41, Position 2 = 50 Branch analysis from position: 41 Jump found. Position 1 = 53 Branch analysis from position: 53 Jump found. Position 1 = 54 Branch analysis from position: 54 Jump found. Position 1 = 58 Branch analysis from position: 58 Jump found. Position 1 = -2 Branch analysis from position: 50 Jump found. Position 1 = 54 Branch analysis from position: 54 Branch analysis from position: 54 Branch analysis from position: 29 Branch analysis from position: 22 Branch analysis from position: 55 Jump found. Position 1 = -2 filename: /var/www/html/login.php function name: (null) number of ops: 59 compiled vars: !0 = $token, !1 = $crypt, !2 = $hash, !3 = $flag line #* E I O op fetch ext return operands ------------------------------------------------------------------------------------- 3 0 E > NOP 44 1 FETCH_IS $1 '_POST' 2 ISSET_ISEMPTY_DIM_OBJ 16777216 ~2 $1, 'token' 3 BOOL_NOT ~3 ~2 4 > JMPZ ~3, ->55 45 5 > FETCH_R global $4 '_POST' 6 FETCH_DIM_R $5 $4, 'token' 7 ASSIGN !0, $5 47 8 INIT_FCALL_BY_NAME 'substr' 9 SEND_VAR !0 10 SEND_VAL 0 11 SEND_VAL 4 12 DO_FCALL_BY_NAME 3 $7 13 IS_IDENTICAL ~8 $7, 'CmxQ' ; substr($token, 0, 4) == "CmxQ" 14 > JMPZ_EX ~8 ~8, ->22 15 > INIT_FCALL_BY_NAME 'substr' 16 SEND_VAR !0 17 SEND_VAL 44 18 SEND_VAL 4 19 DO_FCALL_BY_NAME 3 $9 20 IS_IDENTICAL ~10 $9, 'MgY%2F' ; substr($token, 44, 4) == "MgY%2F" 21 BOOL ~8 ~10 22 > > JMPZ_EX ~8 ~8, ->29 23 > INIT_FCALL_BY_NAME 'substr' 24 SEND_VAR !0 25 SEND_VAL -4 26 DO_FCALL_BY_NAME 2 $11 27 IS_IDENTICAL ~12 $11, 'Mg%3D%3D' ; substr($token, -4) == "Mg%3D%3D" 28 BOOL ~8 ~12 29 > > JMPZ ~8, ->54 48 30 > FETCH_CLASS 4 :13 'AzDGCrypt' 31 NEW $14 :13 ; $14 = new AzDGCrypt('EKO{this_is_not_the_flag}'); 32 SEND_VAL 'EKO%7Bthis_is_not_the_flag%7D' 33 DO_FCALL_BY_NAME 1 34 ASSIGN !1, $14 49 35 INIT_METHOD_CALL !1, 'decrypt' ; 36 SEND_VAR !0 37 DO_FCALL_BY_NAME 1 $18 ; $18 = $14.decrypt($token); 38 ASSIGN !2, $18 51 39 IS_IDENTICAL ~20 !2, 'e88ef51d4112b999380444ce48488762' ; $14.decrypt($token) == md5("EKO{}") <-- This is what we want ! 40 > JMPZ ~20, ->50 52 41 > INIT_FCALL_BY_NAME 'sha1' 42 SEND_VAR !0 43 DO_FCALL_BY_NAME 1 $21 44 ASSIGN !3, $21 ; $21 c'est le flag, c'est sha1($token) 53 45 ADD_STRING ~23 'Welcome+master%2C+your+key+is+EKO%7B' ; %7B <=> "{" 46 ADD_VAR ~23 ~23, !3 ; Le flag c'est le "!3" 47 ADD_CHAR ~23 ~23, 125 ; 125 == %7D == "}" 48 ECHO ~23 54 49 > JMP ->53 55 50 > INIT_FCALL_BY_NAME 'header' 51 SEND_VAL 'Location%3A+index.php' 52 DO_FCALL_BY_NAME 1 57 53 > > JMP ->54 58 54 > > JMP ->58 59 55 > INIT_FCALL_BY_NAME 'header' 56 SEND_VAL 'Location%3A+index.php' 57 DO_FCALL_BY_NAME 1 61 58 > > RETURN 1
From this result, we understand that POST parameter token has to follow a special format. There are 3 checks based on substrings :
- substr($token, 0, 4) must be equal to “CmxQ”
- substr($token, 44, 4) must be equal to “MgY/”
- substr($token, -4) must be equal to “Mg==”.
If these 3 conditions are met, this token is deciphered using the method decrypt of AzDGCrypt class. AzDGCrypt is instantiated with parameter EKO{this_is_not_the_flag} (Used as ciphering key).
Deciphered text must be equal to e88ef51d4112b999380444ce48488762.
Before trying to reverse this class, we made some research, and found its source code on github.
At this point, we could think we had every thing we needed to decipher this token, be if we look closely at AzDGCrypt class, we can see a randomization part before encryption function : $r = md5(rand(0,32000));
Since we only have some parts of base64 encoded token, we had to brute force this randomization and for every result, check if all of the 3 substrings matched. This is our bruteforce code :
<?php class AzDGCrypt{ var $k; function AzDGCrypt($m){ $this->k = $m; } function ed($t) { $r = md5($this->k); $c=0; $v = ""; for ($i=0;$i<strlen($t);$i++) { if ($c==strlen($r)) $c=0; $v.= substr($t,$i,1) ^ substr($r,$c,1); $c++; } return $v; } function crypt($t, $rand){ $r = md5($rand); $c=0; $v = ""; for ($i=0;$i<strlen($t);$i++){ if ($c==strlen($r)) $c=0; $v.= substr($r,$c,1) . (substr($t,$i,1) ^ substr($r,$c,1)); $c++; } return base64_encode($this->ed($v)); } function decrypt($t) { $t = $this->ed(base64_decode($t)); $v = ""; for ($i=0;$i<strlen($t);$i++){ $md5 = substr($t,$i,1); $i++; $v.= (substr($t,$i,1) ^ $md5); } return $v; } } // bf $crypt = new AzDGCrypt('EKO{this_is_not_the_flag}'); for ($i = 0; $i <= 32000; ++$i) { $result = $crypt->crypt('e88ef51d4112b999380444ce48488762', $i); if (substr($result, 0, 4) == 'CmxQ' && substr($result, 44, 4) == 'MgY/' && substr($result, -4) == 'Mg==') { echo "b64: $result\nRand: $i\n"; break; } }
After a few seconds, we got our expected result
b64: CmxQaQAzBTYKZQYzAWAFZAY1V2NVZAZgUGQCbAU9Vz4CMgY/AzgLaQwxWm5SYVY2UGNUaVFlAm5VO1MzBDNQMg== Rand: 17291
We then test this token with the cached php page
<?php $_POST['token'] = 'CmxQaQAzBTYKZQYzAWAFZAY1V2NVZAZgUGQCbAU9Vz4CMgY/AzgLaQwxWm5SYVY2UGNUaVFlAm5VO1MzBDNQMg=='; apc_bin_loadfile('cache.data'); include('/var/www/html/login.php');
Everything’s alright !
Welcome master, your key is EKO{59a59936b318e8ef20fd923a3e7b05a1e44e9e91}
wow, thank you so much!
Hi, i tried to install PHP 5.4.45 and APC 3.1.13, but I cannot import “cache.data”, i got error “Segmentation fault”.
What is the OS version you used ? Can you give some detail of your command to install ?
Thanks,
Hi,
I have compiled myself both PHP 5.4.45 and APC 3.1.13 on ubuntu 15.04 x64.
If you get segmentation fault, make sure you have not version conflicts (eg: used system phpize instead of freshly compiled 5.4 phpize when compiling APC)
Yeah, I can use it on Ubuntu 15.04 x64. Thank you 🙂