Solution de tiphergane pour Tunnel Routier 2/2

misc

14 mai 2026

Dans cette deuxième partie de l’épreuve, vous devez envoyer des consignes à l’automate afin d’obtenir des valeurs de process répondant aux normes de sureté (fournie dans la description détaillée), le tout en moins d’une minute. Vous avez également accès à l’interface SCADA en lecture seule via le token récupéré dans la première partie de l’épreuve.

pour la partie 2, là on va commencer à jouer avec les coils et les registers. Pour pouvoir interragir avec un objet, il faut activer les bobines associées (coils), puis envoyer l’information dans le registre (register) qui va bien.

Dans les premiers temps, il va falloir identifier quel couple de bobine/registre va jouer sur la lumière et sur la ventilation. Rapidement, on va découvrir que les bobines de 1 à 8 sont là pour la ventillation et 9 à 15 pour les lumières.

Une petite vérification du code source de la page montre que le 2nd flag sera récupérable via /api/automate une fois les conditions de victoire atteintes. il est temps de faire le script final qui va automatiser tout cela:

#!/usr/bin/env python

from pymodbus.client import ModbusTcpClient
from pymodbus.constants import DeviceInformation
import re
import webbrowser
import requests
import json
import time

from pwn import *

host, port = "tunnel-routier.fcsc.fr", 502
c = ModbusTcpClient(host, port=port)
pattern = "FCSC{.*?}"
Seek = True


def connectToScada():
    c.connect()
    if c.connected:
        success(f"successfully connected to {host}:{port}")
    else:
        warn(f"can't connect to {host}:{port}")
        raise SystemExit


def getToken():
    token = readDeviceInformation()
    site = "https://" + host + "/" + token
    api = "https://" + host + "/api/automate/" + token
    return site, api


def readDeviceInformation():
    r = c.read_device_information(read_code=DeviceInformation.BASIC)
    info(f"{r.information=}")
    decoded_info = {k: v.decode("utf-8") for k, v in r.information.items()}
    js = json.dumps(decoded_info)
    js = json.loads(js)
    token_str = js["1"]
    token = re.search(r"Your token is : ([a-f0-9]+)", token_str).group(1)
    return token


def readInputRegisters(loop):
    r = c.read_input_registers(loop)
    info(r.registers)


def activateFans():
    c.write_coils(1, [1, 1, 1, 1, 1, 1, 1, 1])
    for i in range(8):
        c.write_register(i, 1380)


def closeInputRegisters():
    for i in range(17):
        c.write_register(i, 0)
        c.write_coil(i, 0)


def activateLights():
    c.write_coils(9, [1, 1, 1, 1, 1, 1, 1, 1])
    for i in range(8, 16):
        c.write_register(i, 75)


def openWebsite(site) -> str:
    webbrowser.open(site)


def activateAll():
    activateFans()
    activateLights()


if __name__ == "__main__":
    start = time.time()
    connectToScada()
    site, api = getToken()
    r = requests.session()
    s = r.get(site)
    s = s.content
    flag = re.findall(pattern, str(s))
    for a in flag:
        success(f"Youpi !!! (premier flag):\t{a}")
        write("1/flag.txt",a+"\n")

    activateAll()
    #webbrowser.open(site)
    while Seek:
        s = r.get(api)
        s = s.content
        flag = re.findall(pattern, str(s))
        for a in flag:
            success(f"Youpi !!! (second flag):\t{a}")
            write("2/flag.txt",a+"\n")
            Seek = False
            closeInputRegisters()
            c.close()
            end = time.time()
            diff = end - start
            info(f"Temps passé: {diff:.2f} sec")
            raise SystemExit
        sleep(1)

Cette fois ci, plus d’affichage systématique de la page web, étant donné qu’elle nous mets dans le jus avec ses 5 sec de perte lors du chargement, en passant en full TCP le script a un success rate de 100%.

on execute le script et on récupère nos informations:

[+] successfully connected to tunnel-routier.fcsc.fr:502
[*] r.information={0: b'Siemens AG ', 1: b'S7-300 315-2EH13-0AB0 (Your token is : dea4250f335f383945937a957e320d63)', 2: b'3.0.33'}
[+] Youpi !!! (premier flag):	FCSC{d7e9eb1227eed29c5afc9344bc5733931b511f0faf6d1de2d3c74800688f8180}
[+] Youpi !!! (second flag):	FCSC{66396d7c7030d72e366397c4b561334f66300134e37f389f79a33217ebfef371}
[*] Temps passé: 51.52 sec

Et voilà comment on permet au préfet de visiter le tunnel de Grenelle en toute sécurité !