hack.lu 2025 CTF: MISC
Écrit par l’équipe HackGyver – Octobre 2025
Cette année le hackerspace a participé au CTF hack.lu
voici nos writeups pour les épreuves de la catégorie MISC.![]()
1: GISSNINGSLEK
Dans cette première épreuve, nous avons une archive à télécharger contenant le code de celle-ci,
le but étant d'analyser le code, trouver la vulnérabilité, et pouvoir tester nos trouvailles par la suite sur l'instance de l'épreuve.
Analyse du code, nous nous retrouvons avec ceci:
#!/usr/bin/env bash
echo "Låt oss spela en gissningslek!"
echo "Varning: Du får inte ändra din gissning. :("
read -r user_guess
function guess() {
rand=$(( ( RANDOM % 10000 ) + 1337 ))
if [[ "${1}" -eq "${rand}" ]];
then
echo "Rätta"
else
echo "Fel"
exit 1
fi
}
for _ in {1..1000}; do
guess "${user_guess}"
done
/readflag${rand} est créé juste avant le test${1} est l'argument fourni,-eq compare les entiers.
Si l'argument vaut rand, Bash recherche la variable rand et la remplace par sa valeur.
Donc[[ ${rand} -eq ${rand} ]]
Rend la valeur toujours vraie.
nc gissningslek.solven.jetzt 1024
rand
flag : flag{it5_y0ur_lucky_d4y_h3h3_04217a096}2: CÖDEBULLAR
Dans cette épreuve nous avons 2 fichiers, un script python ainsi que des images dans encoded.zip
encoded.zip pèse 337Mb et contient 5536 JPGs de boulette de viande et de hotdog.
Le script python contient ce code:
import os
import random
from PIL import Image
köttbullar_dir = './assets/köttbullar'
hotdogs_dir = './assets/hotdogs'
output_dir = './encoded'
os.makedirs(output_dir, exist_ok=True)
köttbullar_files = [os.path.join(köttbullar_dir, f) for f in os.listdir(köttbullar_dir)]
hotdogs_files = [os.path.join(hotdogs_dir, f) for f in os.listdir(hotdogs_dir)]
with open('./secret.txt', 'r') as f:
FLAG = f.read().strip()
bin_str = ''.join(format(ord(c), '08b') for c in FLAG)
for i, bit in enumerate(bin_str):
src = random.choice(köttbullar_files) if bit == '0' else random.choice(hotdogs_files)
dst = os.path.join(output_dir, f'{i:04}.jpeg')
with Image.open(src) as img:
img.save(dst, format='JPEG', quality=95)
print(f'Encoded {len(bin_str)} bits with CODEBULLAR encoding')On comprend que le script lit un flag dans secret.txt, puis il convertit chaque caractère du flag en une chaine binaire de 8 bits.
Les boulettes de viande valent 0 et les hotdogs 1.
Chaque image est enregistrée séquentiellement avec un nom de fichier numéroté dans le répertoire encodé.
Le but de l'épreuve va donc être de faire l'inverse du script pour retrouver le flag.
En regardant de plus près les images, on se rend compte qu'il y a beaucoup de doublons.
Un script de hachage MD5 nous permet de déterminer que nous avons 32 images uniques.
Maintenant il ne reste plus qu'a déterminé visuellement ces 32 images pour savoir si elle valent 0 ou 1.
Ensuite il ne nous reste plus qu'à faire un script pour tester la logique.
Ce qui nous donne:
import os
import hashlib
TABLE = {
'028e84fb5ac820c8b56c6111570e6e59': 1,
'0ae91c6d107ae8e01895a611f69b9076': 0,
'0b4bb18662e48c9f25a6ddfd03b72e4e': 1,
'0ba665f502e05b7694b25fc524081908': 0,
'12ac15351319e2e13d385b7b1360b421': 1,
'1670914879f249f5e010e905cc15a38b': 1,
'1adbd84414a9670947fde51c7e50793b': 1,
'1cecd8652db95bdb9f5aa3929395115c': 1,
'235e88ac5da4564e63869bd16eed6923': 1,
'361f909edca550bda7c3a500c4e23d2c': 0,
'460e7d9bc00a4d9b39eed107859e47b7': 1,
'4e2c98391db9b709c061de3155a9c5a8': 1,
'5360c0811ae9c834841a1df11435ef39': 1,
'56da50eb4e1380a8c8f4d4c1a24b1902': 1,
'5e8669a685f90ad76ff1e42664bcceeb': 1,
'5f0cfd2d38e3dcb286168ab55611dd7f': 0,
'746416a03e4d829898a75eb38bf1620c': 0,
'779691412dd42f96f30cd7159d7bede0': 0,
'90ee178e82079f07dc8686b958d643ce': 0,
'9355b4e9ec7d67c5561b7b22fe94b435': 0,
'9b6e11ad1b835cd3fb4e4c4999bbcb5b': 0,
'ab1a5c131af9add1fca5a2c51ad59c82': 0,
'ab43bd902c2bf3afbe6aa57f3aa48fa4': 1,
'bc6b23051aa32592fda8e28d8ee2eb4d': 1,
'c53416c34afde69145c102886caaa718': 0,
'c66bd7d67171f5247a12b3acfebe01d9': 0,
'ca337d78c310867a5611646356272df3': 1,
'cace94a2d4fd007e9785559449f784df': 0,
'e2926976ffb002760c9c22ce60a4fe67': 0,
'e5271ec9c7dd3d439048a45967f15aca': 0,
'f167917abe2400b1302cc956750a4ad2': 1,
'ff50ea5e40ca336ec5d16c7883bd6d91': 0,
}
b = ''.join([str(TABLE[hashlib.md5(open(f'encoded/{filepath}', 'rb').read()).hexdigest()]) for filepath in os.listdir('encoded')])
decoded = ''.join(chr(int(b[i:i+8], 2)) for i in range(0, len(b), 8))
print(_)Nous obtenons le résultat suivant:
Hotdogs are sausages served in soft buns, typically made from beef, pork, or chicken. They are often
topped with mustard, ketchup, onions, or relish. The world record HPM (Hotdogs per Minute) is 6,
achieved by Miki Sudo. The flag for this challenge is flag{w3_0bv10u5ly_n33d3d_4_f00d_ch4113n93}.
Köttbullar are Swedish meatballs made from ground beef and pork, mixed with breadcrumbs, egg, and
spices. They are usually served with creamy gravy, lingonberry jam, and boiled potatoes. According to the
swedish government, köttbullar are based on a recipe King Karl XII brought home from the ottoman
empire. However, the Swedish food historian Richard Tellström says this claim is a modern myth3: ZIGBÄKVÄM
Dans cette épreuve, un simple .pcap a téléchargé, le lien 'Hint' dans la description nous renvoie sur le site
d'Ikea, plus précisément sur une page FAQ qui nous explique a quoi sert le Zigbee.
Il n'y a plus qu'à regarder les trames du pcap
La trame 71 nous met sur la voie avec un string FlagDevice
On observe la frame suivante, qui contient: String: flag NWK=0xf001 EP=0x35
On va faire un filtre pour rendre la lecture plus simple: zbee_nwk.addr eq 0xf001 and zbee_nwk.addr eq 0x0000
a partir de la on observe les frames, notamment celle en 111.
Frame 111: 55 bytes on wire (440 bits), 55 bytes captured (440 bits)
IEEE 802.15.4 Data, Src: 0x0000, Dst: 0xf001
ZigBee Network Layer Data, Dst: 0xf001, Src: 0x0000
Frame Control Field: 0x0008, Frame Type: Data, Discover Route: Suppress Data
Destination: 0xf001
Source: 0x0000
Radius: 30
Sequence Number: 36
ZigBee Application Support Layer Data, Dst Endpt: 53, Src Endpt: 1
Frame Control Field: Data (0x00)
Destination Endpoint: 53
Cluster: Manufacturer Specific (0xfc00)
Profile: Home Automation (0x0104)
Source Endpoint: 1
Counter: 5
ZigBee Cluster Library Frame, Mfr: Unknown (0xbeef)
Frame Control Field: Cluster-specific (0x05)
.... ..01 = Frame Type: Cluster-specific (0x1)
.... .1.. = Manufacturer Specific: True
.... 0... = Direction: Client to Server
...0 .... = Disable Default Response: False
Manufacturer Code: Unknown (0xbeef)
Sequence Number: 4
Command: Unknown (0x10)
Data (23 bytes)
Data: 4354524c5f4745545f4e4558545f464c41475f42595445
[Length: 23]Qui a comme Manufacturer Code: Unknown (0xbeef) et en donnée envoyée: 4354524c5f4745545f4e4558545f464c41475f42595445
Convertie cela donne: CTRL_GET_NEXT_FLAG_BYTE
Cela nous met sur la piste du Manufacturer Code: Unknown (0xbeef)
Continuons a regarder les trames, notamment la trame 113, on retrouve 0xbeef avec en donnée envoyer dans bitmap8: 0x66 qui correspond à la lettre f, surement que le reste du flag ce situe dans les autres trames 0xbeef..
On affine donc notre recherche pour ce focalisé sur les trames contenant le manufacturer 0xbeef et celle qui on de la donnée avec bitmap8.
nous trouvons ces trames:
Trame 113: 0x66 - f
Trame 139: 0x6c - l
Trame 161: 0x61 - a
Trame 186: 0x67 - g
Trame 216: 0x7b - {
On sait comment récupérer le flag, mais ça serrais vraiment trop long de faire ça a la main.
Nous allons donc utiliser python pour récuperer toutes les trames contenant 0xbeef ainsi que bitmap8 et nous afficher le flag:
import pyshark
filters = 'zbee_nwk.addr eq 0xf001 and zbee_zcl.attr.bitmap8 and zbee_zcl.cmd.mc == 0xbeef'
cap = pyshark.FileCapture('challenge.pcap', display_filter=filters)
flag = ''.join([chr(int(packet.zbee_zcl.attr_bitmap8, 16)) for packet in cap])
print(flag)
Nous obtenons: flag{zigbee_for_smart_home_1s_gr8}
4: FÄNGELSE
Cette epreuve ne nous donne pas d'indice, juste un fichier a télécharger, alors alllons y.
On se retrouve avec ce code python dans l'archive:
flagbuf = open("flag.txt", "r").read()
while True:
try:
print(f"Side-channel: {len(flagbuf) ^ 0x1337}")
# Just in case..
except Exception as e:
# print(f"Error: {e}") # don't want to leak anything
exit(1337)
code = input("Code: ")
if len(code) > 5:
print("nah")
continue
exec(code)
Le but étant de trouver un moyen de lire flag.txt avec seulement 5 caractères dans exec().
On comprend que l'on va devoir manipuler "len" pour résoudre cette épreuve.
on essaye avec id:
Side-channel: 4890 Code: a=id Side-channel: 4890 Code: len=a Side-channel: 136782775160839
La théorie a l'air de ce confirmé, essayons avec a=anyany d'une chaine de caractère est toujours TrueTrue > 5 est toujours False
$ nc xn--fngelse-5wa.solven.jetzt 1024
Side-channel: 4890
Code: a=all
Side-channel: 4890
Code: len=a
Side-channel: 4918
Code: print(flagbuf)
flag{1_2_3_4_5_6_7_8_9_10_exploit!_12_13_...}
Side-channel: 4918}
Parfait !
Conclusion
Ces épreuves faisaient partie des plus résolues du CTF dans la catégorie MISC.
Et franchement, c'était un vrai régal à faire.
Le thème IKEA / Suède était juste parfait : entre les boulettes de viande, les hotdogs et le Zigbee.
On s'est vraiment crus en train de monter un meuble "CTFÖRJORD" avec un tournevis en ASCII.
Des challenges funs, clairs, et accessibles, parfaits pour se détendre tout en réfléchissant.
Encore un grand bravo a Fluxfingers pour l'humour et la créativité.
Et à tous les gens de l'association pour la bonne ambiance tout le week-end sur discord.
