I had some free time over the weekend and I decided to jump into a CTF competition to get some practice in. I haven’t heard about PWNSEC yet so I decided to take a look at their CTF and solve a few challenges.

Jinja Mastery was a medium web hacking challenge at PWNSEC CTF 2024. The goal of the challenge was to bypass filters placed on the server-side jinja parser and have a specific test string be rendered by the app.

Getting started

The face of the challenge is rather simple. Just an input field which seems to render the Jinja template string passed into it:

input

However, we’ll clearly see a ton of primities being filtered.

filters

We cannot simply get RCE on the server so I took to the provided source to figure out what I’m dealing with:

code

The main logic of the application isn’t overly complicated, but the filtering is quite restrictive.

Several unsuccessful attempts

I decided to try and push my input through the filters in two separate phases:

  1. We clearly cannot render numbers, so I’ll have to figure out a way to do it
  2. Then I’ll try to create an underline character

First, I tried double URI encoding and cheap tricks like that but nothing worked so I decided to do something controversial … I opened the Jinja documentation …

Specificall, you’ll need the template design documentation: https://jinja.palletsprojects.com/en/stable/templates/

I also got ChatGPT to generate a simple Jinja playground for me to play with and test my payloads while also being able to make simple modifications to it without having to fire up docker.

Naturally, it screwed up, but after some cleaning up the results were satisfying:

from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route("/", methods=["GET", "POST"])
def home():
	if request.method == "POST":
		user_input = request.form.get("input_text", "")

		# Safely render the input text

		return render_template_string(f"""
			<!doctype html>
			<title>Input Display</title>
			<h1>Your input was:</h1>
			<p>{ user_input }</p>
			<a href="/">Back</a>""")

# Render a simple form for GET requests
return """
	<!doctype html>
	<title>Flask Input Form</title>
	<h1>Enter your text:</h1>
	<form method="post">
		<input type="text" name="input_text" required>
		<input type="submit" value="Submit">
	</form>
"""

if __name__ == "__main__":
	app.run(debug=True)

From here I went through almost two hours of attempts to create numbers. Without making this writeup too long, I tried:

  1. Multiple types of encoding and decoding of integers
  2. Turning objects into strings and indexing parts of it to bytes
  3. Turning objects into strings and indexing parts of it to bytes, then encoding / decoding these into integers
  4. Decoding char codes from bytes to int

And many other attempts … I probably read through the entire documentation page like 4 times back2back before figuring out a rather simple trick.

Creating numbers

In the end, the solution to creating numbers came to my mind from JSFuck type challenges.

We know computers can convert between bool and integer values by using 1 and 0 instead of True and False. I tried using this trick here and sure enough, got to creating the first number, by pushing a bool value through the interger converter:

testinputnumber

I then used a trick of creating new variables by reusing old ones to create arbitrary numbers with mathematical expressions:

arbitrarynumber

One step closer to the finish line.

The unerline

I started out here with the same tricks I initially tried when creating numbers. I especially wanted to convert the name of fields of classes and global variables into strings to try and index out the underlines from snake case names.

However, the solution was much more simple.

I just used entity encoding:

entityencode

And with that, we’ve defeated the second and final obstacle

lowbar

Solution

And by combining the previous steps, I got the flag:

final

The solution’s final form is:

fr{%set+x+=+false%}{%set+y+=+x|int%}{%set+a+=+true%}{%set+b+=+a|int%}{%set+c+=+a%2bb%}{%set+d+=+c%2bb%}{%set+e+=+c%2bc%}{%set+f%}%26lowbar%3b{%endset%}{%print(d)%}e{%print(f)%}p{%print(e)%}le$t{%print(b)%}ne%26!

After taking a look at it, my solution is so janky compared to some of the ones I’ve seen in the event’s Discord, but fuck if I care at this point. I finally solved it after wrestling with this all night on a friday.

This was actually rather difficult! I probably didn’t find the “right” solution, but solved it on my own and I’m pretty proud of the thoughts process behind it!

Keep hacking and have fun folks :)

~ r4bbit