404CTF - Divers
Divers
5 Ronisés (Intro)
On va jouer à un petit jeu… Arriveras-tu à gagner contre ma génération hyper-sécurisée ?
Code Python fournit par le chall
Résolution
Dans le code fourni, on remarque un élément: la génération d’une seed via timestamp:
def set_seed(): seed(int(time()))Le générateur pseudo-aléatoire est donc seedé avec l’heure actuelle. Le nombre secret n’est donc pas totalement imprévisible, et dépends du moment où le programme est lancé. Le reste du code est déterministe.
L’idée est donc de lancer une première fois le programme afin d’obtenir un résultat:
┌─[✗]─[gabrog@parrot]─[~]└──╼ $nc challenge.404ctf.fr 10200On va jouer à un jeu. Devine mon nombre secret et je te donne le flag !> 0Et non, mon nombre c'était : 71627049126117054643922701134319866539130929766361621550137411269792890719118On peut donc essayer de bruteforcer les timestamps proches pour trouver la seed:
target = int(input("secret affiché = "))
now = int(time())
# On cherche dans une range de +/- 30sfor t in range(now - 30, now + 30): seed(t)
s = genere_nombre_super_secret(8)
if s == target: print("Seed trouvée : ", t) breakEt en connaissant la seed, on peut recalculer le secret. L’idée est donc de prédire l’heure du prochain lancement, pour anticiper la seed, et générer le bon secret:
date = int(datetime.datetime(2026, 5, 16, 22, 51, 0).timestamp())
# On cherche dans une range de +/- 30sfor t in range(date - 1, date + 2): seed(t) s = genere_nombre_super_secret(8)
print("Secret = ", s)Je génère ici 3 secrets: à t-1, t, et t+1 afin de pouvoir déterminer, si ma solution est incorrecte, quel était le timing.
Après quelques tentatives, je réussis à timer correctement le lancement du chall, et à obtenir le flag:
┌─[✗]─[gabrog@parrot]─[~]└──╼ $nc challenge.404ctf.fr 10200On va jouer à un jeu. Devine mon nombre secret et je te donne le flag !> 103817526872077778480925424787095322347587124263041450339005552241472860670495Wow ! Voici le flag : 404CTF{J0l1_T1m1ng!\}Le flag est donc 404CTF\{J0l1_T1m1ng!\}
Super enQuête Libre (1/4) (Intro)
Vous êtes un enquêteur mandaté par Télématique NordRouan, une école réputée dans le domaine de la poterie, pour élucider une affaire de vol. Des pièces d’une valeur inestimable ont disparu et il est impératif de retrouver le coupable, sous peine de voir l’établissement fermer ses portes.
Pour mener votre enquête, vous obtenez l’accès à une base de données recensant l’ensemble des étudiants, professeurs et employés de l’école. Avant de vous confier l’affaire, l’école souhaite évaluer vos compétences. Votre première mission : déterminer quel badge Noel Laurent utilise actuellement. Format du flag : 404CTF{badge_noel_laurent} Exemple du flag : 404CTF{19}
Résolution
On se connecte donc sur un serveur SQLite.
On va lister les tables, et leur format:
sql> .tables AccessLog Attendance Badge Building Course Person Room sqlite_sequence
sql> .schema
# Je ne garde volontairement que ce qui nous intéresse pour ce chall
CREATE TABLE Badge ( badge_id INTEGER PRIMARY KEY AUTOINCREMENT, person_id INTEGER NOT NULL REFERENCES Person(person_id), issue_date TEXT NOT NULL, expiry_date TEXT NOT NULL, active INTEGER NOT NULL CHECK(active IN (0, 1)) );
# [...]
CREATE TABLE Person ( person_id INTEGER PRIMARY KEY AUTOINCREMENT, last_name TEXT NOT NULL, first_name TEXT NOT NULL, birth_date TEXT NOT NULL, email TEXT NOT NULL UNIQUE, status TEXT NOT NULL CHECK(status IN ('student', 'teacher', 'staff')), class TEXT );
# [...]On a donc deux tables itéressantes: Badge et Person. Si on affiche le contenu de la table Person:
sql> SELECT * FROM Person;+-----------+-----------+------------+------------+----------------------------+---------+-------+| person_id | last_name | first_name | birth_date | email | status | class |+-----------+-----------+------------+------------+----------------------------+---------+-------+| 1 | Durand | Alice | 2003-09-15 | alice.durand@campus.fr | student | G1 || 2 | Simon | Baptiste | 2003-08-11 | baptiste.simon@campus.fr | student | G5 || 3 | Michel | Camille | 2003-03-07 | camille.michel@campus.fr | student | G2 |[...]| 28 | Dupont | Béatrice | 2003-11-18 | beatrice.dupont@campus.fr | student | G1 || 29 | François | Cédric | 2002-07-11 | cedric.francois@campus.fr | student | G1 || 30 | Garnier | Delphine | 2006-08-04 | delphine.garnier@campus.fr | student | G6 |+-----------+-----------+------------+------------+----------------------------+---------+-------+ 30 ligne(s) (affichage limité à 30 lignes sur 223)On a 223 enregistrements, et on sait que les noms et prénoms sont enregistrés avec majuscules. On peut donc faire une requête SQL pour chercher l’ID de Noel Laurent:
SELECT person_id FROM Person WHERE first_name = "Laurent" and last_name = "Noel";Ce qui donne en résultat:
sql> SELECT person_id FROM Person WHERE first_name = "Laurent" and last_name = "Noel";+-----------+| person_id |+-----------+| 38 |+-----------+On peut donc chercher le numéro de badge dans la table badge (en filtrant uniquement sur les actifs):
SELECT badge_id FROM Badge WHERE person_id = 38 AND active = 1;Le retour est:
sql> SELECT badge_id FROM Badge WHERE person_id = 38 AND active = 1;+----------+| badge_id |+----------+| 165 |+----------+Le flag est donc 404CTF\{165\}.
Super enQuête Libre (2/4) (Facile)
Vos compétences étant désormais reconnues, Télématique NordRouan vous confie officiellement l’enquête. Vous supposez que le coupable a effectué des repérages avant de passer à l’acte. Identifiez la dernière personne à avoir visité 4 salles différentes au cours d’une même journée. Format du flag : 404CTF{prenom_nom} Exemple du flag : 404CTF{noé_dupont}
Résolution
En ayant les schémas des tables d’après le chall précédent, on peut builder une requête SQL pour avoir la réponse rapidement:
SELECT p.person_id, p.first_name, p.last_name, al.date, MAX(al.time) AS last_visit_time, COUNT(DISTINCT al.room_id) AS nb_roomsFROM AccessLog alJOIN Badge b ON al.badge_id = b.badge_idJOIN Person p ON b.person_id = p.person_idGROUP BY p.person_id, al.dateHAVING COUNT(DISTINCT al.room_id) >= 4ORDER BY al.date DESC, last_visit_time DESCLIMIT 1;Pour explications:
On débute de la table AccessLog qui contient les infos:
- Quel badge
- Dans quelle salle
- A quelle date & heure
Depuis ce log, il faut retrouver la personne, donc à travers son ID de badge.
Jointure sur les badges, puis les personnes:
FROM AccessLog alJOIN Badge b ON al.badge_id = b.badge_idJOIN Person p ON b.person_id = p.person_idOn rajoute un select pour identifier la personne:
SELECT p.person_id, p.first_name, p.last_name,Puis il reste alors à compter le nombres de salles uniques visitées:
COUNT(DISTINCT al.room_id)Sans oublier de grouper par personne, et par date (jour, pas heure):
GROUP BY p.person_id, al.datePuis, on nous demande au moins 4 salles de visitées:
HAVING COUNT(DISTINCT al.room_id) >= 4La requête aurait pu s’arrêter là, mais si on veut réellement un seul résultat à la fin, on peut trier par date et heure décroissante:
ORDER BY al.date DESC, last_visit_time DESCEt pour finaliser le tout, on limite à un résultat:
LIMIT 1La requête retourne:
+-----------+------------+-----------+------------+-----------------+----------+| person_id | first_name | last_name | date | last_visit_time | nb_rooms |+-----------+------------+-----------+------------+-----------------+----------+| 63 | Noémie | Robert | 2026-05-04 | 19:19:57 | 4 |+-----------+------------+-----------+------------+-----------------+----------+Le flag est donc 404CTF\{noémie_robert\}.
Super enQuête Libre (3/4) (Extrême)
L’interrogatoire de ce suspect n’a rien révélé de concluant. Cependant, une enquête interne au sein de Télématique NordRouan révèle que le voleur aurait utilisé un badge dupliqué pour accéder à la salle au moment du vol. D’après cette même enquête interne, ce vol aurait eu lieu le 16 mai vers 19h, soit après le dump de la base des données. Retrouvez alors l’identifiant de ce badge. Pour vous aider, Télématique NordRouan met à votre disposition la carte du campus.
Format du flag : 404CTF{identifiant_badge}
Résolution
On cherche donc dans les AccessLog deux accès proches temporellement, mais impossible physiquement, pour commencer le 16 mai:
SELECT al.badge_id, al.room_id, r.room_name, b.building_name, al.date, al.timeFROM AccessLog alJOIN Room r ON al.room_id = r.room_idJOIN Building b ON r.building_id = b.building_idWHERE al.date = "2025-05-16"ORDER BY al.badge_id, al.time;Sauf que, spoiler, l’énnoncé dit soit après le dump de la base des données. Donc notre date n’est pas dans la base!
Il faut trouver ce même comportement suspect, dans les logs présents actuellement.
On va donc chercher à comparer la table AccessLog avec elle-même pour trouver les ID de badges, les heures, et les salles qui en découlent.
On considère a1 = un passage et a2 un autre passage du même badge.
SELECT a1.badge_id, a1.date, a1.time, r1.room_name, a2.time, r2.room_nameFROM AccessLog a1JOIN AccessLog a2 ON a1.badge_id = a2.badge_id -- Le même ID de badgeJOIN Room r1 ON a1.room_id = r1.room_id -- On récupère l'ID de la room 1JOIN Room r2 ON a2.room_id = r2.room_id -- On récupère l'ID de la room 2WHERE a1.date = a2.date AND a1.room_id != a2.room_id; -- On cherche les dates identiques, mais salles différentesCette requête retourne ceci:
+----------+------------+----------+-----------+----------+-----------+| badge_id | date | time | room_name | time | room_name |+----------+------------+----------+-----------+----------+-----------+| 100 | 2025-09-15 | 17:00:04 | H12 | 17:00:04 | H12 || 98 | 2025-09-15 | 17:01:23 | H12 | 17:01:23 | H12 || 119 | 2025-09-15 | 17:01:33 | H12 | 17:01:33 | H12 || 90 | 2025-09-15 | 17:01:38 | H12 | 17:01:38 | H12 |On remarque que les entrées sont dupliquées, et ceci est dû à la jointure sur AccessLog.
On peut éliminer ce soucis avec rajout de AND a1.log_id < a2.log_id sur la jointure AccesLog a2:
SELECT a1.badge_id, a1.date, a1.time, r1.room_name, a2.time, r2.room_nameFROM AccessLog a1JOIN AccessLog a2 ON a1.badge_id = a2.badge_id AND a1.log_id < a2.log_idJOIN Room r1 ON a1.room_id = r1.room_idJOIN Room r2 ON a2.room_id = r2.room_idWHERE a1.date = a2.date AND a1.room_id != a2.room_id;Le résultat est bien meilleur:
+----------+------------+----------+-----------+----------+-----------+| badge_id | date | time | room_name | time | room_name |+----------+------------+----------+-----------+----------+-----------+| 183 | 2025-10-23 | 11:39:13 | G24 | 12:32:21 | B12 || 102 | 2025-11-05 | 13:31:35 | C33 | 13:33:45 | B12 || 157 | 2025-11-11 | 11:11:25 | G12 | 13:19:44 | C51 || 200 | 2025-12-04 | 09:49:56 | C01 | 13:41:55 | H03 || 211 | 2025-12-05 | 08:46:23 | C34 | 11:32:48 | E43 |Mais on a toujours trop d’entrées: 1321. Il faut filter davantage pour essayer d’extraire le comportement suspect.
On peut donc essayer de chercher des comportements suspects en peu de temps, en filtrant sur une différence de temps courte pour le même badge.
Nos dates et heures sont stockées dans deux colonnes différentes. Il faut trouver un moyen de comparer ces éléments de façon simple: le timestamp.
On peut les build avec strftime, à condition d’avoir le bon format, et c’est le cas ici!
Par exemple, on pourrait avoir:
strftime('%s', a1.date || ' ' || a1.time)pour récupérer la date et heure d’un log en timestamp.
Et si on a deux timestamp, alors on peut les comparer. Et si on peut les comparer, alors on peut déceler les comportements suspects!
On va donc rajouter cette comparaison de timestamp ABS(strftime('%s', a1.date || ' ' || a1.time) - strftime('%s', a2.date || ' ' || a2.time)) < 120 à notre requête (on prend 2minutes comme référenciel, on peut affiner si besoin).
SELECT a1.badge_id, a1.date, a1.time AS time1, r1.room_name AS room1, a2.time AS time2, r2.room_name AS room2FROM AccessLog a1JOIN AccessLog a2 ON a1.badge_id = a2.badge_id AND a1.log_id < a2.log_idJOIN Room r1 ON a1.room_id = r1.room_idJOIN Room r2 ON a2.room_id = r2.room_idWHERE a1.date = a2.date AND a1.room_id != a2.room_id AND ABS( strftime('%s', a1.date || ' ' || a1.time) - strftime('%s', a2.date || ' ' || a2.time)) < 120ORDER BY a1.badge_id, a1.date;Ce qui retourne:
+----------+------------+----------+-------+----------+-------+| badge_id | date | time1 | room1 | time2 | room2 |+----------+------------+----------+-------+----------+-------+| 114 | 2026-02-17 | 08:05:52 | C34 | 08:07:23 | C23 || 174 | 2026-03-17 | 09:47:59 | C14 | 09:49:12 | C02 || 134 | 2026-04-03 | 08:10:18 | G34 | 08:12:07 | G24 || 105 | 2026-04-03 | 17:10:14 | H05 | 17:11:24 | H11 || 218 | 2026-04-10 | 13:35:24 | G22 | 13:36:57 | G11 || 118 | 2026-04-24 | 15:19:40 | H22 | 15:21:22 | H32 |+----------+------------+----------+-------+----------+-------+Aucun comportement suspect ici, tout a l’air correct. On peut donc augmenter la durée (5minutes):
+----------+------------+----------+-------+----------+-------+| badge_id | date | time1 | room1 | time2 | room2 |+----------+------------+----------+-------+----------+-------+| 102 | 2025-11-05 | 13:31:35 | C33 | 13:33:45 | B12 || 214 | 2025-12-11 | 15:21:38 | B05 | 15:23:56 | B23 || 117 | 2025-12-12 | 11:37:24 | D32 | 11:40:54 | E21 || 118 | 2025-12-18 | 09:51:27 | A22 | 09:53:47 | G22 || 158 | 2026-01-15 | 17:02:04 | F03 | 17:04:32 | E24 || 236 | 2026-01-30 | 11:31:49 | D11 | 11:35:01 | E25 || 248 | 2026-02-06 | 15:18:29 | C35 | 15:21:47 | A22 || 136 | 2026-02-06 | 15:24:53 | F03 | 15:27:21 | E33 || 166 | 2026-02-11 | 17:09:31 | B11 | 17:12:47 | G22 || 114 | 2026-02-17 | 08:05:52 | C34 | 08:07:23 | C23 || 90 | 2026-02-18 | 08:07:40 | C33 | 08:10:03 | B14 || 228 | 2026-03-11 | 11:33:04 | F03 | 11:35:15 | E22 || 174 | 2026-03-11 | 11:37:37 | F03 | 11:42:15 | F43 || 209 | 2026-03-11 | 11:39:50 | F03 | 11:42:17 | E31 || 174 | 2026-03-17 | 09:47:59 | C14 | 09:49:12 | C02 || 206 | 2026-03-20 | 17:04:05 | F32 | 17:06:29 | E42 || 102 | 2026-04-01 | 09:51:12 | H21 | 09:53:23 | H04 || 134 | 2026-04-03 | 08:10:18 | G34 | 08:12:07 | G24 || 105 | 2026-04-03 | 17:10:14 | H05 | 17:11:24 | H11 || 218 | 2026-04-10 | 13:35:24 | G22 | 13:36:57 | G11 || 238 | 2026-04-13 | 08:01:46 | C51 | 08:06:02 | C11 || 211 | 2026-04-14 | 17:07:13 | F14 | 17:09:30 | E42 || 232 | 2026-04-16 | 09:46:41 | C52 | 09:49:02 | B32 || 159 | 2026-04-16 | 09:48:46 | D22 | 09:51:54 | E24 || 118 | 2026-04-24 | 15:19:40 | H22 | 15:21:22 | H32 || 184 | 2026-05-04 | 17:01:45 | A23 | 17:05:50 | C43 |+----------+------------+----------+-------+----------+-------+On remarque que certaines entrées sont correctes: 2min de différence dans un même bâtiment n’est pas un comportement suspect. On peut retirer ces éléments:
JOIN Building b1 ON r1.building_id = b1.building_idJOIN Building b2 ON r2.building_id = b2.building_id[...]WHERE a1.date = a2.date [...] AND b1.building_id != b2.building_idSoit la commande SQL complète (qui commence à être conséquente):
SELECT a1.badge_id, a1.date, a1.time AS time1, r1.room_name AS room1, b1.building_name AS building1, a2.time AS time2, r2.room_name AS room2, b2.building_name AS building2FROM AccessLog a1JOIN AccessLog a2 ON a1.badge_id = a2.badge_id AND a1.log_id < a2.log_idJOIN Room r1 ON a1.room_id = r1.room_idJOIN Room r2 ON a2.room_id = r2.room_idJOIN Building b1 ON r1.building_id = b1.building_idJOIN Building b2 ON r2.building_id = b2.building_idWHERE a1.date = a2.date AND a1.room_id != a2.room_id AND b1.building_id != b2.building_id AND ABS( strftime('%s', a1.date || ' ' || a1.time) - strftime('%s', a2.date || ' ' || a2.time) ) < 360ORDER BY a1.badge_id, a1.date;Qui retourne:
+----------+------------+----------+-------+-----------+----------+-------+-----------+| badge_id | date | time1 | room1 | building1 | time2 | room2 | building2 |+----------+------------+----------+-------+-----------+----------+-------+-----------+| 102 | 2025-11-05 | 13:31:35 | C33 | C | 13:33:45 | B12 | B || 117 | 2025-12-12 | 11:37:24 | D32 | D | 11:40:54 | E21 | E || 118 | 2025-12-18 | 09:51:27 | A22 | A | 09:53:47 | G22 | G || 158 | 2026-01-15 | 17:02:04 | F03 | F | 17:04:32 | E24 | E || 236 | 2026-01-30 | 11:31:49 | D11 | D | 11:35:01 | E25 | E || 248 | 2026-02-06 | 15:18:29 | C35 | C | 15:21:47 | A22 | A || 136 | 2026-02-06 | 15:24:53 | F03 | F | 15:27:21 | E33 | E || 166 | 2026-02-11 | 17:09:31 | B11 | B | 17:12:47 | G22 | G || 90 | 2026-02-18 | 08:07:40 | C33 | C | 08:10:03 | B14 | B || 228 | 2026-03-11 | 11:33:04 | F03 | F | 11:35:15 | E22 | E || 209 | 2026-03-11 | 11:39:50 | F03 | F | 11:42:17 | E31 | E || 206 | 2026-03-20 | 17:04:05 | F32 | F | 17:06:29 | E42 | E || 211 | 2026-04-14 | 17:07:13 | F14 | F | 17:09:30 | E42 | E || 232 | 2026-04-16 | 09:46:41 | C52 | C | 09:49:02 | B32 | B || 159 | 2026-04-16 | 09:48:46 | D22 | D | 09:51:54 | E24 | E || 184 | 2026-05-04 | 17:01:45 | A23 | A | 17:05:50 | C43 | C |+----------+------------+----------+-------+-----------+----------+-------+-----------+On note ces deux lignes:
| 228 | 2026-03-11 | 11:33:04 | F03 | F | 11:35:15 | E22 | E || 209 | 2026-03-11 | 11:39:50 | F03 | F | 11:42:17 | E31 | E |Les
SELECT a1.badge_id, a1.date, a1.time AS time1, r1.room_name AS room1, b1.building_name AS building1, a2.time AS time2, r2.room_name AS room2, b2.building_name AS building2 FROM AccessLog a1 JOIN AccessLog a2 ON a1.badge_id = a2.badge_id AND a1.log_id < a2.log_id JOIN Room r1 ON a1.room_id = r1.room_id JOIN Room r2 ON a2.room_id = r2.room_id JOIN Building b1 ON r1.building_id = b1.building_id JOIN Building b2 ON r2.building_id = b2.building_id WHERE a1.date = a2.date AND a1.room_id != a2.room_id AND b1.building_id != b2.building_id AND ABS( strftime(‘%s’, a1.date || ’ ’ || a1.time)
- strftime(‘%s’, a2.date || ’ ’ || a2.time) ) < 360 ORDER BY a1.badge_id, a1.date;
← Back to blog