Français [EKOPARTY PRE-CTF 2015] [Reverse 200 – Reversing the APC cache] Write Up

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}

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *