HTB: Watch Tower Challenge

Watch Tower - HackTheBox Challenge Writeup

Challenge Information

FieldValue
Challenge NameWatch Tower
CategoryForensics (Misc)
DifficultyVery Easy
Authord3vn0mi

Description

An infrastructure monitoring system flagged abnormal behavior and triggered a network capture. The goal is to review the capture, identify what information the intruders collected, and — more importantly — what they altered in the process, since that’s where the flag is hidden.

Solution

The provided artifact was a network capture (tower_logs.pcapng) from an industrial control system (ICS/SCADA) environment. Loading it into tshark showed the entire capture was Modbus/TCP traffic — a common OT/ICS protocol — exchanged between an HMI (Human-Machine Interface) host and a PLC (Programmable Logic Controller).

Breaking the traffic down by Modbus function code revealed three distinct request types:

  • Function 1 — Read Coils (304 occurrences): the “collection” activity the prompt hinted at, the intruder reading device state.
  • Function 16 — Write Multiple Registers (114 occurrences): the “alteration” activity, i.e. actual writes to PLC registers.
  • Function 15 — Write Coils (2 occurrences): a handful of additional writes.

Since the challenge specifically calls out data that was collected and altered, the Write Multiple Registers (func code 16) requests originating from the HMI were the natural place to dig deeper. Rather than the register values being interesting, the register addresses (the reference_num field of each write request) turned out to be the exfiltration channel: read as a sequence and decoded as ASCII, the addresses spelled out the flag. The register values themselves were decoy noise designed to distract from the real payload.

Key Steps

Step 1: Recover the real capture file

The handed-out challenge file was a 0-byte stub. The actual pcapng was inside a password-protected zip in the solve output directory.

Terminal window
# Extract the real capture (password: hackthebox)
unzip -o -P hackthebox a12c73aa-897e-4875-816f-d5cc577609bf.zip
# -> yields tower_logs.pcapng

Step 2: Confirm the protocol and get basic capture info

Terminal window
# Sanity-check the capture is readable and get summary stats
capinfos tower_logs.pcapng

Step 3: Inspect the protocol hierarchy / traffic

Terminal window
# Dump src/dst IPs plus Modbus function code and reference for every frame
tshark -r tower_logs.pcapng -T fields \
-e frame.number -e ip.src -e ip.dst \
-e modbus.func_code -e modbus.reference_num

This confirmed the capture was 100% Modbus/TCP between the HMI and the PLC.

Step 4: Tally function codes to spot “collect vs. alter” activity

Terminal window
# See distribution of Modbus function codes
tshark -r tower_logs.pcapng -T fields -e modbus.func_code | sort | uniq -c | sort -rn

Output showed:

  • 1 → 304 hits (Read Coils — data collection)
  • 16 → 114 hits (Write Multiple Registers — data alteration)
  • 15 → 2 hits (Write Coils)

Step 5: Isolate the Write Multiple Registers (func 16) traffic from the HMI

Terminal window
# Filter to write-multiple-register requests from the HMI host
tshark -r tower_logs.pcapng \
-Y "modbus.func_code==16 && ip.src==192.168.1.150" \
-T fields -e frame.number -e modbus.reference_num

Step 6: Compare register addresses vs. register values

Terminal window
# Dump the actual register VALUES for the same write requests, for comparison
tshark -r tower_logs.pcapng \
-Y "modbus.func_code==16 && ip.src==192.168.1.150" \
-T fields -e modbus.reference_num -e modbus.regval_uint16

The register values looked like arbitrary noise, while the register addresses, taken in packet order and converted from decimal to ASCII, spelled out a readable HTB-flag-shaped string — confirming the addresses were the actual exfiltration/alteration channel.

Step 7: Decode the addresses as ASCII and reconstruct the flag

Terminal window
# Pseudocode for the decode: each modbus.reference_num value maps
# to an ASCII character code; concatenating them in frame order
# reconstructs the flag string.
python3 -c "
addrs = [...] # reference_num values extracted from Step 5, in frame order
print(''.join(chr(a) for a in addrs))
"
Terminal window
printf 'HTB{REDACTED}\n' > flag.txt

Tools Used

ToolPurpose
unzipExtract the password-protected capture archive
tsharkRead and filter the Modbus/TCP pcapng, extract per-field data
capinfosQuick sanity-check / summary of the capture file
python3Decode the extracted register addresses from decimal to ASCII

Key Learnings

  • Industrial protocols like Modbus/TCP have almost no built-in confidentiality or integrity protection — every field, including ones not normally considered “data” (like a register address), is fair game for abuse as a covert channel.
  • When a challenge distinguishes between data collected vs. data altered, map that directly onto protocol semantics: Modbus function codes cleanly separate reads (1 = Read Coils) from writes (16 = Write Multiple Registers), which is a strong signal for where to focus.
  • Don’t assume the “obvious” field carries the payload — here the register value was a decoy, and the real signal was hiding in the address field of the write requests. Always check adjacent fields (addresses, counts, unit IDs) before concluding a field is just noise.
  • Filtering with tshark -Y display filters (modbus.func_code, ip.src, etc.) is fast enough to iterate through several hypotheses directly at the command line without needing a GUI.

Flag

HTB{REDACTED}