Cyber Apocalypse 2025 Writeup - Trial by Fire

· 2min · Juicecat
Table of Contents

Trial by Fire

For this exercise, we were given a url with a webapp and a zip file containing the source code.

Looking at the source code, we see it's a flask webapp inside of a docker image managed by uwsgi and supervisord index.md-2.png When we visit the site, we are greeted by this screen:index.md-3.png

The last line stood out to me. Checking the jinja template for this page in the source code shows us the obvious solution here: template injection: index.md-4.png

So, I now know that template injection is likely the way to victory. What I need now is a field that I can inject a payload into that is not escaped. Luckily enough, there is only one real field that we actually enter into the application- the warrior's username at the start. Searching the source for templates, we see another instance:index.md-5.png So, this seems like an obvious direction to go in. Let's set our username to {{ url_for.__globals__ }}

After playing out the game, we are greeted with this screen- index.md-6.png

Looking at the data, there are several interesting pieces of information:

  1. The application path: /app/venv/lib/python3.12/site-packages/flask/app.py
  2. Access to dangerous functions like eval and exec in the __builtins__ dictionary
  3. Session data showing the injection worked: 'warrior_name': '{{ url_for.__globals__ }}'
  4. Access to the current application context: current_app': <HTB 'application.app'>

So, to find the flag we can:

  • open function via __builtins__
  • os module
  • Various Python execution functions

I ended up with the SSTI payload {{ url_for.__globals__.__builtins__.open('/app/flag.txt').read() }}, which simply just reads the flag. Unfortunately, the web form limits the character name length, so we need to actually use burpsuite to intercept the request and replace our name with the payload: index.md-7.png

Then, after playing out the sequence we are greeted with the flag :) index.md-1.png