Description
Description: Strings are not always an alternative.
Hints: APC 3.1.13 with PHP 5.4 was used.
Attachment: reversing200.zip
Resolution
Nous nous retrouvons devant un mystérieux fichier cache.data. Celui-ci n’étant pas identifié avec notre habituelle commande « file », on essaye alors avec la commande strings.
Ici quelques choses intéressantes :
/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{ [...]
Evidemment, le flag n’était pas « EKO{this_is_not_the_flag} ».
L’énoncé nous parle de cache APC, nous allons donc poursuivre cette voie. D’ailleurs il y a une fonction qui a l’air assez sympa dans le doc php, apc_bin_loadfile().
Nous allons donc compiler php 5.4 et la dernière version d’APC afin de pouvoir charger le fichier de cache et pouvoir l’utiliser. Ce qui nous montre quelque choses intéressantes :
./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());"
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 ) )
Nous apprenons donc que le fichier de cache contient deux pages php. Bien entendu un file_get_contents pour obtenir le code source ne fonctionne pas (comme APC ne met pas en cache les file_get_contents()), on va alors devoir inclure les différents fichiers et désassembler le résultat.
Pour ceci nous avons utilisé l’extension php vld qui nous a bien simplifié la compréhension des opcodes php.
./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';"
Ce qui nous a donné :
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
On comprend ici que le paramètre POST « token » doit avoir un certain format. Il est vérifié par 3 substr : substr($token, 0, 4) doit valoir « CmxQ », substr($token, 44, 4) : « MgY/ » et substr($token, -4) : « Mg== ».
Si ces conditions sont remplies, il est décrypté à l’aide de la méthode « decrypt » d’une classe s’appelant « AzDGCrypt » et instanciée avec comme paramètre « EKO{this_is_not_the_flag} » (qui sert de clé de cryptage), et le texte décrypté doit avoir comme valeur « e88ef51d4112b999380444ce48488762 » .
Au lieu de nous embêter à comprendre le code désassemblé de cette classe, nous avons fait une recherche sur google « au cas où », et cela a porté ses fruits, le code source était disponible sur github.
Arrivé à ce point, on aurait pu se dire que comme nous avions le code pour crypter/décryter, la clé de cryptage ainsi que le texte à retrouver en clair, nous avions tout, mais en fait pas tout à fait.
La fonction de cryptage a une partie aléatoire : $r = md5(rand(0,32000));, ce qui fait que le token base64 n’a que très peu de chances de correspondre aux verifications faites par les trois substr.
Nous avons alors modifié la fonction crypt afin qu’elle prenne un nouvel argument, celui qui aurait dû être aléatoire, puis nous avons créé un script de bruteforce. 32000 possibilités ça va assez vite à tester.
<?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; } }
Au bout de quelques instants, le résultat s’affiche :
b64: CmxQaQAzBTYKZQYzAWAFZAY1V2NVZAZgUGQCbAU9Vz4CMgY/AzgLaQwxWm5SYVY2UGNUaVFlAm5VO1MzBDNQMg== Rand: 17291
On teste alors ce token avec la page cachée :
<?php $_POST['token'] = 'CmxQaQAzBTYKZQYzAWAFZAY1V2NVZAZgUGQCbAU9Vz4CMgY/AzgLaQwxWm5SYVY2UGNUaVFlAm5VO1MzBDNQMg=='; apc_bin_loadfile('cache.data'); include('/var/www/html/login.php');
Tout est OK ! 🙂
Welcome master, your key is EKO{59a59936b318e8ef20fd923a3e7b05a1e44e9e91}