Le BreizhCTF est une compétition autour de la sécurité, qui a lieu à Rennes tous les ans, où des équipes de 5 personnes se retrouvent face à code obscurcit, des binaires à reverse, des injections diverses et variées... tout ça pour trouver des *flags* et marquer un maximum de points.
L'édition 2018 s'est tenue durant la nuit du 20 au 21 avril dans le hall de l'université de Rennes 1. Vous pouvez trouver la vidéo de l'évènement sur [Vimeo](https://vimeo.com/265870042). L'évènement s'est bien terminé pour notre équipe puisque nous avons fini 7ème (équipe ~).
Pour ce challenge, on récupérait un texte inconnu et un fichier javascript [baby.js](/assets/code/bzhctf18-baby.js) qui avait l'air bien obscurcit.
En écrivant ce write-up, je me rends compte qu'il n'a pas été obscurcit à la main mais à l'aide de l'outil [JSFuck](http://www.jsfuck.com/) (merci [Tristan](https://tclaverie.eu/)).
Voilà à quoi ressemble du code après passage dans JSFuck :
Pour ce challenge, l'objectif est de trouver des valeurs de départs dont le hash sha512 commence par `1337`.
C'est le principe du proof-of-work utilisé par Bitcoin. La seule façon connue à ce jour est d'essayer plein de valeurs de départ aléatoires, les hasher et regarder si le hash obtenu commence par `1337`.
Pour la complexité demandée, un simple programme en javascript suffit. Pour les valeurs aléatoires, je récupère 100 octets depuis `/dev/urandom` que j'encode en base64.
C'est cette chaine de caractère en base64 que je vais hasher.
En effet, pour valider les résultats, il est nécessaire de communiquer avec le serveur en utilisant un protocole texte : si jamais la valeur générée contient un byte interprété comme un retour à la ligne, mon résultat sera tronqué.
randomSource.getRandomBytes(100, function (buff) {
let gen = buff.toString('base64');
const hash = crypto.createHash('sha512');
hash.update(gen);
const res = hash.digest('hex')
if (res.substring(0,4) == "1337") {
console.log(gen);
}
resolve();
});
});
}
async function loop() {
while(true) {
await iterate();
}
}
loop();
```
Pour s'exécuter, il faut installer le paquet `randbytes`.
```
npm install randbytes
```
Ensuite, il suffit d'exécuter le script, de récupérer 42 valeurs et les envoyer au serveur.
```
node index.js
```
## Breizh Kartenn
Le challenge consistait à renvoyer le nom de la ville correspondant au code postal fourni. Il était plutôt buggé car il avait un comportement indéfini quand plusieurs villes avaient le même code postal.
En effet, il acceptait uniquement une seule des villes pour un code postal donné mais sans règle claire.
À cela s'ajoute qu'en fonction de la couleur du texte envoyé, il fallait répondre un texte différent.
Si le texte de la question était vert, il fallait renvoyer `YA! Me gwel <nom de la ville> :)`, si il était rouge il fallait renvoyer `NANN! Me ne gwel ket <nom de la ville> :/`.
Pour ce qui est de la couleur du texte, il s'agit de code spéciaux interprétés par les émulateurs de terminaux.
Pour faire un essai :
```
echo -e "\e[92m vert \e[91m rouge \e[0m"
```
Ce sont donc ces caractères que l'on va chercher pour connaitre la couleur du texte. Si vous voulez en savoir plus sur la couleur dans les terminaux, je vous recommande la lecture de [Bash tips: Colors and formatting (ANSI/VT100 Control sequences)](https://misc.flogisoft.com/bash/tip_colors_and_formatting).
Étant donné que l'on doit répondre à un service TCP avec un protocole texte, j'ai dù utiliser la bibliothèque socket de python également.
Cependant, faisant face à un protocole texte principalement fait pour être utilisé par un humain, il est difficile de savoir combien d'octets il faut lire avant d'avoir tout le message.
st = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
st.connect(("148.60.87.243", 9400))
msg = read_until(st, b'<ENTER>')
print(msg.decode('utf-8'))
st.send(b'\n')
while True:
msg = read_until(st, b'>>')
print(msg.decode('utf-8'))
if b'bzhctf' in msg or b'BZHCTF' in msg or b'KENAVO' in msg:
break
vert = b'\x1b[92m' in msg
rouge = b'\x1b[91m' in msg
msg = msg.decode('utf-8')
postcode = re.search(r"(\d+)\?", msg).group(1)
reponse = ""
if vert:
reponse = "YA! Me gwel "+mes_villes[postcode]+" :)\n"
else:
reponse = "NANN! Me ne gwel ket "+mes_villes[postcode]+" :/\n"
st.send(bytes(reponse, 'utf-8'))
print(reponse)
```
Au bout d'un certain nombre de bonnes réponses, on obtient le flag.
## Breizh Path
Tout comme le challenge précédent, BreizhPath nécessite d'intéragir avec un protocole texte relativement pratique pour un humain mais pas nécesseraiment facile à automatiser.
Certaines parties du texte sont colorées, ajoutant à la difficulté.
Le but de ce challenge est de trouver le chemin le plus court entre deux points d'une carte représentée par un quadrillage possédant des murs.
Il nous faut donc un algorithme de "path finding", on peut citer entre autre Breadth First Search, Dijkstra ou encore A\*. Pour plus d'informations sur ces algorithmes, je recommande [Introduction to A\*](https://www.redblobgames.com/pathfinding/a-star/introduction.html).
Pour gagner du temps, on peut récupérer l'implémentation python sur [la page dédiée](https://www.redblobgames.com/pathfinding/a-star/implementation.html#python).
Ensuite il nous reste à récupérer le labyrinthe depuis le socket, le stocker dans un format de donnée compatible avec notre implémentation, puis récupérer le résultat et le convertir au format attendu.
Il faut donc trouver le chemin le plus court de S à K. Dans ce cas, il fallait aller 4 fois à gauche puis 3 fois en haut puis 1 fois à gauche.
On nous dit que :
* haut = `i`
* bas = `k`
* gauche = `j`
* droite = `l`
La réponse attendue est donc `jjjjiiij`.
Pour l'implementation, j'ai utilisé Dijkstra, mais A* aurait éte suffisant et aurait nécessité moins de ressources.
On suppose que le fichier [implementation.py](https://www.redblobgames.com/pathfinding/a-star/implementation.py) contenant A\* et Dijkstra fourni par Red Blob Games se trouve dans le même dossier que notre solution.
```python
from implementation import *
import sys, socket
st = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
st.connect(("148.60.87.243", 9500))
data = ""
while "<ENTER>" not in data:
data = st.recv(200000).decode("utf-8")
print(data)
st.send(b'\n')
def convert_path_to_relative_moves(p):
if len(p) <2:
return ''
dx = p[0][0] - p[1][0]
dy = p[0][1] - p[1][1]
if dx <0:#right
return 'l'+recompute_path(p[1:])
elif dx > 0: # left
return 'j'+recompute_path(p[1:])
elif dy <0:#bottom
return 'k'+recompute_path(p[1:])
elif dy > 0: # up
return 'i'+recompute_path(p[1:])
while True:
# get input
data = b''
while b'New path to find' not in data and b'Out of the Grid' not in data:
data += st.recv(8096)
data = data.decode("utf-8").strip()
print(data)
if "No next step" in data:
st.close()
sys.exit(1)
# parse input
x, y, walls, weights, k, s = 0, 0, [], {}, None, None
On a donc décidé de voir si il n'était pas possible de sortir de la jail.
Notre point de départ était l'article [BreizhCTF 2016 – Write-Up – PyJail 1, 2, 3](https://securite.intrinsec.com/2016/05/17/breizhctf-2016-write-up-pyjail-1-2-3/) d'Intrinsec.
Malheureusement pas de clé `_module` dans dans la classe `warnings.catch_warnings`.
En cherchant, [Maximilien](http://mricher.fr/) a trouvé un article nommé [Python Sandbox Escape](http://blog.orleven.com/2016/10/27/python-sandbox-excape/) (en chinois !) qui nous a permis de trouver l'inspiration.
Nous avons finalement trouvé une variable qui contenait le module `os` qui nous a permis d'exécuter des commandes et, finalement, de lire le flag.
La jail possède une liste noire de mots clés, dont `os`. Il existe plusieurs solutions pour contourner cette limitation quand il s'agit d'une chaine de caractère, comme une concaténation : `'o'+'s'` ou encore en utilisant les codes ASCII `'\x6F\x73'`.
Finalement, la ligne suivante nous a permis de lire le flag :