- 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 |
||
|---|---|---|
| .. | ||
| solve | ||
| README.md | ||
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
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 |
Step 3 — Decode the state machine logic
- Register 100 = current state (0–6)
- Register 101 = XOR-encoded expected value
- Register 105 = XOR key =
0x0D13 - Decode:
expected = reg[101] XOR 0x0D13 - Write
expectedto 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)
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
ESPILON{m0dbu5_0p3r4t1ng_r00m}



